Skip to content

Commit 526e1cc

Browse files
committed
merged with dev branch
2 parents 409a28b + 6966a11 commit 526e1cc

File tree

7 files changed

+255
-1
lines changed

7 files changed

+255
-1
lines changed

docs/useCases.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ 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)
56+
- [Get File Version Summaries](#get-file-version-summaries)
5557
- [Files write use cases](#files-write-use-cases)
5658
- [File Uploading Use Cases](#file-uploading-use-cases)
5759
- [Delete a File](#delete-a-file)
@@ -1571,9 +1573,33 @@ If restrict is false then enableAccessRequest and termsOfAccess are ignored
15711573
If restrict is true and enableAccessRequest is false then termsOfAccess is required.
15721574
The enableAccessRequest and termsOfAccess are applied to the Draft version of the Dataset and affect all of the restricted files in said Draft version.
15731575

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

1576-
Returns an array of [FileVersionSummaryInfo](../src/files/domain/models/FileVersionSummaryInfo.ts) that contains information about what changed in every specific version.
1602+
Get the file versions summaries, return a list of summaries for each version
15771603

15781604
##### Example call:
15791605

src/files/domain/repositories/IFilesRepository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,6 @@ export interface IFilesRepository {
8787
): Promise<void>
8888

8989
getFileVersionSummaries(fileId: number | string): Promise<FileVersionSummaryInfo[]>
90+
91+
isFileDeleted(fileId: number | string): Promise<boolean>
9092
}
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata'
1818
import { UpdateFileTabularTags } from './domain/useCases/UpdateFileTabularTags'
1919
import { UpdateFileCategories } from './domain/useCases/UpdateFileCategories'
2020
import { GetFileVersionSummaries } from './domain/useCases/GetFileVersionSummaries'
21+
import { IsFileDeleted } from './domain/useCases/IsFileDeleted'
2122

2223
const filesRepository = new FilesRepository()
2324
const directUploadClient = new DirectUploadClient(filesRepository)
@@ -40,6 +41,7 @@ const updateFileMetadata = new UpdateFileMetadata(filesRepository)
4041
const updateFileTabularTags = new UpdateFileTabularTags(filesRepository)
4142
const updateFileCategories = new UpdateFileCategories(filesRepository)
4243
const getFileVersionSummaries = new GetFileVersionSummaries(filesRepository)
44+
const isFileDeleted = new IsFileDeleted(filesRepository)
4345

4446
export {
4547
getDatasetFiles,
@@ -60,6 +62,7 @@ export {
6062
updateFileCategories,
6163
replaceFile,
6264
getFileVersionSummaries
65+
isFileDeleted
6366
}
6467

6568
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
@@ -427,4 +427,12 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
427427
throw error
428428
})
429429
}
430+
431+
public async isFileDeleted(fileId: number | string): Promise<boolean> {
432+
return this.doGet(this.buildApiEndpoint(this.filesResourceName, 'hasBeenDeleted', fileId), true)
433+
.then((response) => response.data.data)
434+
.catch((error) => {
435+
throw error
436+
})
437+
}
430438
}

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,6 +40,7 @@ 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'
@@ -47,6 +49,7 @@ import {
4749
FileVersionState,
4850
FileVersionSummaryInfo
4951
} from '../../../src/files/domain/models/FileVersionSummaryInfo'
52+
import { DirectUploadClient } from '../../../src/files/infra/clients/DirectUploadClient'
5053

5154
describe('FilesRepository', () => {
5255
const sut: FilesRepository = new FilesRepository()
@@ -1020,6 +1023,169 @@ describe('FilesRepository', () => {
10201023
})
10211024
})
10221025

1026+
describe('isFileDeleted', () => {
1027+
const testTextFile1Name = 'test-file-1.txt'
1028+
const testTextFile2Name = 'test-file-2.txt'
1029+
const testCollectionAlias = 'isFileDeletedTestCollection'
1030+
1031+
let deleFileTestDatasetIds: CreatedDatasetIdentifiers
1032+
let fileId: number
1033+
let singlepartFile: File
1034+
1035+
const createTestFileUploadDestination = async (file: File, datasetId: number) => {
1036+
const destination = await sut.getFileUploadDestination(datasetId, file)
1037+
destination.urls = destination.urls.map((url) => url.replace('localstack', 'localhost'))
1038+
return destination
1039+
}
1040+
1041+
const calculateBlobChecksum = (blob: Buffer): string => {
1042+
return crypto.createHash('md5').update(blob).digest('hex')
1043+
}
1044+
1045+
beforeAll(async () => {
1046+
await createCollectionViaApi(testCollectionAlias)
1047+
await setStorageDriverViaApi(testCollectionAlias, 'LocalStack')
1048+
await publishCollectionViaApi(testCollectionAlias)
1049+
1050+
deleFileTestDatasetIds = await createDataset.execute(
1051+
TestConstants.TEST_NEW_DATASET_DTO,
1052+
testCollectionAlias
1053+
)
1054+
1055+
singlepartFile = await createSinglepartFileBlob()
1056+
1057+
await uploadFileViaApi(deleFileTestDatasetIds.numericId, testTextFile1Name)
1058+
1059+
const datasetFiles = await sut.getDatasetFiles(
1060+
deleFileTestDatasetIds.numericId,
1061+
latestDatasetVersionId,
1062+
false,
1063+
FileOrderCriteria.NAME_AZ
1064+
)
1065+
fileId = datasetFiles.files[0].id
1066+
})
1067+
1068+
describe('Basic deletion scenarios', () => {
1069+
test('should return False if a file has not been deleted', async () => {
1070+
const hasBeenDeleted = await sut.isFileDeleted(fileId)
1071+
expect(hasBeenDeleted).toBe(false)
1072+
})
1073+
1074+
test('should return error if the dataset is unpublished and the file has been deleted', async () => {
1075+
await sut.deleteFile(fileId)
1076+
1077+
const expectedError = new ReadError(`[404] File with ID ${nonExistentFiledId} not found.`)
1078+
await expect(sut.isFileDeleted(nonExistentFiledId)).rejects.toThrow(expectedError)
1079+
})
1080+
1081+
test('should return correctly when the file has or has not been deleted, in a published dataset', async () => {
1082+
await uploadFileViaApi(deleFileTestDatasetIds.numericId, testTextFile1Name)
1083+
await publishDatasetViaApi(deleFileTestDatasetIds.numericId)
1084+
await waitForNoLocks(deleFileTestDatasetIds.numericId, 10)
1085+
1086+
const datasetFiles = await sut.getDatasetFiles(
1087+
deleFileTestDatasetIds.numericId,
1088+
latestDatasetVersionId,
1089+
false,
1090+
FileOrderCriteria.NAME_AZ
1091+
)
1092+
fileId = datasetFiles.files[0].id
1093+
1094+
const fileHasNotBeenDeleted = await sut.isFileDeleted(fileId)
1095+
expect(fileHasNotBeenDeleted).toBe(false)
1096+
1097+
await sut.deleteFile(fileId)
1098+
1099+
const fileHasBeenDeleted = await sut.isFileDeleted(fileId)
1100+
expect(fileHasBeenDeleted).toBe(true)
1101+
})
1102+
1103+
test('should return error when file does not exist', async () => {
1104+
const expectedError = new ReadError(`[404] File with ID ${nonExistentFiledId} not found.`)
1105+
await expect(sut.isFileDeleted(nonExistentFiledId)).rejects.toThrow(expectedError)
1106+
})
1107+
})
1108+
1109+
describe('File replacement scenario', () => {
1110+
test('should return True when file has been replaced', async () => {
1111+
const directUploadSut = new DirectUploadClient(sut)
1112+
const progressMock = jest.fn()
1113+
const abortController = new AbortController()
1114+
1115+
// Upload original file
1116+
const originalBuffer = Buffer.from(await singlepartFile.arrayBuffer())
1117+
const originalDestination = await createTestFileUploadDestination(
1118+
singlepartFile,
1119+
deleFileTestDatasetIds.numericId
1120+
)
1121+
1122+
const originalStorageId = await directUploadSut.uploadFile(
1123+
deleFileTestDatasetIds.numericId,
1124+
singlepartFile,
1125+
progressMock,
1126+
abortController,
1127+
originalDestination
1128+
)
1129+
1130+
const originalUploadedFileDTO = {
1131+
fileName: singlepartFile.name,
1132+
storageId: originalStorageId,
1133+
checksumType: 'md5',
1134+
checksumValue: calculateBlobChecksum(originalBuffer),
1135+
mimeType: singlepartFile.type
1136+
}
1137+
1138+
await sut.addUploadedFilesToDataset(deleFileTestDatasetIds.numericId, [
1139+
originalUploadedFileDTO
1140+
])
1141+
1142+
const originalFileId = (
1143+
await sut.getDatasetFiles(
1144+
deleFileTestDatasetIds.numericId,
1145+
DatasetNotNumberedVersion.LATEST,
1146+
true,
1147+
FileOrderCriteria.NAME_AZ
1148+
)
1149+
).files[0].id
1150+
1151+
// Create and upload replacement file
1152+
const newFileBlob = await createSinglepartFileBlob(testTextFile2Name, 2000)
1153+
const newBuffer = Buffer.from(await newFileBlob.arrayBuffer())
1154+
const newDestination = await createTestFileUploadDestination(
1155+
newFileBlob,
1156+
deleFileTestDatasetIds.numericId
1157+
)
1158+
1159+
const newStorageId = await directUploadSut.uploadFile(
1160+
deleFileTestDatasetIds.numericId,
1161+
newFileBlob,
1162+
progressMock,
1163+
abortController,
1164+
newDestination
1165+
)
1166+
1167+
const newUploadedFileDTO = {
1168+
fileName: newFileBlob.name,
1169+
storageId: newStorageId,
1170+
checksumType: 'md5',
1171+
checksumValue: calculateBlobChecksum(newBuffer),
1172+
mimeType: newFileBlob.type
1173+
}
1174+
1175+
await publishDatasetViaApi(deleFileTestDatasetIds.numericId)
1176+
await waitForNoLocks(deleFileTestDatasetIds.numericId, 10)
1177+
1178+
await sut.replaceFile(originalFileId, newUploadedFileDTO)
1179+
1180+
const isDeleted = await sut.isFileDeleted(originalFileId)
1181+
expect(isDeleted).toBe(true)
1182+
1183+
await deletePublishedDatasetViaApi(deleFileTestDatasetIds.persistentId)
1184+
await deleteCollectionViaApi(testCollectionAlias)
1185+
})
1186+
})
1187+
})
1188+
10231189
describe('restrictFile', () => {
10241190
let restrictFileDatasetIds: CreatedDatasetIdentifiers
10251191
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)