Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,33 @@ The following error might arise from the `AddUploadedFileToDataset` use case:

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

#### Update File Metadata

Updates Metadata of a File.

###### Example call:

```typescript
import { updateFileMetadata } from '@iqss/dataverse-client-javascript'

/* ... */

const fileId: number | string = 123
const updateFileMetadataDTO = {
description: 'My description bbb.',
categories: ['Data'],
restrict: false
}

await updateFileMetadata.execute(fileId, updateFileMetadataDTO).then((fileId) => {
console.log(`File updated successfully with file ID: ${fileId}`)
})
```

_See [use case](../src/files/domain/useCases/UpdateFileMetadata.ts) implementation_.

The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.

#### Delete a File

Deletes a File.
Expand Down
7 changes: 7 additions & 0 deletions src/files/domain/dtos/UpdateFileMetadataDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface UpdateFileMetadataDTO {
description?: string
prevFreeform?: string
categories?: string[]
dataFileTags?: string[]
restrict?: boolean
}
5 changes: 5 additions & 0 deletions src/files/domain/repositories/IFilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -65,4 +66,8 @@ export interface IFilesRepository {
replaceFile(fileId: number | string, uploadedFileDTO: UploadedFileDTO): Promise<undefined>

restrictFile(fileId: number | string, restrict: boolean): Promise<undefined>
updateFileMetadata(
fileId: number | string,
updateFileMetadataDTO: UpdateFileMetadataDTO
): Promise<void>
}
26 changes: 26 additions & 0 deletions src/files/domain/useCases/UpdateFileMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { IFilesRepository } from '../repositories/IFilesRepository'
import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO'

export class UpdateFileMetadata implements UseCase<void> {
private filesRepository: IFilesRepository

constructor(filesRepository: IFilesRepository) {
this.filesRepository = filesRepository
}

/**
* Updates the metadata for a particular File.
* 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
*
* @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<void>}
*/
async execute(
fileId: number | string,
updateFileMetadataDTO: UpdateFileMetadataDTO
): Promise<void> {
await this.filesRepository.updateFileMetadata(fileId, updateFileMetadataDTO)
}
}
8 changes: 6 additions & 2 deletions src/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AddUploadedFilesToDataset } from './domain/useCases/AddUploadedFilesToD
import { DeleteFile } from './domain/useCases/DeleteFile'
import { ReplaceFile } from './domain/useCases/ReplaceFile'
import { RestrictFile } from './domain/useCases/RestrictFile'
import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata'

const filesRepository = new FilesRepository()
const directUploadClient = new DirectUploadClient(filesRepository)
Expand All @@ -32,6 +33,7 @@ const addUploadedFilesToDataset = new AddUploadedFilesToDataset(filesRepository)
const deleteFile = new DeleteFile(filesRepository)
const replaceFile = new ReplaceFile(filesRepository)
const restrictFile = new RestrictFile(filesRepository)
const updateFileMetadata = new UpdateFileMetadata(filesRepository)

export {
getDatasetFiles,
Expand All @@ -46,8 +48,9 @@ export {
uploadFile,
addUploadedFilesToDataset,
deleteFile,
replaceFile,
restrictFile
restrictFile,
updateFileMetadata,
replaceFile
}

export { FileModel as File, FileEmbargo, FileChecksum } from './domain/models/FileModel'
Expand Down Expand Up @@ -77,3 +80,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'
20 changes: 20 additions & 0 deletions src/files/infra/repositories/FilesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -344,4 +345,23 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
throw error
})
}

public async updateFileMetadata(
fileId: string | number,
updateFileMetadata: UpdateFileMetadataDTO
): Promise<void> {
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
})
}
}
104 changes: 104 additions & 0 deletions test/functional/files/UpdateFileMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
ApiConfig,
createDataset,
CreatedDatasetIdentifiers,
WriteError,
updateFileMetadata,
getFile,
DatasetNotNumberedVersion,
getDatasetFiles
} from '../../../src'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import {
createCollectionViaApi,
deleteCollectionViaApi
} from '../../testHelpers/collections/collectionHelper'
import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper'
import { uploadFileViaApi } from '../../testHelpers/files/filesHelper'
import { TestConstants } from '../../testHelpers/TestConstants'
import { UpdateFileMetadataDTO } from '../../../src/files/domain/dtos/UpdateFileMetadataDTO'
import { FileModel } from '../../../src/files/domain/models/FileModel'

describe('execute', () => {
const testCollectionAlias = 'updateFileMetadatFunctionalTest'
let testDatasetIds: CreatedDatasetIdentifiers
const testTextFile1Name = 'test-file-1.txt'
const metadataUpdate: UpdateFileMetadataDTO = {
description: 'This is a test file',
categories: ['file'],
restrict: true
}

beforeAll(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
await createCollectionViaApi(testCollectionAlias)

try {
testDatasetIds = await createDataset.execute(
TestConstants.TEST_NEW_DATASET_DTO,
testCollectionAlias
)
} catch (error) {
throw new Error('Tests beforeAll(): Error while creating test dataset')
}

await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => {
throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`)
})
})

afterAll(async () => {
try {
await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId)
} catch (error) {
throw new Error('Tests afterAll(): Error while deleting test dataset')
}

try {
await deleteCollectionViaApi(testCollectionAlias)
} catch (error) {
throw new Error('Tests afterAll(): Error while deleting test collection')
}
})

test('should successfully update metadata of a file', async () => {
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
const fileId = datasetFiles.files[0].id

try {
await updateFileMetadata.execute(fileId, metadataUpdate)
} catch (error) {
throw new Error('File metadata should be updated')
} finally {
const fileInfo: FileModel = (await getFile.execute(
fileId,
DatasetNotNumberedVersion.LATEST
)) as FileModel

expect(fileInfo.description).toEqual(metadataUpdate.description)
expect(fileInfo.categories).toEqual(metadataUpdate.categories)
expect(fileInfo.restricted).toEqual(metadataUpdate.restrict)
}
})

test('should throw an error when the file id does not exist', async () => {
let writeError: WriteError | undefined = undefined
const nonExistentFileId = 5

try {
await updateFileMetadata.execute(nonExistentFileId, metadataUpdate)
throw new Error('Use case should throw an error')
} catch (error) {
writeError = error as WriteError
} finally {
expect(writeError).toBeInstanceOf(WriteError)
expect(writeError?.message).toEqual(
`There was an error when writing the resource. Reason was: [400] Error attempting get the requested data file.`
)
}
})
})
1 change: 0 additions & 1 deletion test/functional/users/DeleteCurrentApiToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
37 changes: 37 additions & 0 deletions test/integration/files/FilesRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,43 @@ describe('FilesRepository', () => {
})
})

describe('updateFileMetadata', () => {
test('should update file metadata when file exists', async () => {
const testFileMetadata = {
description: 'My description test.',
categories: ['Data'],
restrict: false
}

const actual = await sut.updateFileMetadata(testFileId, testFileMetadata)

expect(actual).toBeUndefined()

const fileInfo: FileModel = (await sut.getFile(
testFileId,
DatasetNotNumberedVersion.LATEST,
false
)) as FileModel

expect(fileInfo.description).toBe(testFileMetadata.description)
expect(fileInfo.categories).toEqual(testFileMetadata.categories)
expect(fileInfo.restricted).toBe(testFileMetadata.restrict)
})

test('should return error when file does not exist', async () => {
const testFileMetadata = {
description: 'My description test.',
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
)
})
})

describe('deleteFile', () => {
let deleFileTestDatasetIds: CreatedDatasetIdentifiers
const testTextFile1Name = 'test-file-1.txt'
Expand Down
16 changes: 16 additions & 0 deletions test/testHelpers/files/filesHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,22 @@ export const updateFileTabularTags = async (
)
}

export const getFileMetadata = async (fileId: number): Promise<AxiosResponse> => {
return await axios.get(`${TestConstants.TEST_API_URL}/files/${fileId}/metadata`, {
headers: {
'X-Dataverse-Key': process.env.TEST_API_KEY
}
})
}

export const createFileMetadataWithCategories = (): FileMetadata => {
return {
categories: ['category1', 'category2'],
description: 'description',
directoryLabel: 'directoryLabel'
}
}

export const calculateBlobChecksum = (blob: Buffer, checksumAlgorithm: string): string => {
const hash = crypto.createHash(checksumAlgorithm)
hash.update(blob)
Expand Down
46 changes: 46 additions & 0 deletions test/unit/files/UpdateFileMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})