From 7f7cf9cd278ce0f3c5781ad80b0feab7c2086379 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 7 Feb 2025 17:05:33 -0500 Subject: [PATCH 1/5] feat: use case for deaccession dataset --- docs/useCases.md | 31 ++++ src/datasets/domain/dtos/DatasetDTO.ts | 5 + .../repositories/IDatasetsRepository.ts | 7 +- .../domain/useCases/DeaccessionDataset.ts | 32 ++++ src/datasets/index.ts | 8 +- .../infra/repositories/DatasetsRepository.ts | 21 ++- .../datasets/DeaccessionDataset.test.ts | 155 ++++++++++++++++++ .../datasets/DatasetsRepository.test.ts | 57 ++++++- test/unit/datasets/DeaccessionDataset.test.ts | 27 +++ 9 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 src/datasets/domain/useCases/DeaccessionDataset.ts create mode 100644 test/functional/datasets/DeaccessionDataset.test.ts create mode 100644 test/unit/datasets/DeaccessionDataset.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 26317d1c..b94fa8b8 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -36,6 +36,7 @@ The different use cases currently available in the package are classified below, - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) - [Publish a Dataset](#publish-a-dataset) + - [Deaccession a Dataset](#deaccession-a-dataset) - [Files](#Files) - [Files read use cases](#files-read-use-cases) - [Get a File](#get-a-file) @@ -753,6 +754,36 @@ The `versionUpdateType` parameter can be a [VersionUpdateType](../src/datasets/d - `VersionUpdateType.MAJOR` - `VersionUpdateType.UPDATE_CURRENT` +#### Deaccesion a Dataset + +Deaccesion a Dataset, given its identifier, version, and deaccessionDatasetDTO to perform. + +##### Example call: + +```typescript +import { deaccessionDataset } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 +const version = ':latestPublished' +const deaccessionDatasetDTO = { + deaccessionReason: 'Description of the deaccession reason.', + deaccessionForwardURL: 'https://demo.dataverse.org' +} + +deaccessionDataset.execute(datasetId, version, deaccessionDatasetDTO) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/DeaccesionDataset.ts) implementation_. +The `datasetId` parameter can be a string for persistent identifiers, or a number for numeric identifiers. +The `version` parameter should be a string or a [DatasetNotNumberedVersion](../src/datasets/domain/models/DatasetNotNumberedVersion.ts) enum value. If not set, the default value is `DatasetNotNumberedVersion.LATEST`. +In `deaccessionDatasetDTO`, deaccessionForwardURL is optional. + +You cannot deaccession a dataset more than once. If you call this endpoint twice for the same dataset version, you will get a not found error on the second call, since the dataset you are looking for will no longer be published since it is already deaccessioned. + ## Files ### Files read use cases diff --git a/src/datasets/domain/dtos/DatasetDTO.ts b/src/datasets/domain/dtos/DatasetDTO.ts index 23d668c9..1543a4ad 100644 --- a/src/datasets/domain/dtos/DatasetDTO.ts +++ b/src/datasets/domain/dtos/DatasetDTO.ts @@ -10,6 +10,11 @@ export interface DatasetMetadataBlockValuesDTO { fields: DatasetMetadataFieldsDTO } +export interface DatasetDeaccessionDTO { + deaccessionReason: string + deaccessionForwardURL?: string +} + export type DatasetMetadataFieldsDTO = Record export type DatasetMetadataFieldValueDTO = diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index c5cd44d4..2433b207 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -3,7 +3,7 @@ import { DatasetLock } from '../models/DatasetLock' import { DatasetPreviewSubset } from '../models/DatasetPreviewSubset' import { DatasetUserPermissions } from '../models/DatasetUserPermissions' import { CreatedDatasetIdentifiers } from '../models/CreatedDatasetIdentifiers' -import { DatasetDTO } from '../dtos/DatasetDTO' +import { DatasetDTO, DatasetDeaccessionDTO } from '../dtos/DatasetDTO' import { MetadataBlock } from '../../../metadataBlocks' import { DatasetVersionDiff } from '../models/DatasetVersionDiff' @@ -45,4 +45,9 @@ export interface IDatasetsRepository { dataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[] ): Promise + deaccessionDataset( + datasetId: number | string, + datasetVersionId: string, + deaccessionDTO: DatasetDeaccessionDTO + ): Promise } diff --git a/src/datasets/domain/useCases/DeaccessionDataset.ts b/src/datasets/domain/useCases/DeaccessionDataset.ts new file mode 100644 index 00000000..894218f7 --- /dev/null +++ b/src/datasets/domain/useCases/DeaccessionDataset.ts @@ -0,0 +1,32 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { DatasetDeaccessionDTO } from '../dtos/DatasetDTO' +import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion' + +export class DeaccessionDataset implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Deaccession a dataset, given a dataset id, a dataset version id, and a DatasetDeaccessionDTO object. + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {string | DatasetNotNumberedVersion} [datasetVersionId=DatasetNotNumberedVersion.LATEST] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. If this parameter is not set, the default value is: DatasetNotNumberedVersion.LATEST + * @param {DatasetDeaccessionDTO} [DatasetDeaccessionDTO] - The DatasetDeaccessionDTO object that contains the deaccession reason and an optional deaccession forward URL. + * @returns A promise that resolves when the dataset is deaccessioned + * @throws An error if the dataset could not be deaccessioned + */ + async execute( + datasetId: number | string, + datasetVersionId: string | DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST, + DatasetDeaccessionDTO: DatasetDeaccessionDTO + ): Promise { + return await this.datasetsRepository.deaccessionDataset( + datasetId, + datasetVersionId, + DatasetDeaccessionDTO + ) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 2eaaed5d..eb08a46b 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -16,6 +16,7 @@ import { MultipleMetadataFieldValidator } from './domain/useCases/validators/Mul import { PublishDataset } from './domain/useCases/PublishDataset' import { UpdateDataset } from './domain/useCases/UpdateDataset' import { GetDatasetVersionDiff } from './domain/useCases/GetDatasetVersionDiff' +import { DeaccessionDataset } from './domain/useCases/DeaccessionDataset' const datasetsRepository = new DatasetsRepository() @@ -46,6 +47,7 @@ const updateDataset = new UpdateDataset( metadataBlocksRepository, datasetResourceValidator ) +const deaccessionDataset = new DeaccessionDataset(datasetsRepository) export { getDataset, @@ -59,7 +61,8 @@ export { getDatasetVersionDiff, publishDataset, createDataset, - updateDataset + updateDataset, + deaccessionDataset } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' @@ -83,7 +86,8 @@ export { DatasetMetadataFieldsDTO, DatasetMetadataFieldValueDTO, DatasetMetadataBlockValuesDTO, - DatasetMetadataChildFieldValueDTO + DatasetMetadataChildFieldValueDTO, + DatasetDeaccessionDTO } from './domain/dtos/DatasetDTO' export { CreatedDatasetIdentifiers } from './domain/models/CreatedDatasetIdentifiers' export { VersionUpdateType } from './domain/models/Dataset' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index a4491291..db39fec2 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -10,7 +10,7 @@ import { transformDatasetUserPermissionsResponseToDatasetUserPermissions } from import { DatasetLock } from '../../domain/models/DatasetLock' import { CreatedDatasetIdentifiers } from '../../domain/models/CreatedDatasetIdentifiers' import { DatasetPreviewSubset } from '../../domain/models/DatasetPreviewSubset' -import { DatasetDTO } from '../../domain/dtos/DatasetDTO' +import { DatasetDTO, DatasetDeaccessionDTO } from '../../domain/dtos/DatasetDTO' import { MetadataBlock } from '../../../metadataBlocks' import { transformDatasetModelToNewDatasetRequestPayload } from './transformers/datasetTransformers' import { transformDatasetLocksResponseToDatasetLocks } from './transformers/datasetLocksTransformers' @@ -214,4 +214,23 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async deaccessionDataset( + datasetId: string | number, + datasetVersionId: string, + deaccessionDTO: DatasetDeaccessionDTO + ): Promise { + return this.doPost( + this.buildApiEndpoint( + this.datasetsResourceName, + `versions/${datasetVersionId}/deaccession`, + datasetId + ), + deaccessionDTO + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/test/functional/datasets/DeaccessionDataset.test.ts b/test/functional/datasets/DeaccessionDataset.test.ts new file mode 100644 index 00000000..4c0a882a --- /dev/null +++ b/test/functional/datasets/DeaccessionDataset.test.ts @@ -0,0 +1,155 @@ +import { + deaccessionDataset, + DatasetDeaccessionDTO, + createDataset, + publishDataset, + VersionUpdateType +} from '../../../src/datasets' +import { ApiConfig, WriteError } from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { + waitForNoLocks, + deletePublishedDatasetViaApi, + deleteUnpublishedDatasetViaApi +} from '../../testHelpers/datasets/datasetHelper' + +const testDataset = { + license: { + name: 'CC0 1.0', + uri: 'http://creativecommons.org/publicdomain/zero/1.0', + iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png' + }, + metadataBlockValues: [ + { + name: 'citation', + fields: { + title: 'Dataset created using the createDataset use case', + author: [ + { + authorName: 'Admin, Dataverse', + authorAffiliation: 'Dataverse.org' + }, + { + authorName: 'Owner, Dataverse', + authorAffiliation: 'Dataversedemo.org' + } + ], + datasetContact: [ + { + datasetContactEmail: 'finch@mailinator.com', + datasetContactName: 'Finch, Fiona' + } + ], + dsDescription: [ + { + dsDescriptionValue: 'This is the description of the dataset.' + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } + ] +} + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should deaccession a dataset when required fields are sent', async () => { + const createdDatasetIdentifiers = await createDataset.execute(testDataset) + + const response = await publishDataset.execute( + createdDatasetIdentifiers.persistentId, + VersionUpdateType.MAJOR + ) + await waitForNoLocks(createdDatasetIdentifiers.numericId, 10) + + expect(response).toBeUndefined() + + const testDeaccessionDataset: DatasetDeaccessionDTO = { + deaccessionReason: 'Description of the deaccession reason.', + deaccessionForwardURL: 'https://demo.dataverse.org' + } + + const actual = await deaccessionDataset.execute( + createdDatasetIdentifiers.numericId, + '1.0', + testDeaccessionDataset + ) + + expect(actual).toBeUndefined() + + await deletePublishedDatasetViaApi(createdDatasetIdentifiers.persistentId) + }) + + test('should throw an error when the dataset id is incorrect', async () => { + const createdDatasetIdentifiers = await createDataset.execute(testDataset) + + const testDeaccessionDataset: DatasetDeaccessionDTO = { + deaccessionReason: 'Description of the deaccession reason.', + deaccessionForwardURL: 'https://demo.dataverse.org' + } + + await expect( + deaccessionDataset.execute( + createdDatasetIdentifiers.numericId, + ':latest-published', + testDeaccessionDataset + ) + ).rejects.toThrow(Error) + + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + }) + + test('should not deaccession a dataset when it is not published', async () => { + const createdDatasetIdentifiers = await createDataset.execute(testDataset) + const testDeaccessionDataset: DatasetDeaccessionDTO = { + deaccessionReason: 'Description of the deaccession reason.' + } + + await expect( + deaccessionDataset.execute( + createdDatasetIdentifiers.numericId, + ':latest-published', + testDeaccessionDataset + ) + ).rejects.toBeInstanceOf(WriteError) + }) + + test('should deaccession a dataset when it is deaccessioned once', async () => { + const createdDatasetIdentifiers = await createDataset.execute(testDataset) + + const response = await publishDataset.execute( + createdDatasetIdentifiers.persistentId, + VersionUpdateType.MAJOR + ) + await waitForNoLocks(createdDatasetIdentifiers.numericId, 10) + + expect(response).toBeUndefined() + + const testDeaccessionDataset: DatasetDeaccessionDTO = { + deaccessionReason: 'Description of the deaccession reason.', + deaccessionForwardURL: 'https://demo.dataverse.org' + } + + const actual = await deaccessionDataset.execute( + createdDatasetIdentifiers.numericId, + '1.0', + testDeaccessionDataset + ) + + expect(actual).toBeUndefined() + + await expect( + deaccessionDataset.execute(createdDatasetIdentifiers.numericId, '1.0', testDeaccessionDataset) + ).rejects.toThrow(Error) + + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + }) +}) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index c1d6aa85..d927ce42 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -17,7 +17,8 @@ import { VersionUpdateType, createDataset, CreatedDatasetIdentifiers, - DatasetDTO + DatasetDTO, + DatasetDeaccessionDTO } from '../../../src/datasets' import { ApiConfig, WriteError } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -866,4 +867,58 @@ describe('DatasetsRepository', () => { ).rejects.toThrow(expectedError) }) }) + + describe('deaccessionDataset', () => { + test('should deaccession a dataset', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + + const deaccessionDTO: DatasetDeaccessionDTO = { + deaccessionReason: 'Deaccessioning the dataset for testing purposes' + } + + const actual = await sut.deaccessionDataset(testDatasetIds.numericId, '1.0', deaccessionDTO) + + expect(actual).toBeUndefined() + }) + + test('should return error when dataset is deaccessioned', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + + const deaccessionDTO: DatasetDeaccessionDTO = { + deaccessionReason: 'Deaccessioning the dataset for testing purposes' + } + + const actual = await sut.deaccessionDataset(testDatasetIds.numericId, '1.0', deaccessionDTO) + + expect(actual).toBeUndefined() + + await expect( + sut.deaccessionDataset(testDatasetIds.numericId, '1.0', deaccessionDTO) + ).rejects.toBeInstanceOf(WriteError) + }) + + test('should return error when dataset is not published', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + const deaccessionDTO: DatasetDeaccessionDTO = { + deaccessionReason: 'Deaccessioning the dataset for testing purposes' + } + + await expect( + sut.deaccessionDataset(testDatasetIds.numericId, ':latest-published', deaccessionDTO) + ).rejects.toBeInstanceOf(WriteError) + }) + + test('should return error when dataset does not exist', async () => { + await expect( + sut.deaccessionDataset(nonExistentTestDatasetId, '1.0', { + deaccessionReason: 'Deaccessioning the dataset for testing purposes' + }) + ).rejects.toBeInstanceOf(WriteError) + }) + }) }) diff --git a/test/unit/datasets/DeaccessionDataset.test.ts b/test/unit/datasets/DeaccessionDataset.test.ts new file mode 100644 index 00000000..a116e2b4 --- /dev/null +++ b/test/unit/datasets/DeaccessionDataset.test.ts @@ -0,0 +1,27 @@ +import { DeaccessionDataset } from '../../../src/datasets/domain/useCases/DeaccessionDataset' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { WriteError, DatasetDeaccessionDTO } from '../../../src' + +const deaccessionDatasetDTO: DatasetDeaccessionDTO = { + deaccessionReason: 'Deaccessioning the dataset for testing purposes' +} + +describe('execute', () => { + test('should return undefined on repository success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.deaccessionDataset = jest.fn().mockResolvedValue(undefined) + const sut = new DeaccessionDataset(datasetsRepositoryStub) + + const actual = await sut.execute(1, '1.0', deaccessionDatasetDTO) + + expect(actual).toEqual(undefined) + }) + + test('should return error result on repository error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.deaccessionDataset = jest.fn().mockRejectedValue(new WriteError()) + const sut = new DeaccessionDataset(datasetsRepositoryStub) + + await expect(sut.execute(111, '1.0', deaccessionDatasetDTO)).rejects.toThrow(WriteError) + }) +}) From 49e739393f8e62e5fede41e1000a715353117d2a Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 7 Feb 2025 17:19:48 -0500 Subject: [PATCH 2/5] fix: minor changes --- docs/useCases.md | 3 +-- src/datasets/domain/useCases/DeaccessionDataset.ts | 5 ++--- test/functional/datasets/DeaccessionDataset.test.ts | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index b94fa8b8..f4444f04 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -779,8 +779,7 @@ deaccessionDataset.execute(datasetId, version, deaccessionDatasetDTO) _See [use case](../src/datasets/domain/useCases/DeaccesionDataset.ts) implementation_. The `datasetId` parameter can be a string for persistent identifiers, or a number for numeric identifiers. -The `version` parameter should be a string or a [DatasetNotNumberedVersion](../src/datasets/domain/models/DatasetNotNumberedVersion.ts) enum value. If not set, the default value is `DatasetNotNumberedVersion.LATEST`. -In `deaccessionDatasetDTO`, deaccessionForwardURL is optional. +The `version` parameter should be a string or a [DatasetNotNumberedVersion](../src/datasets/domain/models/DatasetNotNumberedVersion.ts) enum value. You cannot deaccession a dataset more than once. If you call this endpoint twice for the same dataset version, you will get a not found error on the second call, since the dataset you are looking for will no longer be published since it is already deaccessioned. diff --git a/src/datasets/domain/useCases/DeaccessionDataset.ts b/src/datasets/domain/useCases/DeaccessionDataset.ts index 894218f7..f0ab7e02 100644 --- a/src/datasets/domain/useCases/DeaccessionDataset.ts +++ b/src/datasets/domain/useCases/DeaccessionDataset.ts @@ -13,14 +13,13 @@ export class DeaccessionDataset implements UseCase { /** * Deaccession a dataset, given a dataset id, a dataset version id, and a DatasetDeaccessionDTO object. * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). - * @param {string | DatasetNotNumberedVersion} [datasetVersionId=DatasetNotNumberedVersion.LATEST] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. If this parameter is not set, the default value is: DatasetNotNumberedVersion.LATEST - * @param {DatasetDeaccessionDTO} [DatasetDeaccessionDTO] - The DatasetDeaccessionDTO object that contains the deaccession reason and an optional deaccession forward URL. + * @param {string | DatasetNotNumberedVersion} [datasetVersionId] - The dataset version identifier, which can be a version-specific numeric string (for example, 1.0) or a DatasetNotNumberedVersion enum value. * @returns A promise that resolves when the dataset is deaccessioned * @throws An error if the dataset could not be deaccessioned */ async execute( datasetId: number | string, - datasetVersionId: string | DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST, + datasetVersionId: string | DatasetNotNumberedVersion, DatasetDeaccessionDTO: DatasetDeaccessionDTO ): Promise { return await this.datasetsRepository.deaccessionDataset( diff --git a/test/functional/datasets/DeaccessionDataset.test.ts b/test/functional/datasets/DeaccessionDataset.test.ts index 4c0a882a..fc74ebbe 100644 --- a/test/functional/datasets/DeaccessionDataset.test.ts +++ b/test/functional/datasets/DeaccessionDataset.test.ts @@ -120,6 +120,8 @@ describe('execute', () => { testDeaccessionDataset ) ).rejects.toBeInstanceOf(WriteError) + + await deletePublishedDatasetViaApi(createdDatasetIdentifiers.persistentId) }) test('should deaccession a dataset when it is deaccessioned once', async () => { From 109870025dfecb0f79bf0dbc001c8d40d3915798 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 7 Feb 2025 18:19:55 -0500 Subject: [PATCH 3/5] fix: minor change in use case --- .../datasets/DeaccessionDataset.test.ts | 4 +- test/testHelpers/datasets/datasetHelper.ts | 9 +++- test/unit/datasets/DatasetsRepository.test.ts | 54 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/test/functional/datasets/DeaccessionDataset.test.ts b/test/functional/datasets/DeaccessionDataset.test.ts index fc74ebbe..721b40d0 100644 --- a/test/functional/datasets/DeaccessionDataset.test.ts +++ b/test/functional/datasets/DeaccessionDataset.test.ts @@ -121,7 +121,7 @@ describe('execute', () => { ) ).rejects.toBeInstanceOf(WriteError) - await deletePublishedDatasetViaApi(createdDatasetIdentifiers.persistentId) + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) }) test('should deaccession a dataset when it is deaccessioned once', async () => { @@ -152,6 +152,6 @@ describe('execute', () => { deaccessionDataset.execute(createdDatasetIdentifiers.numericId, '1.0', testDeaccessionDataset) ).rejects.toThrow(Error) - await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + await deletePublishedDatasetViaApi(createdDatasetIdentifiers.persistentId) }) }) diff --git a/test/testHelpers/datasets/datasetHelper.ts b/test/testHelpers/datasets/datasetHelper.ts index 50e4e1d1..0d4c9fb3 100644 --- a/test/testHelpers/datasets/datasetHelper.ts +++ b/test/testHelpers/datasets/datasetHelper.ts @@ -10,7 +10,8 @@ import { DvObjectType } from '../../../src/core/domain/models/DvObjectOwnerNode' import TurndownService from 'turndown' import { DatasetDTO, - DatasetMetadataFieldValueDTO + DatasetMetadataFieldValueDTO, + DatasetDeaccessionDTO } from '../../../src/datasets/domain/dtos/DatasetDTO' import { MetadataBlock, MetadataFieldType } from '../../../src' import { @@ -788,3 +789,9 @@ export const createUpdateDatasetRequestPayload = (): UpdateDatasetRequestPayload ] } } + +export const createDatasetDeaccessionDTO = (): DatasetDeaccessionDTO => { + return { + deaccessionReason: 'Test reason.' + } +} diff --git a/test/unit/datasets/DatasetsRepository.test.ts b/test/unit/datasets/DatasetsRepository.test.ts index 55671c8b..8b7f8c98 100644 --- a/test/unit/datasets/DatasetsRepository.test.ts +++ b/test/unit/datasets/DatasetsRepository.test.ts @@ -24,6 +24,7 @@ import { } from '../../testHelpers/datasets/datasetPreviewHelper' import { createDatasetDTO, + createDatasetDeaccessionDTO, createDatasetMetadataBlockModel, createNewDatasetRequestPayload } from '../../testHelpers/datasets/datasetHelper' @@ -902,4 +903,57 @@ describe('DatasetsRepository', () => { expect(error).toBeInstanceOf(Error) }) }) + + describe('deaccsionDataset', () => { + const version = '1.0' + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/datasets/${testDatasetModel.id}/versions/${version}/deaccession` + const expectedApiKeyRequestConfig = { + ...TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + } + const testDeaccessionDataset = createDatasetDeaccessionDTO() + const testDeaccessionDatasetJSON = JSON.stringify(testDeaccessionDataset) + test('should return nothing when providing id, version update type and response is successful', async () => { + jest.spyOn(axios, 'post').mockResolvedValue(undefined) + + let actual = await sut.deaccessionDataset( + testDatasetModel.id, + version, + testDeaccessionDataset + ) + + expect(axios.post).toHaveBeenCalledWith( + expectedApiEndpoint, + testDeaccessionDatasetJSON, + expectedApiKeyRequestConfig + ) + expect(actual).toBeUndefined() + + ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) + + actual = await sut.deaccessionDataset(testDatasetModel.id, version, testDeaccessionDataset) + + expect(axios.post).toHaveBeenCalledWith( + expectedApiEndpoint, + testDeaccessionDatasetJSON, + expectedApiKeyRequestConfig + ) + expect(actual).toBeUndefined() + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'post').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + let error: WriteError | undefined = undefined + await sut + .deaccessionDataset(testDatasetModel.id, version, testDeaccessionDataset) + .catch((e) => (error = e)) + + expect(axios.post).toHaveBeenCalledWith( + expectedApiEndpoint, + testDeaccessionDatasetJSON, + expectedApiKeyRequestConfig + ) + expect(error).toBeInstanceOf(Error) + }) + }) }) From c9e082940c4f1281103cbfc371203a12245f4164 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 10 Feb 2025 11:05:14 -0500 Subject: [PATCH 4/5] fix: typo and add a test --- docs/useCases.md | 6 ++--- src/datasets/domain/dtos/DatasetDTO.ts | 5 ---- .../domain/dtos/DatasetDeaccessionDTO.ts | 4 ++++ .../domain/useCases/DeaccessionDataset.ts | 2 +- src/datasets/index.ts | 4 ++-- .../infra/repositories/DatasetsRepository.ts | 3 ++- .../datasets/DeaccessionDataset.test.ts | 24 +++++++++++-------- .../datasets/DatasetsRepository.test.ts | 4 ++++ test/testHelpers/datasets/datasetHelper.ts | 4 ++-- test/unit/datasets/DatasetsRepository.test.ts | 10 ++++---- 10 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 src/datasets/domain/dtos/DatasetDeaccessionDTO.ts diff --git a/docs/useCases.md b/docs/useCases.md index f4444f04..b4bca638 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -754,9 +754,9 @@ The `versionUpdateType` parameter can be a [VersionUpdateType](../src/datasets/d - `VersionUpdateType.MAJOR` - `VersionUpdateType.UPDATE_CURRENT` -#### Deaccesion a Dataset +#### Deaccession a Dataset -Deaccesion a Dataset, given its identifier, version, and deaccessionDatasetDTO to perform. +Deaccession a Dataset, given its identifier, version, and deaccessionDatasetDTO to perform. ##### Example call: @@ -777,7 +777,7 @@ deaccessionDataset.execute(datasetId, version, deaccessionDatasetDTO) /* ... */ ``` -_See [use case](../src/datasets/domain/useCases/DeaccesionDataset.ts) implementation_. +_See [use case](../src/datasets/domain/useCases/DeaccessionDataset.ts) implementation_. The `datasetId` parameter can be a string for persistent identifiers, or a number for numeric identifiers. The `version` parameter should be a string or a [DatasetNotNumberedVersion](../src/datasets/domain/models/DatasetNotNumberedVersion.ts) enum value. diff --git a/src/datasets/domain/dtos/DatasetDTO.ts b/src/datasets/domain/dtos/DatasetDTO.ts index 1543a4ad..23d668c9 100644 --- a/src/datasets/domain/dtos/DatasetDTO.ts +++ b/src/datasets/domain/dtos/DatasetDTO.ts @@ -10,11 +10,6 @@ export interface DatasetMetadataBlockValuesDTO { fields: DatasetMetadataFieldsDTO } -export interface DatasetDeaccessionDTO { - deaccessionReason: string - deaccessionForwardURL?: string -} - export type DatasetMetadataFieldsDTO = Record export type DatasetMetadataFieldValueDTO = diff --git a/src/datasets/domain/dtos/DatasetDeaccessionDTO.ts b/src/datasets/domain/dtos/DatasetDeaccessionDTO.ts new file mode 100644 index 00000000..1ebcfeed --- /dev/null +++ b/src/datasets/domain/dtos/DatasetDeaccessionDTO.ts @@ -0,0 +1,4 @@ +export interface DatasetDeaccessionDTO { + deaccessionReason: string + deaccessionForwardURL?: string +} diff --git a/src/datasets/domain/useCases/DeaccessionDataset.ts b/src/datasets/domain/useCases/DeaccessionDataset.ts index f0ab7e02..94144c56 100644 --- a/src/datasets/domain/useCases/DeaccessionDataset.ts +++ b/src/datasets/domain/useCases/DeaccessionDataset.ts @@ -1,6 +1,6 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { IDatasetsRepository } from '../repositories/IDatasetsRepository' -import { DatasetDeaccessionDTO } from '../dtos/DatasetDTO' +import { DatasetDeaccessionDTO } from '../dtos/DatasetDeaccessionDTO' import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion' export class DeaccessionDataset implements UseCase { diff --git a/src/datasets/index.ts b/src/datasets/index.ts index eb08a46b..b13e8aad 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -86,8 +86,8 @@ export { DatasetMetadataFieldsDTO, DatasetMetadataFieldValueDTO, DatasetMetadataBlockValuesDTO, - DatasetMetadataChildFieldValueDTO, - DatasetDeaccessionDTO + DatasetMetadataChildFieldValueDTO } from './domain/dtos/DatasetDTO' +export { DatasetDeaccessionDTO } from './domain/dtos/DatasetDeaccessionDTO' export { CreatedDatasetIdentifiers } from './domain/models/CreatedDatasetIdentifiers' export { VersionUpdateType } from './domain/models/Dataset' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index db39fec2..5fd02917 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -10,7 +10,8 @@ import { transformDatasetUserPermissionsResponseToDatasetUserPermissions } from import { DatasetLock } from '../../domain/models/DatasetLock' import { CreatedDatasetIdentifiers } from '../../domain/models/CreatedDatasetIdentifiers' import { DatasetPreviewSubset } from '../../domain/models/DatasetPreviewSubset' -import { DatasetDTO, DatasetDeaccessionDTO } from '../../domain/dtos/DatasetDTO' +import { DatasetDTO } from '../../domain/dtos/DatasetDTO' +import { DatasetDeaccessionDTO } from '../../domain/dtos/DatasetDeaccessionDTO' import { MetadataBlock } from '../../../metadataBlocks' import { transformDatasetModelToNewDatasetRequestPayload } from './transformers/datasetTransformers' import { transformDatasetLocksResponseToDatasetLocks } from './transformers/datasetLocksTransformers' diff --git a/test/functional/datasets/DeaccessionDataset.test.ts b/test/functional/datasets/DeaccessionDataset.test.ts index 721b40d0..102b2c89 100644 --- a/test/functional/datasets/DeaccessionDataset.test.ts +++ b/test/functional/datasets/DeaccessionDataset.test.ts @@ -72,7 +72,7 @@ describe('execute', () => { expect(response).toBeUndefined() - const testDeaccessionDataset: DatasetDeaccessionDTO = { + const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = { deaccessionReason: 'Description of the deaccession reason.', deaccessionForwardURL: 'https://demo.dataverse.org' } @@ -80,7 +80,7 @@ describe('execute', () => { const actual = await deaccessionDataset.execute( createdDatasetIdentifiers.numericId, '1.0', - testDeaccessionDataset + testDeaccessionDatasetDTO ) expect(actual).toBeUndefined() @@ -91,7 +91,7 @@ describe('execute', () => { test('should throw an error when the dataset id is incorrect', async () => { const createdDatasetIdentifiers = await createDataset.execute(testDataset) - const testDeaccessionDataset: DatasetDeaccessionDTO = { + const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = { deaccessionReason: 'Description of the deaccession reason.', deaccessionForwardURL: 'https://demo.dataverse.org' } @@ -100,7 +100,7 @@ describe('execute', () => { deaccessionDataset.execute( createdDatasetIdentifiers.numericId, ':latest-published', - testDeaccessionDataset + testDeaccessionDatasetDTO ) ).rejects.toThrow(Error) @@ -109,7 +109,7 @@ describe('execute', () => { test('should not deaccession a dataset when it is not published', async () => { const createdDatasetIdentifiers = await createDataset.execute(testDataset) - const testDeaccessionDataset: DatasetDeaccessionDTO = { + const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = { deaccessionReason: 'Description of the deaccession reason.' } @@ -117,14 +117,14 @@ describe('execute', () => { deaccessionDataset.execute( createdDatasetIdentifiers.numericId, ':latest-published', - testDeaccessionDataset + testDeaccessionDatasetDTO ) ).rejects.toBeInstanceOf(WriteError) await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) }) - test('should deaccession a dataset when it is deaccessioned once', async () => { + test('should not deaccession a dataset when it is deaccessioned once', async () => { const createdDatasetIdentifiers = await createDataset.execute(testDataset) const response = await publishDataset.execute( @@ -135,7 +135,7 @@ describe('execute', () => { expect(response).toBeUndefined() - const testDeaccessionDataset: DatasetDeaccessionDTO = { + const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = { deaccessionReason: 'Description of the deaccession reason.', deaccessionForwardURL: 'https://demo.dataverse.org' } @@ -143,13 +143,17 @@ describe('execute', () => { const actual = await deaccessionDataset.execute( createdDatasetIdentifiers.numericId, '1.0', - testDeaccessionDataset + testDeaccessionDatasetDTO ) expect(actual).toBeUndefined() await expect( - deaccessionDataset.execute(createdDatasetIdentifiers.numericId, '1.0', testDeaccessionDataset) + deaccessionDataset.execute( + createdDatasetIdentifiers.numericId, + '1.0', + testDeaccessionDatasetDTO + ) ).rejects.toThrow(Error) await deletePublishedDatasetViaApi(createdDatasetIdentifiers.persistentId) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index d927ce42..1f8add69 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -881,6 +881,10 @@ describe('DatasetsRepository', () => { const actual = await sut.deaccessionDataset(testDatasetIds.numericId, '1.0', deaccessionDTO) expect(actual).toBeUndefined() + + const dataset = await sut.getDataset(testDatasetIds.numericId, '1.0', true, false) + + expect(dataset.versionInfo.state).toBe('DEACCESSIONED') }) test('should return error when dataset is deaccessioned', async () => { diff --git a/test/testHelpers/datasets/datasetHelper.ts b/test/testHelpers/datasets/datasetHelper.ts index 0d4c9fb3..84935471 100644 --- a/test/testHelpers/datasets/datasetHelper.ts +++ b/test/testHelpers/datasets/datasetHelper.ts @@ -10,9 +10,9 @@ import { DvObjectType } from '../../../src/core/domain/models/DvObjectOwnerNode' import TurndownService from 'turndown' import { DatasetDTO, - DatasetMetadataFieldValueDTO, - DatasetDeaccessionDTO + DatasetMetadataFieldValueDTO } from '../../../src/datasets/domain/dtos/DatasetDTO' +import { DatasetDeaccessionDTO } from '../../../src/datasets/domain/dtos/DatasetDeaccessionDTO' import { MetadataBlock, MetadataFieldType } from '../../../src' import { NewDatasetRequestPayload, diff --git a/test/unit/datasets/DatasetsRepository.test.ts b/test/unit/datasets/DatasetsRepository.test.ts index 8b7f8c98..64a1e464 100644 --- a/test/unit/datasets/DatasetsRepository.test.ts +++ b/test/unit/datasets/DatasetsRepository.test.ts @@ -910,15 +910,15 @@ describe('DatasetsRepository', () => { const expectedApiKeyRequestConfig = { ...TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY } - const testDeaccessionDataset = createDatasetDeaccessionDTO() - const testDeaccessionDatasetJSON = JSON.stringify(testDeaccessionDataset) + const testDeaccessionDatasetDTO = createDatasetDeaccessionDTO() + const testDeaccessionDatasetJSON = JSON.stringify(testDeaccessionDatasetDTO) test('should return nothing when providing id, version update type and response is successful', async () => { jest.spyOn(axios, 'post').mockResolvedValue(undefined) let actual = await sut.deaccessionDataset( testDatasetModel.id, version, - testDeaccessionDataset + testDeaccessionDatasetDTO ) expect(axios.post).toHaveBeenCalledWith( @@ -930,7 +930,7 @@ describe('DatasetsRepository', () => { ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) - actual = await sut.deaccessionDataset(testDatasetModel.id, version, testDeaccessionDataset) + actual = await sut.deaccessionDataset(testDatasetModel.id, version, testDeaccessionDatasetDTO) expect(axios.post).toHaveBeenCalledWith( expectedApiEndpoint, @@ -945,7 +945,7 @@ describe('DatasetsRepository', () => { let error: WriteError | undefined = undefined await sut - .deaccessionDataset(testDatasetModel.id, version, testDeaccessionDataset) + .deaccessionDataset(testDatasetModel.id, version, testDeaccessionDatasetDTO) .catch((e) => (error = e)) expect(axios.post).toHaveBeenCalledWith( From 5d575257cfe27818b30d4bcfcf9014664ebe6f6e Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 10 Feb 2025 11:10:05 -0500 Subject: [PATCH 5/5] fix: lint --- src/datasets/domain/repositories/IDatasetsRepository.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 2433b207..dd2b954d 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -3,7 +3,8 @@ import { DatasetLock } from '../models/DatasetLock' import { DatasetPreviewSubset } from '../models/DatasetPreviewSubset' import { DatasetUserPermissions } from '../models/DatasetUserPermissions' import { CreatedDatasetIdentifiers } from '../models/CreatedDatasetIdentifiers' -import { DatasetDTO, DatasetDeaccessionDTO } from '../dtos/DatasetDTO' +import { DatasetDTO } from '../dtos/DatasetDTO' +import { DatasetDeaccessionDTO } from '../dtos/DatasetDeaccessionDTO' import { MetadataBlock } from '../../../metadataBlocks' import { DatasetVersionDiff } from '../models/DatasetVersionDiff'