Skip to content

Commit 6966a11

Browse files
authored
Merge pull request #302 from IQSS/301-file-has-been-deleted-use-case
File: Use Case File Has Been Deleted
2 parents c748a8f + fbd9e61 commit 6966a11

File tree

7 files changed

+254
-1
lines changed

7 files changed

+254
-1
lines changed

docs/useCases.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ The different use cases currently available in the package are classified below,
5252
- [Get the size of Downloading all the files of a Dataset Version](#get-the-size-of-downloading-all-the-files-of-a-dataset-version)
5353
- [Get User Permissions on a File](#get-user-permissions-on-a-file)
5454
- [List Files in a Dataset](#list-files-in-a-dataset)
55+
- [Is File Deleted](#is-file-deleted)
5556
- [Files write use cases](#files-write-use-cases)
5657
- [File Uploading Use Cases](#file-uploading-use-cases)
5758
- [Delete a File](#delete-a-file)
@@ -1571,6 +1572,30 @@ If restrict is false then enableAccessRequest and termsOfAccess are ignored
15711572
If restrict is true and enableAccessRequest is false then termsOfAccess is required.
15721573
The enableAccessRequest and termsOfAccess are applied to the Draft version of the Dataset and affect all of the restricted files in said Draft version.
15731574

1575+
#### Is File Deleted
1576+
1577+
Check if the file has been deleted, return a boolean.
1578+
1579+
##### Example call:
1580+
1581+
```typescript
1582+
import { isFileDeleted } from '@iqss/dataverse-client-javascript'
1583+
1584+
/* ... */
1585+
1586+
const fileId = 12345
1587+
1588+
await isFileDeleted.execute(fileId).then((isDeleted: boolean) => {
1589+
/* ... */
1590+
})
1591+
1592+
/* ... */
1593+
```
1594+
1595+
_See [use case](../src/files/domain/useCases/isFileDeleted.ts) implementation_.
1596+
1597+
The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.
1598+
15741599
## Metadata Blocks
15751600

15761601
### Metadata Blocks read use cases

src/files/domain/repositories/IFilesRepository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,6 @@ export interface IFilesRepository {
8484
categories: string[],
8585
replace?: boolean
8686
): Promise<void>
87+
88+
isFileDeleted(fileId: number | string): Promise<boolean>
8789
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { IFilesRepository } from '../repositories/IFilesRepository'
2+
import { UseCase } from '../../../core/domain/useCases/UseCase'
3+
4+
export class IsFileDeleted implements UseCase<boolean> {
5+
constructor(private readonly filesRepository: IFilesRepository) {}
6+
7+
/**
8+
* Returns a boolean, indicating whether the file has been deleted or not.
9+
*
10+
* @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
11+
* @returns {Promise<boolean>} - A boolean indicating whether the file has been deleted or not.
12+
*/
13+
async execute(fileId: number | string): Promise<boolean> {
14+
return await this.filesRepository.isFileDeleted(fileId)
15+
}
16+
}

src/files/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { RestrictFile } from './domain/useCases/RestrictFile'
1717
import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata'
1818
import { UpdateFileTabularTags } from './domain/useCases/UpdateFileTabularTags'
1919
import { UpdateFileCategories } from './domain/useCases/UpdateFileCategories'
20+
import { IsFileDeleted } from './domain/useCases/IsFileDeleted'
2021

2122
const filesRepository = new FilesRepository()
2223
const directUploadClient = new DirectUploadClient(filesRepository)
@@ -38,6 +39,7 @@ const restrictFile = new RestrictFile(filesRepository)
3839
const updateFileMetadata = new UpdateFileMetadata(filesRepository)
3940
const updateFileTabularTags = new UpdateFileTabularTags(filesRepository)
4041
const updateFileCategories = new UpdateFileCategories(filesRepository)
42+
const isFileDeleted = new IsFileDeleted(filesRepository)
4143

4244
export {
4345
getDatasetFiles,
@@ -56,7 +58,8 @@ export {
5658
updateFileMetadata,
5759
updateFileTabularTags,
5860
updateFileCategories,
59-
replaceFile
61+
replaceFile,
62+
isFileDeleted
6063
}
6164

6265
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
@@ -415,4 +415,12 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
415415
throw error
416416
})
417417
}
418+
419+
public async isFileDeleted(fileId: number | string): Promise<boolean> {
420+
return this.doGet(this.buildApiEndpoint(this.filesResourceName, 'hasBeenDeleted', fileId), true)
421+
.then((response) => response.data.data)
422+
.catch((error) => {
423+
throw error
424+
})
425+
}
418426
}

test/integration/files/FilesRepository.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as crypto from 'crypto'
12
import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepository'
23
import {
34
ApiConfig,
@@ -39,10 +40,12 @@ import {
3940
import {
4041
createCollectionViaApi,
4142
deleteCollectionViaApi,
43+
publishCollectionViaApi,
4244
setStorageDriverViaApi
4345
} from '../../testHelpers/collections/collectionHelper'
4446
import { RestrictFileDTO } from '../../../src/files/domain/dtos/RestrictFileDTO'
4547
import { DatasetsRepository } from '../../../src/datasets/infra/repositories/DatasetsRepository'
48+
import { DirectUploadClient } from '../../../src/files/infra/clients/DirectUploadClient'
4649

4750
describe('FilesRepository', () => {
4851
const sut: FilesRepository = new FilesRepository()
@@ -840,6 +843,169 @@ describe('FilesRepository', () => {
840843
})
841844
})
842845

846+
describe('isFileDeleted', () => {
847+
const testTextFile1Name = 'test-file-1.txt'
848+
const testTextFile2Name = 'test-file-2.txt'
849+
const testCollectionAlias = 'isFileDeletedTestCollection'
850+
851+
let deleFileTestDatasetIds: CreatedDatasetIdentifiers
852+
let fileId: number
853+
let singlepartFile: File
854+
855+
const createTestFileUploadDestination = async (file: File, datasetId: number) => {
856+
const destination = await sut.getFileUploadDestination(datasetId, file)
857+
destination.urls = destination.urls.map((url) => url.replace('localstack', 'localhost'))
858+
return destination
859+
}
860+
861+
const calculateBlobChecksum = (blob: Buffer): string => {
862+
return crypto.createHash('md5').update(blob).digest('hex')
863+
}
864+
865+
beforeAll(async () => {
866+
await createCollectionViaApi(testCollectionAlias)
867+
await setStorageDriverViaApi(testCollectionAlias, 'LocalStack')
868+
await publishCollectionViaApi(testCollectionAlias)
869+
870+
deleFileTestDatasetIds = await createDataset.execute(
871+
TestConstants.TEST_NEW_DATASET_DTO,
872+
testCollectionAlias
873+
)
874+
875+
singlepartFile = await createSinglepartFileBlob()
876+
877+
await uploadFileViaApi(deleFileTestDatasetIds.numericId, testTextFile1Name)
878+
879+
const datasetFiles = await sut.getDatasetFiles(
880+
deleFileTestDatasetIds.numericId,
881+
latestDatasetVersionId,
882+
false,
883+
FileOrderCriteria.NAME_AZ
884+
)
885+
fileId = datasetFiles.files[0].id
886+
})
887+
888+
describe('Basic deletion scenarios', () => {
889+
test('should return False if a file has not been deleted', async () => {
890+
const hasBeenDeleted = await sut.isFileDeleted(fileId)
891+
expect(hasBeenDeleted).toBe(false)
892+
})
893+
894+
test('should return error if the dataset is unpublished and the file has been deleted', async () => {
895+
await sut.deleteFile(fileId)
896+
897+
const expectedError = new ReadError(`[404] File with ID ${nonExistentFiledId} not found.`)
898+
await expect(sut.isFileDeleted(nonExistentFiledId)).rejects.toThrow(expectedError)
899+
})
900+
901+
test('should return correctly when the file has or has not been deleted, in a published dataset', async () => {
902+
await uploadFileViaApi(deleFileTestDatasetIds.numericId, testTextFile1Name)
903+
await publishDatasetViaApi(deleFileTestDatasetIds.numericId)
904+
await waitForNoLocks(deleFileTestDatasetIds.numericId, 10)
905+
906+
const datasetFiles = await sut.getDatasetFiles(
907+
deleFileTestDatasetIds.numericId,
908+
latestDatasetVersionId,
909+
false,
910+
FileOrderCriteria.NAME_AZ
911+
)
912+
fileId = datasetFiles.files[0].id
913+
914+
const fileHasNotBeenDeleted = await sut.isFileDeleted(fileId)
915+
expect(fileHasNotBeenDeleted).toBe(false)
916+
917+
await sut.deleteFile(fileId)
918+
919+
const fileHasBeenDeleted = await sut.isFileDeleted(fileId)
920+
expect(fileHasBeenDeleted).toBe(true)
921+
})
922+
923+
test('should return error when file does not exist', async () => {
924+
const expectedError = new ReadError(`[404] File with ID ${nonExistentFiledId} not found.`)
925+
await expect(sut.isFileDeleted(nonExistentFiledId)).rejects.toThrow(expectedError)
926+
})
927+
})
928+
929+
describe('File replacement scenario', () => {
930+
test('should return True when file has been replaced', async () => {
931+
const directUploadSut = new DirectUploadClient(sut)
932+
const progressMock = jest.fn()
933+
const abortController = new AbortController()
934+
935+
// Upload original file
936+
const originalBuffer = Buffer.from(await singlepartFile.arrayBuffer())
937+
const originalDestination = await createTestFileUploadDestination(
938+
singlepartFile,
939+
deleFileTestDatasetIds.numericId
940+
)
941+
942+
const originalStorageId = await directUploadSut.uploadFile(
943+
deleFileTestDatasetIds.numericId,
944+
singlepartFile,
945+
progressMock,
946+
abortController,
947+
originalDestination
948+
)
949+
950+
const originalUploadedFileDTO = {
951+
fileName: singlepartFile.name,
952+
storageId: originalStorageId,
953+
checksumType: 'md5',
954+
checksumValue: calculateBlobChecksum(originalBuffer),
955+
mimeType: singlepartFile.type
956+
}
957+
958+
await sut.addUploadedFilesToDataset(deleFileTestDatasetIds.numericId, [
959+
originalUploadedFileDTO
960+
])
961+
962+
const originalFileId = (
963+
await sut.getDatasetFiles(
964+
deleFileTestDatasetIds.numericId,
965+
DatasetNotNumberedVersion.LATEST,
966+
true,
967+
FileOrderCriteria.NAME_AZ
968+
)
969+
).files[0].id
970+
971+
// Create and upload replacement file
972+
const newFileBlob = await createSinglepartFileBlob(testTextFile2Name, 2000)
973+
const newBuffer = Buffer.from(await newFileBlob.arrayBuffer())
974+
const newDestination = await createTestFileUploadDestination(
975+
newFileBlob,
976+
deleFileTestDatasetIds.numericId
977+
)
978+
979+
const newStorageId = await directUploadSut.uploadFile(
980+
deleFileTestDatasetIds.numericId,
981+
newFileBlob,
982+
progressMock,
983+
abortController,
984+
newDestination
985+
)
986+
987+
const newUploadedFileDTO = {
988+
fileName: newFileBlob.name,
989+
storageId: newStorageId,
990+
checksumType: 'md5',
991+
checksumValue: calculateBlobChecksum(newBuffer),
992+
mimeType: newFileBlob.type
993+
}
994+
995+
await publishDatasetViaApi(deleFileTestDatasetIds.numericId)
996+
await waitForNoLocks(deleFileTestDatasetIds.numericId, 10)
997+
998+
await sut.replaceFile(originalFileId, newUploadedFileDTO)
999+
1000+
const isDeleted = await sut.isFileDeleted(originalFileId)
1001+
expect(isDeleted).toBe(true)
1002+
1003+
await deletePublishedDatasetViaApi(deleFileTestDatasetIds.persistentId)
1004+
await deleteCollectionViaApi(testCollectionAlias)
1005+
})
1006+
})
1007+
})
1008+
8431009
describe('restrictFile', () => {
8441010
let restrictFileDatasetIds: CreatedDatasetIdentifiers
8451011
const testTextFile1Name = 'test-file-1.txt'
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { IFilesRepository } from '../../../src/files/domain/repositories/IFilesRepository'
2+
import { IsFileDeleted } from '../../../src/files/domain/useCases/IsFileDeleted'
3+
import { ReadError } from '../../../src'
4+
5+
describe('execute', () => {
6+
test('should return true when file has been deleted', async () => {
7+
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
8+
filesRepositoryStub.isFileDeleted = jest.fn().mockResolvedValue(true)
9+
const sut = new IsFileDeleted(filesRepositoryStub)
10+
11+
const result = await sut.execute(1)
12+
13+
expect(result).toBe(true)
14+
})
15+
16+
test('should return false when file has not been deleted', async () => {
17+
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
18+
filesRepositoryStub.isFileDeleted = jest.fn().mockResolvedValue(false)
19+
const sut = new IsFileDeleted(filesRepositoryStub)
20+
21+
const result = await sut.execute(1)
22+
23+
expect(result).toBe(false)
24+
})
25+
26+
test('should return error result on repository error', async () => {
27+
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
28+
filesRepositoryStub.isFileDeleted = jest.fn().mockRejectedValue(new ReadError())
29+
const sut = new IsFileDeleted(filesRepositoryStub)
30+
31+
await expect(sut.execute(1)).rejects.toThrow(ReadError)
32+
})
33+
})

0 commit comments

Comments
 (0)