Skip to content

Commit 022c5ec

Browse files
authored
Merge pull request #255 from IQSS/246-implement-use-case-for-deaccession-a-dataset
Implement Use Case for Deaccession Dataset
2 parents 67d880f + 5d57525 commit 022c5ec

File tree

11 files changed

+405
-2
lines changed

11 files changed

+405
-2
lines changed

docs/useCases.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The different use cases currently available in the package are classified below,
3636
- [Create a Dataset](#create-a-dataset)
3737
- [Update a Dataset](#update-a-dataset)
3838
- [Publish a Dataset](#publish-a-dataset)
39+
- [Deaccession a Dataset](#deaccession-a-dataset)
3940
- [Files](#Files)
4041
- [Files read use cases](#files-read-use-cases)
4142
- [Get a File](#get-a-file)
@@ -756,6 +757,35 @@ The `versionUpdateType` parameter can be a [VersionUpdateType](../src/datasets/d
756757
- `VersionUpdateType.MAJOR`
757758
- `VersionUpdateType.UPDATE_CURRENT`
758759

760+
#### Deaccession a Dataset
761+
762+
Deaccession a Dataset, given its identifier, version, and deaccessionDatasetDTO to perform.
763+
764+
##### Example call:
765+
766+
```typescript
767+
import { deaccessionDataset } from '@iqss/dataverse-client-javascript'
768+
769+
/* ... */
770+
771+
const datasetId = 1
772+
const version = ':latestPublished'
773+
const deaccessionDatasetDTO = {
774+
deaccessionReason: 'Description of the deaccession reason.',
775+
deaccessionForwardURL: 'https://demo.dataverse.org'
776+
}
777+
778+
deaccessionDataset.execute(datasetId, version, deaccessionDatasetDTO)
779+
780+
/* ... */
781+
```
782+
783+
_See [use case](../src/datasets/domain/useCases/DeaccessionDataset.ts) implementation_.
784+
The `datasetId` parameter can be a string for persistent identifiers, or a number for numeric identifiers.
785+
The `version` parameter should be a string or a [DatasetNotNumberedVersion](../src/datasets/domain/models/DatasetNotNumberedVersion.ts) enum value.
786+
787+
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.
788+
759789
## Files
760790

761791
### Files read use cases
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface DatasetDeaccessionDTO {
2+
deaccessionReason: string
3+
deaccessionForwardURL?: string
4+
}

src/datasets/domain/repositories/IDatasetsRepository.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DatasetPreviewSubset } from '../models/DatasetPreviewSubset'
44
import { DatasetUserPermissions } from '../models/DatasetUserPermissions'
55
import { CreatedDatasetIdentifiers } from '../models/CreatedDatasetIdentifiers'
66
import { DatasetDTO } from '../dtos/DatasetDTO'
7+
import { DatasetDeaccessionDTO } from '../dtos/DatasetDeaccessionDTO'
78
import { MetadataBlock } from '../../../metadataBlocks'
89
import { DatasetVersionDiff } from '../models/DatasetVersionDiff'
910

@@ -45,4 +46,9 @@ export interface IDatasetsRepository {
4546
dataset: DatasetDTO,
4647
datasetMetadataBlocks: MetadataBlock[]
4748
): Promise<void>
49+
deaccessionDataset(
50+
datasetId: number | string,
51+
datasetVersionId: string,
52+
deaccessionDTO: DatasetDeaccessionDTO
53+
): Promise<void>
4854
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { IDatasetsRepository } from '../repositories/IDatasetsRepository'
3+
import { DatasetDeaccessionDTO } from '../dtos/DatasetDeaccessionDTO'
4+
import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion'
5+
6+
export class DeaccessionDataset implements UseCase<void> {
7+
private datasetsRepository: IDatasetsRepository
8+
9+
constructor(datasetsRepository: IDatasetsRepository) {
10+
this.datasetsRepository = datasetsRepository
11+
}
12+
13+
/**
14+
* Deaccession a dataset, given a dataset id, a dataset version id, and a DatasetDeaccessionDTO object.
15+
* @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
16+
* @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.
17+
* @returns A promise that resolves when the dataset is deaccessioned
18+
* @throws An error if the dataset could not be deaccessioned
19+
*/
20+
async execute(
21+
datasetId: number | string,
22+
datasetVersionId: string | DatasetNotNumberedVersion,
23+
DatasetDeaccessionDTO: DatasetDeaccessionDTO
24+
): Promise<void> {
25+
return await this.datasetsRepository.deaccessionDataset(
26+
datasetId,
27+
datasetVersionId,
28+
DatasetDeaccessionDTO
29+
)
30+
}
31+
}

src/datasets/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { MultipleMetadataFieldValidator } from './domain/useCases/validators/Mul
1616
import { PublishDataset } from './domain/useCases/PublishDataset'
1717
import { UpdateDataset } from './domain/useCases/UpdateDataset'
1818
import { GetDatasetVersionDiff } from './domain/useCases/GetDatasetVersionDiff'
19+
import { DeaccessionDataset } from './domain/useCases/DeaccessionDataset'
1920

2021
const datasetsRepository = new DatasetsRepository()
2122

@@ -46,6 +47,7 @@ const updateDataset = new UpdateDataset(
4647
metadataBlocksRepository,
4748
datasetResourceValidator
4849
)
50+
const deaccessionDataset = new DeaccessionDataset(datasetsRepository)
4951

5052
export {
5153
getDataset,
@@ -59,7 +61,8 @@ export {
5961
getDatasetVersionDiff,
6062
publishDataset,
6163
createDataset,
62-
updateDataset
64+
updateDataset,
65+
deaccessionDataset
6366
}
6467
export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion'
6568
export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions'
@@ -86,5 +89,6 @@ export {
8689
DatasetMetadataBlockValuesDTO,
8790
DatasetMetadataChildFieldValueDTO
8891
} from './domain/dtos/DatasetDTO'
92+
export { DatasetDeaccessionDTO } from './domain/dtos/DatasetDeaccessionDTO'
8993
export { CreatedDatasetIdentifiers } from './domain/models/CreatedDatasetIdentifiers'
9094
export { VersionUpdateType } from './domain/models/Dataset'

src/datasets/infra/repositories/DatasetsRepository.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DatasetLock } from '../../domain/models/DatasetLock'
1111
import { CreatedDatasetIdentifiers } from '../../domain/models/CreatedDatasetIdentifiers'
1212
import { DatasetPreviewSubset } from '../../domain/models/DatasetPreviewSubset'
1313
import { DatasetDTO } from '../../domain/dtos/DatasetDTO'
14+
import { DatasetDeaccessionDTO } from '../../domain/dtos/DatasetDeaccessionDTO'
1415
import { MetadataBlock } from '../../../metadataBlocks'
1516
import { transformDatasetModelToNewDatasetRequestPayload } from './transformers/datasetTransformers'
1617
import { transformDatasetLocksResponseToDatasetLocks } from './transformers/datasetLocksTransformers'
@@ -214,4 +215,23 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
214215
throw error
215216
})
216217
}
218+
219+
public async deaccessionDataset(
220+
datasetId: string | number,
221+
datasetVersionId: string,
222+
deaccessionDTO: DatasetDeaccessionDTO
223+
): Promise<void> {
224+
return this.doPost(
225+
this.buildApiEndpoint(
226+
this.datasetsResourceName,
227+
`versions/${datasetVersionId}/deaccession`,
228+
datasetId
229+
),
230+
deaccessionDTO
231+
)
232+
.then(() => undefined)
233+
.catch((error) => {
234+
throw error
235+
})
236+
}
217237
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import {
2+
deaccessionDataset,
3+
DatasetDeaccessionDTO,
4+
createDataset,
5+
publishDataset,
6+
VersionUpdateType
7+
} from '../../../src/datasets'
8+
import { ApiConfig, WriteError } from '../../../src'
9+
import { TestConstants } from '../../testHelpers/TestConstants'
10+
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
11+
import {
12+
waitForNoLocks,
13+
deletePublishedDatasetViaApi,
14+
deleteUnpublishedDatasetViaApi
15+
} from '../../testHelpers/datasets/datasetHelper'
16+
17+
const testDataset = {
18+
license: {
19+
name: 'CC0 1.0',
20+
uri: 'http://creativecommons.org/publicdomain/zero/1.0',
21+
iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png'
22+
},
23+
metadataBlockValues: [
24+
{
25+
name: 'citation',
26+
fields: {
27+
title: 'Dataset created using the createDataset use case',
28+
author: [
29+
{
30+
authorName: 'Admin, Dataverse',
31+
authorAffiliation: 'Dataverse.org'
32+
},
33+
{
34+
authorName: 'Owner, Dataverse',
35+
authorAffiliation: 'Dataversedemo.org'
36+
}
37+
],
38+
datasetContact: [
39+
{
40+
datasetContactEmail: '[email protected]',
41+
datasetContactName: 'Finch, Fiona'
42+
}
43+
],
44+
dsDescription: [
45+
{
46+
dsDescriptionValue: 'This is the description of the dataset.'
47+
}
48+
],
49+
subject: ['Medicine, Health and Life Sciences']
50+
}
51+
}
52+
]
53+
}
54+
55+
describe('execute', () => {
56+
beforeEach(async () => {
57+
ApiConfig.init(
58+
TestConstants.TEST_API_URL,
59+
DataverseApiAuthMechanism.API_KEY,
60+
process.env.TEST_API_KEY
61+
)
62+
})
63+
64+
test('should deaccession a dataset when required fields are sent', async () => {
65+
const createdDatasetIdentifiers = await createDataset.execute(testDataset)
66+
67+
const response = await publishDataset.execute(
68+
createdDatasetIdentifiers.persistentId,
69+
VersionUpdateType.MAJOR
70+
)
71+
await waitForNoLocks(createdDatasetIdentifiers.numericId, 10)
72+
73+
expect(response).toBeUndefined()
74+
75+
const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = {
76+
deaccessionReason: 'Description of the deaccession reason.',
77+
deaccessionForwardURL: 'https://demo.dataverse.org'
78+
}
79+
80+
const actual = await deaccessionDataset.execute(
81+
createdDatasetIdentifiers.numericId,
82+
'1.0',
83+
testDeaccessionDatasetDTO
84+
)
85+
86+
expect(actual).toBeUndefined()
87+
88+
await deletePublishedDatasetViaApi(createdDatasetIdentifiers.persistentId)
89+
})
90+
91+
test('should throw an error when the dataset id is incorrect', async () => {
92+
const createdDatasetIdentifiers = await createDataset.execute(testDataset)
93+
94+
const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = {
95+
deaccessionReason: 'Description of the deaccession reason.',
96+
deaccessionForwardURL: 'https://demo.dataverse.org'
97+
}
98+
99+
await expect(
100+
deaccessionDataset.execute(
101+
createdDatasetIdentifiers.numericId,
102+
':latest-published',
103+
testDeaccessionDatasetDTO
104+
)
105+
).rejects.toThrow(Error)
106+
107+
await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId)
108+
})
109+
110+
test('should not deaccession a dataset when it is not published', async () => {
111+
const createdDatasetIdentifiers = await createDataset.execute(testDataset)
112+
const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = {
113+
deaccessionReason: 'Description of the deaccession reason.'
114+
}
115+
116+
await expect(
117+
deaccessionDataset.execute(
118+
createdDatasetIdentifiers.numericId,
119+
':latest-published',
120+
testDeaccessionDatasetDTO
121+
)
122+
).rejects.toBeInstanceOf(WriteError)
123+
124+
await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId)
125+
})
126+
127+
test('should not deaccession a dataset when it is deaccessioned once', async () => {
128+
const createdDatasetIdentifiers = await createDataset.execute(testDataset)
129+
130+
const response = await publishDataset.execute(
131+
createdDatasetIdentifiers.persistentId,
132+
VersionUpdateType.MAJOR
133+
)
134+
await waitForNoLocks(createdDatasetIdentifiers.numericId, 10)
135+
136+
expect(response).toBeUndefined()
137+
138+
const testDeaccessionDatasetDTO: DatasetDeaccessionDTO = {
139+
deaccessionReason: 'Description of the deaccession reason.',
140+
deaccessionForwardURL: 'https://demo.dataverse.org'
141+
}
142+
143+
const actual = await deaccessionDataset.execute(
144+
createdDatasetIdentifiers.numericId,
145+
'1.0',
146+
testDeaccessionDatasetDTO
147+
)
148+
149+
expect(actual).toBeUndefined()
150+
151+
await expect(
152+
deaccessionDataset.execute(
153+
createdDatasetIdentifiers.numericId,
154+
'1.0',
155+
testDeaccessionDatasetDTO
156+
)
157+
).rejects.toThrow(Error)
158+
159+
await deletePublishedDatasetViaApi(createdDatasetIdentifiers.persistentId)
160+
})
161+
})

test/integration/datasets/DatasetsRepository.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
VersionUpdateType,
1818
createDataset,
1919
CreatedDatasetIdentifiers,
20-
DatasetDTO
20+
DatasetDTO,
21+
DatasetDeaccessionDTO
2122
} from '../../../src/datasets'
2223
import { ApiConfig, WriteError } from '../../../src'
2324
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
@@ -866,4 +867,62 @@ describe('DatasetsRepository', () => {
866867
).rejects.toThrow(expectedError)
867868
})
868869
})
870+
871+
describe('deaccessionDataset', () => {
872+
test('should deaccession a dataset', async () => {
873+
const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
874+
await publishDatasetViaApi(testDatasetIds.numericId)
875+
await waitForNoLocks(testDatasetIds.numericId, 10)
876+
877+
const deaccessionDTO: DatasetDeaccessionDTO = {
878+
deaccessionReason: 'Deaccessioning the dataset for testing purposes'
879+
}
880+
881+
const actual = await sut.deaccessionDataset(testDatasetIds.numericId, '1.0', deaccessionDTO)
882+
883+
expect(actual).toBeUndefined()
884+
885+
const dataset = await sut.getDataset(testDatasetIds.numericId, '1.0', true, false)
886+
887+
expect(dataset.versionInfo.state).toBe('DEACCESSIONED')
888+
})
889+
890+
test('should return error when dataset is deaccessioned', async () => {
891+
const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
892+
await publishDatasetViaApi(testDatasetIds.numericId)
893+
await waitForNoLocks(testDatasetIds.numericId, 10)
894+
895+
const deaccessionDTO: DatasetDeaccessionDTO = {
896+
deaccessionReason: 'Deaccessioning the dataset for testing purposes'
897+
}
898+
899+
const actual = await sut.deaccessionDataset(testDatasetIds.numericId, '1.0', deaccessionDTO)
900+
901+
expect(actual).toBeUndefined()
902+
903+
await expect(
904+
sut.deaccessionDataset(testDatasetIds.numericId, '1.0', deaccessionDTO)
905+
).rejects.toBeInstanceOf(WriteError)
906+
})
907+
908+
test('should return error when dataset is not published', async () => {
909+
const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
910+
911+
const deaccessionDTO: DatasetDeaccessionDTO = {
912+
deaccessionReason: 'Deaccessioning the dataset for testing purposes'
913+
}
914+
915+
await expect(
916+
sut.deaccessionDataset(testDatasetIds.numericId, ':latest-published', deaccessionDTO)
917+
).rejects.toBeInstanceOf(WriteError)
918+
})
919+
920+
test('should return error when dataset does not exist', async () => {
921+
await expect(
922+
sut.deaccessionDataset(nonExistentTestDatasetId, '1.0', {
923+
deaccessionReason: 'Deaccessioning the dataset for testing purposes'
924+
})
925+
).rejects.toBeInstanceOf(WriteError)
926+
})
927+
})
869928
})

0 commit comments

Comments
 (0)