Skip to content

Commit 9d043ca

Browse files
authored
Merge pull request #253 from IQSS/245-delete-file-use-case
245 delete file use case
2 parents 15cdfa3 + 9646936 commit 9d043ca

File tree

10 files changed

+330
-8
lines changed

10 files changed

+330
-8
lines changed

docs/useCases.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The different use cases currently available in the package are classified below,
4949
- [List Files in a Dataset](#list-files-in-a-dataset)
5050
- [Files write use cases](#files-write-use-cases)
5151
- [File Uploading Use Cases](#file-uploading-use-cases)
52+
- [Delete a File](#delete-a-file)
5253
- [Metadata Blocks](#metadata-blocks)
5354
- [Metadata Blocks read use cases](#metadata-blocks-read-use-cases)
5455
- [Get All Facetable Metadata Fields](#get-all-facetable-metadata-fields)
@@ -1203,6 +1204,34 @@ The following error might arise from the `AddUploadedFileToDataset` use case:
12031204

12041205
- AddUploadedFileToDatasetError: This error indicates that there was an error while adding the uploaded file to the dataset.
12051206

1207+
#### Delete a File
1208+
1209+
Deletes a File.
1210+
1211+
##### Example call:
1212+
1213+
```typescript
1214+
import { deleteFile } from '@iqss/dataverse-client-javascript'
1215+
1216+
/* ... */
1217+
1218+
const fileId = 12345
1219+
1220+
deleteFile.execute(fileId)
1221+
1222+
/* ... */
1223+
```
1224+
1225+
_See [use case](../src/files/domain/useCases/DeleteFile.ts) implementation_.
1226+
1227+
The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.
1228+
1229+
Note that the behavior of deleting files depends on if the dataset has ever been published or not.
1230+
1231+
- If the dataset has never been published, the file will be deleted forever.
1232+
- If the dataset has published, the file is deleted from the draft (and future published versions).
1233+
- If the dataset has published, the deleted file can still be downloaded because it was part of a published version.
1234+
12061235
## Metadata Blocks
12071236

12081237
### Metadata Blocks read use cases

src/core/infra/repositories/ApiRepository.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@ export abstract class ApiRepository {
4747

4848
protected buildApiEndpoint(
4949
resourceName: string,
50-
operation: string,
51-
resourceId: number | string | undefined = undefined
50+
operation?: string,
51+
resourceId?: number | string
5252
) {
53+
const operationSegment = operation ? `/${operation}` : ''
54+
5355
return typeof resourceId === 'number'
54-
? `/${resourceName}/${resourceId}/${operation}`
56+
? `/${resourceName}/${resourceId}${operationSegment}`
5557
: typeof resourceId === 'string'
56-
? `/${resourceName}/:persistentId/${operation}?persistentId=${resourceId}`
57-
: `/${resourceName}/${operation}`
58+
? `/${resourceName}/:persistentId${operationSegment}?persistentId=${resourceId}`
59+
: `/${resourceName}${operationSegment}`
5860
}
5961

6062
// eslint-disable-next-line @typescript-eslint/no-explicit-any

src/files/domain/repositories/IFilesRepository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,6 @@ export interface IFilesRepository {
5959
datasetId: number | string,
6060
uploadedFileDTOs: UploadedFileDTO[]
6161
): Promise<undefined>
62+
63+
deleteFile(fileId: number | string): Promise<undefined>
6264
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { IFilesRepository } from '../repositories/IFilesRepository'
2+
import { UseCase } from '../../../core/domain/useCases/UseCase'
3+
4+
export class DeleteFile implements UseCase<void> {
5+
constructor(private readonly filesRepository: IFilesRepository) {}
6+
7+
/**
8+
* Deletes a file.
9+
* More detailed information about the file deletion behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#deleting-files
10+
*
11+
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
12+
* @returns {Promise<void>} -This method does not return anything upon successful completion.
13+
*/
14+
async execute(fileId: number | string): Promise<void> {
15+
return await this.filesRepository.deleteFile(fileId)
16+
}
17+
}

src/files/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { GetFileAndDataset } from './domain/useCases/GetFileAndDataset'
1111
import { UploadFile } from './domain/useCases/UploadFile'
1212
import { DirectUploadClient } from './infra/clients/DirectUploadClient'
1313
import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToDataset'
14+
import { DeleteFile } from './domain/useCases/DeleteFile'
1415

1516
const filesRepository = new FilesRepository()
1617
const directUploadClient = new DirectUploadClient(filesRepository)
@@ -26,6 +27,7 @@ const getFileAndDataset = new GetFileAndDataset(filesRepository)
2627
const getFileCitation = new GetFileCitation(filesRepository)
2728
const uploadFile = new UploadFile(directUploadClient)
2829
const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
30+
const deleteFile = new DeleteFile(filesRepository)
2931

3032
export {
3133
getDatasetFiles,
@@ -38,7 +40,8 @@ export {
3840
getFileAndDataset,
3941
getFileCitation,
4042
uploadFile,
41-
addUploadedFilesToDataset
43+
addUploadedFilesToDataset,
44+
deleteFile
4245
}
4346

4447
export { FileModel as File, FileEmbargo, FileChecksum } from './domain/models/FileModel'

src/files/infra/repositories/FilesRepository.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,12 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
293293
queryParams.searchText = fileSearchCriteria.searchText
294294
}
295295
}
296+
297+
public async deleteFile(fileId: number | string): Promise<undefined> {
298+
return this.doDelete(this.buildApiEndpoint(this.filesResourceName, undefined, fileId))
299+
.then(() => undefined)
300+
.catch((error) => {
301+
throw error
302+
})
303+
}
296304
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
ApiConfig,
3+
createDataset,
4+
CreatedDatasetIdentifiers,
5+
deleteFile,
6+
getDatasetFileCounts,
7+
getDatasetFiles,
8+
WriteError
9+
} from '../../../src'
10+
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
11+
import {
12+
createCollectionViaApi,
13+
deleteCollectionViaApi
14+
} from '../../testHelpers/collections/collectionHelper'
15+
import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper'
16+
import { uploadFileViaApi } from '../../testHelpers/files/filesHelper'
17+
import { TestConstants } from '../../testHelpers/TestConstants'
18+
19+
describe('execute', () => {
20+
const testCollectionAlias = 'deleteFileFunctionalTest'
21+
let testDatasetIds: CreatedDatasetIdentifiers
22+
const testTextFile1Name = 'test-file-1.txt'
23+
24+
beforeAll(async () => {
25+
ApiConfig.init(
26+
TestConstants.TEST_API_URL,
27+
DataverseApiAuthMechanism.API_KEY,
28+
process.env.TEST_API_KEY
29+
)
30+
await createCollectionViaApi(testCollectionAlias)
31+
32+
try {
33+
testDatasetIds = await createDataset.execute(
34+
TestConstants.TEST_NEW_DATASET_DTO,
35+
testCollectionAlias
36+
)
37+
} catch (error) {
38+
throw new Error('Tests beforeAll(): Error while creating test dataset')
39+
}
40+
await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => {
41+
throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`)
42+
})
43+
})
44+
45+
afterAll(async () => {
46+
try {
47+
await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId)
48+
} catch (error) {
49+
throw new Error('Tests afterAll(): Error while deleting test dataset')
50+
}
51+
try {
52+
await deleteCollectionViaApi(testCollectionAlias)
53+
} catch (error) {
54+
throw new Error('Tests afterAll(): Error while deleting test collection')
55+
}
56+
})
57+
58+
test('should successfully delete a file', async () => {
59+
try {
60+
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
61+
62+
await deleteFile.execute(datasetFiles.files[0].id)
63+
} catch (error) {
64+
throw new Error('File should be deleted')
65+
} finally {
66+
const datasetFileCounts = await getDatasetFileCounts.execute(testDatasetIds.numericId)
67+
68+
expect(datasetFileCounts.total).toEqual(0)
69+
}
70+
})
71+
72+
test('should throw an error when the file id does not exist', async () => {
73+
expect.assertions(2)
74+
let writeError: WriteError | undefined = undefined
75+
const nonExistentFileId = 5
76+
77+
try {
78+
await deleteFile.execute(nonExistentFileId)
79+
throw new Error('Use case should throw an error')
80+
} catch (error) {
81+
writeError = error as WriteError
82+
} finally {
83+
expect(writeError).toBeInstanceOf(WriteError)
84+
expect(writeError?.message).toEqual(
85+
`There was an error when writing the resource. Reason was: [404] File with ID ${nonExistentFileId} not found.`
86+
)
87+
}
88+
})
89+
})

test/integration/files/FilesRepository.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
} from '../../../src/datasets'
2929
import { FileModel } from '../../../src/files/domain/models/FileModel'
3030
import { FileCounts } from '../../../src/files/domain/models/FileCounts'
31-
import { FileDownloadSizeMode } from '../../../src'
31+
import { FileDownloadSizeMode, WriteError } from '../../../src'
3232
import {
3333
deaccessionDatasetViaApi,
3434
publishDatasetViaApi,
@@ -646,4 +646,83 @@ describe('FilesRepository', () => {
646646
).rejects.toThrow(errorExpected)
647647
})
648648
})
649+
650+
describe('deleteFile', () => {
651+
let deleFileTestDatasetIds: CreatedDatasetIdentifiers
652+
const testTextFile1Name = 'test-file-1.txt'
653+
654+
beforeEach(async () => {
655+
try {
656+
deleFileTestDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO)
657+
} catch (error) {
658+
throw new Error('Tests beforeEach(): Error while creating test dataset')
659+
}
660+
await uploadFileViaApi(deleFileTestDatasetIds.numericId, testTextFile1Name).catch(() => {
661+
throw new Error(`Tests beforeEach(): Error while uploading file ${testTextFile1Name}`)
662+
})
663+
})
664+
665+
test('should successfully delete a file', async () => {
666+
const datasetFiles = await sut.getDatasetFiles(
667+
deleFileTestDatasetIds.numericId,
668+
latestDatasetVersionId,
669+
false,
670+
FileOrderCriteria.NAME_AZ
671+
)
672+
await sut.deleteFile(datasetFiles.files[0].id)
673+
674+
const datasetFileCounts = await sut.getDatasetFileCounts(
675+
deleFileTestDatasetIds.numericId,
676+
latestDatasetVersionId,
677+
false
678+
)
679+
expect(datasetFileCounts.total).toEqual(0)
680+
681+
await deleteUnpublishedDatasetViaApi(deleFileTestDatasetIds.numericId)
682+
})
683+
684+
test('should delete a file from the draft dataset but not from the published dataset', async () => {
685+
await publishDatasetViaApi(deleFileTestDatasetIds.numericId).catch(() => {
686+
throw new Error('Error while publishing test Dataset')
687+
})
688+
689+
await waitForNoLocks(deleFileTestDatasetIds.numericId, 10).catch(() => {
690+
throw new Error('Error while waiting for no locks')
691+
})
692+
693+
const datasetFiles = await sut.getDatasetFiles(
694+
deleFileTestDatasetIds.numericId,
695+
latestDatasetVersionId,
696+
false,
697+
FileOrderCriteria.NAME_AZ
698+
)
699+
await sut.deleteFile(datasetFiles.files[0].id)
700+
701+
const datasetFileCounts = await sut.getDatasetFileCounts(
702+
deleFileTestDatasetIds.numericId,
703+
DatasetNotNumberedVersion.DRAFT,
704+
false
705+
)
706+
707+
expect(datasetFileCounts.total).toEqual(0)
708+
709+
const publishedDatasetFileCounts = await sut.getDatasetFileCounts(
710+
deleFileTestDatasetIds.numericId,
711+
DatasetNotNumberedVersion.LATEST_PUBLISHED,
712+
false
713+
)
714+
715+
expect(publishedDatasetFileCounts.total).toBeGreaterThan(0)
716+
717+
await deletePublishedDatasetViaApi(deleFileTestDatasetIds.persistentId).catch(() => {
718+
throw new Error('Error while deleting published test Dataset')
719+
})
720+
})
721+
722+
test('should return error when file does not exist', async () => {
723+
const expectedError = new WriteError(`[404] File with ID ${nonExistentFiledId} not found.`)
724+
725+
await expect(sut.deleteFile(nonExistentFiledId)).rejects.toThrow(expectedError)
726+
})
727+
})
649728
})

test/unit/files/DeleteFile.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { IFilesRepository } from '../../../src/files/domain/repositories/IFilesRepository'
2+
import { WriteError } from '../../../src'
3+
import { DeleteFile } from '../../../src/files/domain/useCases/DeleteFile'
4+
5+
describe('execute', () => {
6+
test('should return undefined when repository call is successful', async () => {
7+
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
8+
filesRepositoryStub.deleteFile = jest.fn().mockResolvedValue(undefined)
9+
10+
const sut = new DeleteFile(filesRepositoryStub)
11+
12+
const actual = await sut.execute(1)
13+
14+
expect(actual).toEqual(undefined)
15+
})
16+
17+
test('should return error result on repository error', async () => {
18+
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
19+
filesRepositoryStub.deleteFile = jest.fn().mockRejectedValue(new WriteError())
20+
21+
const sut = new DeleteFile(filesRepositoryStub)
22+
23+
await expect(sut.execute(1)).rejects.toThrow(WriteError)
24+
})
25+
})

0 commit comments

Comments
 (0)