Skip to content

Commit 52f826c

Browse files
authored
Merge pull request #249 from IQSS/213-implement-use-case-for-edit-files-metadata-on-file-page
Implement updateFileMetadata use case
2 parents 471d6c9 + 1922064 commit 52f826c

File tree

11 files changed

+294
-3
lines changed

11 files changed

+294
-3
lines changed

docs/useCases.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,33 @@ The following error might arise from the `AddUploadedFileToDataset` use case:
12841284

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

1287+
#### Update File Metadata
1288+
1289+
Updates Metadata of a File.
1290+
1291+
###### Example call:
1292+
1293+
```typescript
1294+
import { updateFileMetadata } from '@iqss/dataverse-client-javascript'
1295+
1296+
/* ... */
1297+
1298+
const fileId: number | string = 123
1299+
const updateFileMetadataDTO = {
1300+
description: 'My description bbb.',
1301+
categories: ['Data'],
1302+
restrict: false
1303+
}
1304+
1305+
await updateFileMetadata.execute(fileId, updateFileMetadataDTO).then((fileId) => {
1306+
console.log(`File updated successfully with file ID: ${fileId}`)
1307+
})
1308+
```
1309+
1310+
_See [use case](../src/files/domain/useCases/UpdateFileMetadata.ts) implementation_.
1311+
1312+
The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.
1313+
12871314
#### Delete a File
12881315

12891316
Deletes a File.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface UpdateFileMetadataDTO {
2+
description?: string
3+
prevFreeform?: string
4+
categories?: string[]
5+
dataFileTags?: string[]
6+
restrict?: boolean
7+
}

src/files/domain/repositories/IFilesRepository.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FileModel } from '../models/FileModel'
88
import { Dataset } from '../../../datasets'
99
import { FileUploadDestination } from '../models/FileUploadDestination'
1010
import { UploadedFileDTO } from '../dtos/UploadedFileDTO'
11+
import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO'
1112

1213
export interface IFilesRepository {
1314
getDatasetFiles(
@@ -65,4 +66,8 @@ export interface IFilesRepository {
6566
replaceFile(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<undefined>
6667

6768
restrictFile(fileId: number | string, restrict: boolean): Promise<undefined>
69+
updateFileMetadata(
70+
fileId: number | string,
71+
updateFileMetadataDTO: UpdateFileMetadataDTO
72+
): Promise<void>
6873
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { IFilesRepository } from '../repositories/IFilesRepository'
3+
import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO'
4+
5+
export class UpdateFileMetadata implements UseCase<void> {
6+
private filesRepository: IFilesRepository
7+
8+
constructor(filesRepository: IFilesRepository) {
9+
this.filesRepository = filesRepository
10+
}
11+
12+
/**
13+
* Updates the metadata for a particular File.
14+
* More detailed information about updating a file's metadata behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#updating-file-metadata
15+
*
16+
* @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
17+
* @param {UpdateFileMetadataDTO} [updateFileMetadataDTO] - The DTO containing the metadata updates.
18+
* @returns {Promise<void>}
19+
*/
20+
async execute(
21+
fileId: number | string,
22+
updateFileMetadataDTO: UpdateFileMetadataDTO
23+
): Promise<void> {
24+
await this.filesRepository.updateFileMetadata(fileId, updateFileMetadataDTO)
25+
}
26+
}

src/files/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToD
1414
import { DeleteFile } from './domain/useCases/DeleteFile'
1515
import { ReplaceFile } from './domain/useCases/ReplaceFile'
1616
import { RestrictFile } from './domain/useCases/RestrictFile'
17+
import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata'
1718

1819
const filesRepository = new FilesRepository()
1920
const directUploadClient = new DirectUploadClient(filesRepository)
@@ -32,6 +33,7 @@ const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
3233
const deleteFile = new DeleteFile(filesRepository)
3334
const replaceFile = new ReplaceFile(filesRepository)
3435
const restrictFile = new RestrictFile(filesRepository)
36+
const updateFileMetadata = new UpdateFileMetadata(filesRepository)
3537

3638
export {
3739
getDatasetFiles,
@@ -46,8 +48,9 @@ export {
4648
uploadFile,
4749
addUploadedFilesToDataset,
4850
deleteFile,
49-
replaceFile,
50-
restrictFile
51+
restrictFile,
52+
updateFileMetadata,
53+
replaceFile
5154
}
5255

5356
export { FileModel as File, FileEmbargo, FileChecksum } from './domain/models/FileModel'
@@ -77,3 +80,4 @@ export { FileDownloadSizeMode } from './domain/models/FileDownloadSizeMode'
7780
export { FilesSubset } from './domain/models/FilesSubset'
7881
export { FilePreview, FilePreviewChecksum } from './domain/models/FilePreview'
7982
export { UploadedFileDTO } from './domain/dtos/UploadedFileDTO'
83+
export { UpdateFileMetadataDTO } from './domain/dtos/UpdateFileMetadataDTO'

src/files/infra/repositories/FilesRepository.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Dataset } from '../../../datasets'
1818
import { FileUploadDestination } from '../../domain/models/FileUploadDestination'
1919
import { transformUploadDestinationsResponseToUploadDestination } from './transformers/fileUploadDestinationsTransformers'
2020
import { UploadedFileDTO } from '../../domain/dtos/UploadedFileDTO'
21+
import { UpdateFileMetadataDTO } from '../../domain/dtos/UpdateFileMetadataDTO'
2122
import { ApiConstants } from '../../../core/infra/repositories/ApiConstants'
2223

2324
export interface GetFilesQueryParams {
@@ -344,4 +345,23 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
344345
throw error
345346
})
346347
}
348+
349+
public async updateFileMetadata(
350+
fileId: string | number,
351+
updateFileMetadata: UpdateFileMetadataDTO
352+
): Promise<void> {
353+
const formData = new FormData()
354+
formData.append('jsonData', JSON.stringify(updateFileMetadata))
355+
356+
return this.doPost(
357+
this.buildApiEndpoint(this.filesResourceName, `${fileId}/metadata`),
358+
formData,
359+
{},
360+
ApiConstants.CONTENT_TYPE_MULTIPART_FORM_DATA
361+
)
362+
.then(() => undefined)
363+
.catch((error) => {
364+
throw error
365+
})
366+
}
347367
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
ApiConfig,
3+
createDataset,
4+
CreatedDatasetIdentifiers,
5+
WriteError,
6+
updateFileMetadata,
7+
getFile,
8+
DatasetNotNumberedVersion,
9+
getDatasetFiles
10+
} from '../../../src'
11+
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
12+
import {
13+
createCollectionViaApi,
14+
deleteCollectionViaApi
15+
} from '../../testHelpers/collections/collectionHelper'
16+
import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper'
17+
import { uploadFileViaApi } from '../../testHelpers/files/filesHelper'
18+
import { TestConstants } from '../../testHelpers/TestConstants'
19+
import { UpdateFileMetadataDTO } from '../../../src/files/domain/dtos/UpdateFileMetadataDTO'
20+
import { FileModel } from '../../../src/files/domain/models/FileModel'
21+
22+
describe('execute', () => {
23+
const testCollectionAlias = 'updateFileMetadatFunctionalTest'
24+
let testDatasetIds: CreatedDatasetIdentifiers
25+
const testTextFile1Name = 'test-file-1.txt'
26+
const metadataUpdate: UpdateFileMetadataDTO = {
27+
description: 'This is a test file',
28+
categories: ['file'],
29+
restrict: true
30+
}
31+
32+
beforeAll(async () => {
33+
ApiConfig.init(
34+
TestConstants.TEST_API_URL,
35+
DataverseApiAuthMechanism.API_KEY,
36+
process.env.TEST_API_KEY
37+
)
38+
await createCollectionViaApi(testCollectionAlias)
39+
40+
try {
41+
testDatasetIds = await createDataset.execute(
42+
TestConstants.TEST_NEW_DATASET_DTO,
43+
testCollectionAlias
44+
)
45+
} catch (error) {
46+
throw new Error('Tests beforeAll(): Error while creating test dataset')
47+
}
48+
49+
await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => {
50+
throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`)
51+
})
52+
})
53+
54+
afterAll(async () => {
55+
try {
56+
await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId)
57+
} catch (error) {
58+
throw new Error('Tests afterAll(): Error while deleting test dataset')
59+
}
60+
61+
try {
62+
await deleteCollectionViaApi(testCollectionAlias)
63+
} catch (error) {
64+
throw new Error('Tests afterAll(): Error while deleting test collection')
65+
}
66+
})
67+
68+
test('should successfully update metadata of a file', async () => {
69+
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
70+
const fileId = datasetFiles.files[0].id
71+
72+
try {
73+
await updateFileMetadata.execute(fileId, metadataUpdate)
74+
} catch (error) {
75+
throw new Error('File metadata should be updated')
76+
} finally {
77+
const fileInfo: FileModel = (await getFile.execute(
78+
fileId,
79+
DatasetNotNumberedVersion.LATEST
80+
)) as FileModel
81+
82+
expect(fileInfo.description).toEqual(metadataUpdate.description)
83+
expect(fileInfo.categories).toEqual(metadataUpdate.categories)
84+
expect(fileInfo.restricted).toEqual(metadataUpdate.restrict)
85+
}
86+
})
87+
88+
test('should throw an error when the file id does not exist', async () => {
89+
let writeError: WriteError | undefined = undefined
90+
const nonExistentFileId = 5
91+
92+
try {
93+
await updateFileMetadata.execute(nonExistentFileId, metadataUpdate)
94+
throw new Error('Use case should throw an error')
95+
} catch (error) {
96+
writeError = error as WriteError
97+
} finally {
98+
expect(writeError).toBeInstanceOf(WriteError)
99+
expect(writeError?.message).toEqual(
100+
`There was an error when writing the resource. Reason was: [400] Error attempting get the requested data file.`
101+
)
102+
}
103+
})
104+
})

test/functional/users/DeleteCurrentApiToken.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ describe('execute', () => {
2424
const testApiToken = await createApiTokenViaApi('deleteCurrentApiTokenFTUser')
2525
ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, testApiToken)
2626
await deleteCurrentApiToken.execute()
27-
// Since the token has been deleted, the next call using it should return a WriteError
2827
await expect(deleteCurrentApiToken.execute()).rejects.toBeInstanceOf(WriteError)
2928
})
3029
})

test/integration/files/FilesRepository.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,43 @@ describe('FilesRepository', () => {
647647
})
648648
})
649649

650+
describe('updateFileMetadata', () => {
651+
test('should update file metadata when file exists', async () => {
652+
const testFileMetadata = {
653+
description: 'My description test.',
654+
categories: ['Data'],
655+
restrict: false
656+
}
657+
658+
const actual = await sut.updateFileMetadata(testFileId, testFileMetadata)
659+
660+
expect(actual).toBeUndefined()
661+
662+
const fileInfo: FileModel = (await sut.getFile(
663+
testFileId,
664+
DatasetNotNumberedVersion.LATEST,
665+
false
666+
)) as FileModel
667+
668+
expect(fileInfo.description).toBe(testFileMetadata.description)
669+
expect(fileInfo.categories).toEqual(testFileMetadata.categories)
670+
expect(fileInfo.restricted).toBe(testFileMetadata.restrict)
671+
})
672+
673+
test('should return error when file does not exist', async () => {
674+
const testFileMetadata = {
675+
description: 'My description test.',
676+
categories: ['Data'],
677+
restrict: false
678+
}
679+
const errorExpected = new WriteError(`[400] Error attempting get the requested data file.`)
680+
681+
await expect(sut.updateFileMetadata(nonExistentFiledId, testFileMetadata)).rejects.toThrow(
682+
errorExpected
683+
)
684+
})
685+
})
686+
650687
describe('deleteFile', () => {
651688
let deleFileTestDatasetIds: CreatedDatasetIdentifiers
652689
const testTextFile1Name = 'test-file-1.txt'

test/testHelpers/files/filesHelper.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,22 @@ export const updateFileTabularTags = async (
237237
)
238238
}
239239

240+
export const getFileMetadata = async (fileId: number): Promise<AxiosResponse> => {
241+
return await axios.get(`${TestConstants.TEST_API_URL}/files/${fileId}/metadata`, {
242+
headers: {
243+
'X-Dataverse-Key': process.env.TEST_API_KEY
244+
}
245+
})
246+
}
247+
248+
export const createFileMetadataWithCategories = (): FileMetadata => {
249+
return {
250+
categories: ['category1', 'category2'],
251+
description: 'description',
252+
directoryLabel: 'directoryLabel'
253+
}
254+
}
255+
240256
export const calculateBlobChecksum = (blob: Buffer, checksumAlgorithm: string): string => {
241257
const hash = crypto.createHash(checksumAlgorithm)
242258
hash.update(blob)

0 commit comments

Comments
 (0)