From 81255e0df6120831f5ddbc04c21cb9af517a74cd Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 31 Jan 2025 16:02:31 -0500 Subject: [PATCH] feat: updateFileMetadata use case --- .../domain/dtos/UpdateFileMetadataDTO.ts | 7 +++ .../domain/repositories/IFilesRepository.ts | 6 +++ .../domain/useCases/UpdateFileMetadata.ts | 25 ++++++++++ src/files/index.ts | 6 ++- .../infra/repositories/FilesRepository.ts | 20 ++++++++ .../users/DeleteCurrentApiToken.test.ts | 1 - .../integration/files/FilesRepository.test.ts | 46 ++++++++++++++++++- test/testHelpers/files/filesHelper.ts | 8 ++++ test/unit/files/UpdateFileMetadata.test.ts | 46 +++++++++++++++++++ 9 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 src/files/domain/dtos/UpdateFileMetadataDTO.ts create mode 100644 src/files/domain/useCases/UpdateFileMetadata.ts create mode 100644 test/unit/files/UpdateFileMetadata.test.ts diff --git a/src/files/domain/dtos/UpdateFileMetadataDTO.ts b/src/files/domain/dtos/UpdateFileMetadataDTO.ts new file mode 100644 index 00000000..f26798f4 --- /dev/null +++ b/src/files/domain/dtos/UpdateFileMetadataDTO.ts @@ -0,0 +1,7 @@ +export interface UpdateFileMetadataDTO { + description?: string + prevFreeform?: string + categories?: string[] + dataFileTags?: string[] + restrict?: boolean +} diff --git a/src/files/domain/repositories/IFilesRepository.ts b/src/files/domain/repositories/IFilesRepository.ts index 0f8ab8b8..264fc77e 100644 --- a/src/files/domain/repositories/IFilesRepository.ts +++ b/src/files/domain/repositories/IFilesRepository.ts @@ -8,6 +8,7 @@ import { FileModel } from '../models/FileModel' import { Dataset } from '../../../datasets' import { FileUploadDestination } from '../models/FileUploadDestination' import { UploadedFileDTO } from '../dtos/UploadedFileDTO' +import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO' export interface IFilesRepository { getDatasetFiles( @@ -59,4 +60,9 @@ export interface IFilesRepository { datasetId: number | string, uploadedFileDTOs: UploadedFileDTO[] ): Promise + + updateFileMetadata( + fileId: number | string, + updateFileMetadataDTO: UpdateFileMetadataDTO + ): Promise } diff --git a/src/files/domain/useCases/UpdateFileMetadata.ts b/src/files/domain/useCases/UpdateFileMetadata.ts new file mode 100644 index 00000000..1e7eb2e5 --- /dev/null +++ b/src/files/domain/useCases/UpdateFileMetadata.ts @@ -0,0 +1,25 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IFilesRepository } from '../repositories/IFilesRepository' +import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO' + +export class UpdateFileMetadata implements UseCase { + private filesRepository: IFilesRepository + + constructor(filesRepository: IFilesRepository) { + this.filesRepository = filesRepository + } + + /** + * Updates the metadata for a particular File. + * + * @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {UpdateFileMetadataDTO} [updateFileMetadataDTO] - The DTO containing the metadata updates. + * @returns {Promise} + */ + async execute( + fileId: number | string, + updateFileMetadataDTO: UpdateFileMetadataDTO + ): Promise { + await this.filesRepository.updateFileMetadata(fileId, updateFileMetadataDTO) + } +} diff --git a/src/files/index.ts b/src/files/index.ts index f621b0c9..3f703a21 100644 --- a/src/files/index.ts +++ b/src/files/index.ts @@ -11,6 +11,7 @@ import { GetFileAndDataset } from './domain/useCases/GetFileAndDataset' import { UploadFile } from './domain/useCases/UploadFile' import { DirectUploadClient } from './infra/clients/DirectUploadClient' import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToDataset' +import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata' const filesRepository = new FilesRepository() const directUploadClient = new DirectUploadClient(filesRepository) @@ -26,6 +27,7 @@ const getFileAndDataset = new GetFileAndDataset(filesRepository) const getFileCitation = new GetFileCitation(filesRepository) const uploadFile = new UploadFile(directUploadClient) const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository) +const updateFileMetadata = new UpdateFileMetadata(filesRepository) export { getDatasetFiles, @@ -38,7 +40,8 @@ export { getFileAndDataset, getFileCitation, uploadFile, - addUploadedFilesToDataset + addUploadedFilesToDataset, + updateFileMetadata } export { FileModel as File, FileEmbargo, FileChecksum } from './domain/models/FileModel' @@ -68,3 +71,4 @@ export { FileDownloadSizeMode } from './domain/models/FileDownloadSizeMode' export { FilesSubset } from './domain/models/FilesSubset' export { FilePreview, FilePreviewChecksum } from './domain/models/FilePreview' export { UploadedFileDTO } from './domain/dtos/UploadedFileDTO' +export { UpdateFileMetadataDTO } from './domain/dtos/UpdateFileMetadataDTO' diff --git a/src/files/infra/repositories/FilesRepository.ts b/src/files/infra/repositories/FilesRepository.ts index 8fe5adee..fd0db3c3 100644 --- a/src/files/infra/repositories/FilesRepository.ts +++ b/src/files/infra/repositories/FilesRepository.ts @@ -18,6 +18,7 @@ import { Dataset } from '../../../datasets' import { FileUploadDestination } from '../../domain/models/FileUploadDestination' import { transformUploadDestinationsResponseToUploadDestination } from './transformers/fileUploadDestinationsTransformers' import { UploadedFileDTO } from '../../domain/dtos/UploadedFileDTO' +import { UpdateFileMetadataDTO } from '../../domain/dtos/UpdateFileMetadataDTO' import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' export interface GetFilesQueryParams { @@ -293,4 +294,23 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { queryParams.searchText = fileSearchCriteria.searchText } } + + public async updateFileMetadata( + fileId: string | number, + updateFileMetadata: UpdateFileMetadataDTO + ): Promise { + const formData = new FormData() + formData.append('jsonData', JSON.stringify(updateFileMetadata)) + + return this.doPost( + this.buildApiEndpoint(this.filesResourceName, `${fileId}/metadata`), + formData, + {}, + ApiConstants.CONTENT_TYPE_MULTIPART_FORM_DATA + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/test/functional/users/DeleteCurrentApiToken.test.ts b/test/functional/users/DeleteCurrentApiToken.test.ts index 2cc0cc39..2f69ba91 100644 --- a/test/functional/users/DeleteCurrentApiToken.test.ts +++ b/test/functional/users/DeleteCurrentApiToken.test.ts @@ -24,7 +24,6 @@ describe('execute', () => { const testApiToken = await createApiTokenViaApi('deleteCurrentApiTokenFTUser') ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, testApiToken) await deleteCurrentApiToken.execute() - // Since the token has been deleted, the next call using it should return a WriteError await expect(deleteCurrentApiToken.execute()).rejects.toBeInstanceOf(WriteError) }) }) diff --git a/test/integration/files/FilesRepository.test.ts b/test/integration/files/FilesRepository.test.ts index 5d8b6821..bead6413 100644 --- a/test/integration/files/FilesRepository.test.ts +++ b/test/integration/files/FilesRepository.test.ts @@ -28,7 +28,7 @@ import { } from '../../../src/datasets' import { FileModel } from '../../../src/files/domain/models/FileModel' import { FileCounts } from '../../../src/files/domain/models/FileCounts' -import { FileDownloadSizeMode } from '../../../src' +import { FileDownloadSizeMode, WriteError } from '../../../src' import { deaccessionDatasetViaApi, publishDatasetViaApi, @@ -41,6 +41,7 @@ import { deleteCollectionViaApi, setStorageDriverViaApi } from '../../testHelpers/collections/collectionHelper' +import { getFileMetadata } from '../../testHelpers/files/filesHelper' describe('FilesRepository', () => { const sut: FilesRepository = new FilesRepository() @@ -646,4 +647,47 @@ describe('FilesRepository', () => { ).rejects.toThrow(errorExpected) }) }) + + describe('updateFileMetadata', () => { + test('should update file metadata when file exists', async () => { + const getDatasetFiles = await sut.getDatasetFiles( + testDatasetIds.numericId, + latestDatasetVersionId, + false, + FileOrderCriteria.NAME_AZ + ) + const fileid = getDatasetFiles.files[0].id + console.log('updateFileMetadata fileid', fileid) + const testFileMetadata = { + description: 'My description bbb.', + categories: ['Data'], + restrict: false + } + + const actual = await sut.updateFileMetadata(fileid, testFileMetadata) + const getFileMetadataResult = await getFileMetadata(fileid).catch(() => { + throw new Error(`Error while getting file metadata ${fileid}`) + }) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(actual).toBeUndefined() + expect(getFileMetadataResult.data.description).toBe(testFileMetadata.description) + expect(getFileMetadataResult.data.categories).toEqual(testFileMetadata.categories) + expect(getFileMetadataResult.data.restricted).toBe(testFileMetadata.restrict) + }) + + test('should return error when file does not exist', async () => { + const nonExistentFiledId = 4000 + const testFileMetadata = { + description: 'My description bbb.', + categories: ['Data'], + restrict: false + } + const errorExpected = new WriteError(`[400] Error attempting get the requested data file.`) + + await expect(sut.updateFileMetadata(nonExistentFiledId, testFileMetadata)).rejects.toThrow( + errorExpected + ) + }) + }) }) diff --git a/test/testHelpers/files/filesHelper.ts b/test/testHelpers/files/filesHelper.ts index 666551bf..48074653 100644 --- a/test/testHelpers/files/filesHelper.ts +++ b/test/testHelpers/files/filesHelper.ts @@ -227,3 +227,11 @@ export const updateFileTabularTags = async ( } ) } + +export const getFileMetadata = async (fileId: number): Promise => { + return await axios.get(`${TestConstants.TEST_API_URL}/files/${fileId}/metadata`, { + headers: { + 'X-Dataverse-Key': process.env.TEST_API_KEY + } + }) +} diff --git a/test/unit/files/UpdateFileMetadata.test.ts b/test/unit/files/UpdateFileMetadata.test.ts new file mode 100644 index 00000000..41255e48 --- /dev/null +++ b/test/unit/files/UpdateFileMetadata.test.ts @@ -0,0 +1,46 @@ +import { UpdateFileMetadata } from '../../../src/files/domain/useCases/UpdateFileMetadata' +import { IFilesRepository } from '../../../src/files/domain/repositories/IFilesRepository' +import { WriteError } from '../../../src/core/domain/repositories/WriteError' +import { createFileMetadataWithCategories } from '../../testHelpers/files/filesHelper' + +describe('UpdateFileMetadata', () => { + const testFileMetadata = createFileMetadataWithCategories() + test('should updated file metadata with correct parameters and id', async () => { + const filesRepositoryStub: IFilesRepository = {} as IFilesRepository + filesRepositoryStub.updateFileMetadata = jest.fn().mockResolvedValue(testFileMetadata) + + const sut = new UpdateFileMetadata(filesRepositoryStub) + + await sut.execute(1, testFileMetadata) + + expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith(1, testFileMetadata) + expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledTimes(1) + }) + + test('should return the updated file metadata with correct parameters and persisten Id', async () => { + const filesRepositoryStub: IFilesRepository = { + updateFileMetadata: jest.fn().mockResolvedValue(testFileMetadata) + } as unknown as IFilesRepository + + const sut = new UpdateFileMetadata(filesRepositoryStub) + + await sut.execute('doi:10.5072/FK2/HC6KTB', testFileMetadata) + + expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith( + 'doi:10.5072/FK2/HC6KTB', + testFileMetadata + ) + expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledTimes(1) + }) + + test('should throw an error if the repository throws an error', async () => { + const filesRepositoryStub: IFilesRepository = { + updateFileMetadata: jest.fn().mockRejectedValue(new WriteError()) + } as unknown as IFilesRepository + + const sut = new UpdateFileMetadata(filesRepositoryStub) + + await expect(sut.execute(1, testFileMetadata)).rejects.toThrow(WriteError) + expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith(1, testFileMetadata) + }) +})