Skip to content

Commit c54c37d

Browse files
authored
Merge pull request #285 from IQSS/DatasetVersionDiffModelChange
Implement use case for editing tabular tags and categories
2 parents edf7b28 + 7d22673 commit c54c37d

File tree

11 files changed

+680
-2
lines changed

11 files changed

+680
-2
lines changed

docs/useCases.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,15 +1364,62 @@ const updateFileMetadataDTO = {
13641364
restrict: false
13651365
}
13661366

1367-
await updateFileMetadata.execute(fileId, updateFileMetadataDTO).then((fileId) => {
1368-
console.log(`File updated successfully with file ID: ${fileId}`)
1367+
await updateFileMetadata.execute(fileId, updateFileMetadataDTO).then(() => {
1368+
console.log(`File updated successfully`)
13691369
})
13701370
```
13711371

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

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

1376+
#### Update File Categories
1377+
1378+
Updates Categories of a File.
1379+
1380+
###### Example call:
1381+
1382+
```typescript
1383+
import { updateFileCategories } from '@iqss/dataverse-client-javascript'
1384+
1385+
/* ... */
1386+
1387+
const fileId: number | string = 123
1388+
const categories = ['category 1', 'category 1']
1389+
const replace = true
1390+
1391+
await updateFileCategories.execute(fileId, categories, replace).then(() => {
1392+
console.log(`File updated successfully`)
1393+
})
1394+
```
1395+
1396+
_See [use case](../src/files/domain/useCases/updateFileCategories.ts) implementation_.
1397+
1398+
The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.
1399+
1400+
#### Update File Tabular Tags
1401+
1402+
Updates Tabular Tags of a File.
1403+
1404+
###### Example call:
1405+
1406+
```typescript
1407+
import { updateFileTabularTags } from '@iqss/dataverse-client-javascript'
1408+
1409+
/* ... */
1410+
1411+
const fileId: number | string = 123
1412+
const tabularTags = ['Surveys']
1413+
1414+
await updateFileTabularTags.execute(fileId, tabularTags, replace).then(() => {
1415+
console.log(`File updated successfully`)
1416+
})
1417+
```
1418+
1419+
_See [use case](../src/files/domain/useCases/updateFileTabularTags.ts) implementation_.
1420+
1421+
The `fileId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers.
1422+
13761423
#### Delete a File
13771424

13781425
Deletes a File.

src/files/domain/repositories/IFilesRepository.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,16 @@ export interface IFilesRepository {
7272
fileId: number | string,
7373
updateFileMetadataDTO: UpdateFileMetadataDTO
7474
): Promise<void>
75+
76+
updateFileTabularTags(
77+
fileId: number | string,
78+
tabularTags: string[],
79+
replace?: boolean
80+
): Promise<void>
81+
82+
updateFileCategories(
83+
fileId: number | string,
84+
categories: string[],
85+
replace?: boolean
86+
): Promise<void>
7587
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { IFilesRepository } from '../repositories/IFilesRepository'
3+
4+
export class UpdateFileCategories implements UseCase<void> {
5+
private filesRepository: IFilesRepository
6+
7+
constructor(filesRepository: IFilesRepository) {
8+
this.filesRepository = filesRepository
9+
}
10+
11+
/**
12+
* Updates the categories for a particular File.
13+
* More detailed information about updating a file's categories behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#updating-file-metadata
14+
*
15+
* @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
16+
* @param {string[]} [categories] - The categories to be added to the file.
17+
* @param {boolean} [replace](optional) - If true, replaces the existing categories with the new ones. If false, adds the new categories to the existing ones.
18+
* @returns {Promise<void>}
19+
*/
20+
async execute(fileId: number | string, categories: string[], replace?: boolean): Promise<void> {
21+
await this.filesRepository.updateFileCategories(fileId, categories, replace)
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { IFilesRepository } from '../repositories/IFilesRepository'
3+
4+
export class UpdateFileTabularTags implements UseCase<void> {
5+
private filesRepository: IFilesRepository
6+
7+
constructor(filesRepository: IFilesRepository) {
8+
this.filesRepository = filesRepository
9+
}
10+
11+
/**
12+
* Updates the tabular tabular Tags for a particular File.
13+
* More detailed information about updating a file's tabularTags behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#updating-file-metadata
14+
*
15+
* @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers).
16+
* @param {string[]} [tabularTags] - The tabular tags to be added to the file.
17+
* @param {boolean} [replace](optional) - If true, replaces the existing tabularTags with the new ones. If false, adds the new tabularTags to the existing ones.
18+
* @returns {Promise<void>}
19+
*/
20+
async execute(fileId: number | string, tabularTags: string[], replace?: boolean): Promise<void> {
21+
await this.filesRepository.updateFileTabularTags(fileId, tabularTags, replace)
22+
}
23+
}

src/files/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { DeleteFile } from './domain/useCases/DeleteFile'
1515
import { ReplaceFile } from './domain/useCases/ReplaceFile'
1616
import { RestrictFile } from './domain/useCases/RestrictFile'
1717
import { UpdateFileMetadata } from './domain/useCases/UpdateFileMetadata'
18+
import { UpdateFileTabularTags } from './domain/useCases/UpdateFileTabularTags'
19+
import { UpdateFileCategories } from './domain/useCases/UpdateFileCategories'
1820

1921
const filesRepository = new FilesRepository()
2022
const directUploadClient = new DirectUploadClient(filesRepository)
@@ -34,6 +36,8 @@ const deleteFile = new DeleteFile(filesRepository)
3436
const replaceFile = new ReplaceFile(filesRepository)
3537
const restrictFile = new RestrictFile(filesRepository)
3638
const updateFileMetadata = new UpdateFileMetadata(filesRepository)
39+
const updateFileTabularTags = new UpdateFileTabularTags(filesRepository)
40+
const updateFileCategories = new UpdateFileCategories(filesRepository)
3741

3842
export {
3943
getDatasetFiles,
@@ -50,6 +54,8 @@ export {
5054
deleteFile,
5155
restrictFile,
5256
updateFileMetadata,
57+
updateFileTabularTags,
58+
updateFileCategories,
5359
replaceFile
5460
}
5561

src/files/infra/repositories/FilesRepository.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,4 +381,38 @@ export class FilesRepository extends ApiRepository implements IFilesRepository {
381381
throw error
382382
})
383383
}
384+
385+
public async updateFileTabularTags(
386+
fileId: number | string,
387+
tabularTags: string[],
388+
replace?: boolean
389+
): Promise<void> {
390+
const queryParams = replace !== undefined ? { replace } : {}
391+
return this.doPost(
392+
this.buildApiEndpoint(this.filesResourceName, 'metadata/tabularTags', fileId),
393+
{ tabularTags },
394+
queryParams
395+
)
396+
.then(() => undefined)
397+
.catch((error) => {
398+
throw error
399+
})
400+
}
401+
402+
public async updateFileCategories(
403+
fileId: number | string,
404+
categories: string[],
405+
replace?: boolean
406+
): Promise<void> {
407+
const queryParams = replace !== undefined ? { replace } : {}
408+
return this.doPost(
409+
this.buildApiEndpoint(this.filesResourceName, 'metadata/categories', fileId),
410+
{ categories },
411+
queryParams
412+
)
413+
.then(() => undefined)
414+
.catch((error) => {
415+
throw error
416+
})
417+
}
384418
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import {
2+
ApiConfig,
3+
createDataset,
4+
CreatedDatasetIdentifiers,
5+
WriteError,
6+
updateFileCategories,
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 { FileModel } from '../../../src/files/domain/models/FileModel'
20+
21+
describe('execute', () => {
22+
const testCollectionAlias = 'updateFileMetadataFunctionalTest-categories'
23+
let testDatasetIds: CreatedDatasetIdentifiers
24+
const testTextFile1Name = 'test-file-1.txt'
25+
const metadataUpdate = ['file']
26+
27+
beforeAll(async () => {
28+
ApiConfig.init(
29+
TestConstants.TEST_API_URL,
30+
DataverseApiAuthMechanism.API_KEY,
31+
process.env.TEST_API_KEY
32+
)
33+
await createCollectionViaApi(testCollectionAlias)
34+
35+
try {
36+
testDatasetIds = await createDataset.execute(
37+
TestConstants.TEST_NEW_DATASET_DTO,
38+
testCollectionAlias
39+
)
40+
} catch (error) {
41+
throw new Error('Tests beforeAll(): Error while creating test dataset')
42+
}
43+
44+
await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name).catch(() => {
45+
throw new Error(`Tests beforeAll(): Error while uploading file ${testTextFile1Name}`)
46+
})
47+
})
48+
49+
afterAll(async () => {
50+
try {
51+
await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId)
52+
} catch (error) {
53+
throw new Error('Tests afterAll(): Error while deleting test dataset')
54+
}
55+
56+
try {
57+
await deleteCollectionViaApi(testCollectionAlias)
58+
} catch (error) {
59+
throw new Error('Tests afterAll(): Error while deleting test collection')
60+
}
61+
})
62+
63+
test('should successfully update categories of a file', async () => {
64+
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
65+
const fileId = datasetFiles.files[0].id
66+
67+
try {
68+
await updateFileCategories.execute(fileId, metadataUpdate)
69+
} catch (error) {
70+
throw new Error('File metadata should be updated')
71+
} finally {
72+
const fileInfo: FileModel = (await getFile.execute(
73+
fileId,
74+
DatasetNotNumberedVersion.LATEST
75+
)) as FileModel
76+
77+
expect(fileInfo.categories).toEqual(metadataUpdate)
78+
}
79+
})
80+
81+
test('should successfully update categories of a file with replace parameter', async () => {
82+
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
83+
const fileId = datasetFiles.files[0].id
84+
const newCategories = ['new Category']
85+
try {
86+
await updateFileCategories.execute(fileId, newCategories, true)
87+
} catch (error) {
88+
throw new Error('File metadata should be updated')
89+
} finally {
90+
const fileInfo: FileModel = (await getFile.execute(
91+
fileId,
92+
DatasetNotNumberedVersion.LATEST
93+
)) as FileModel
94+
95+
expect(fileInfo.categories).toEqual(newCategories)
96+
}
97+
})
98+
99+
test('should not duplicate categories when merging', async () => {
100+
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
101+
const fileId = datasetFiles.files[0].id
102+
103+
const initialCategories = ['Category 1', 'Category 2']
104+
const newCategories = ['Category 2', 'Category 3']
105+
const expectedMergedCategories = ['Category 1', 'Category 2', 'Category 3']
106+
107+
await updateFileCategories.execute(fileId, initialCategories, true)
108+
await updateFileCategories.execute(fileId, newCategories, false)
109+
110+
const fileInfo = (await getFile.execute(fileId, DatasetNotNumberedVersion.LATEST)) as FileModel
111+
112+
expect(fileInfo.categories?.sort()).toEqual(expectedMergedCategories.sort())
113+
})
114+
115+
test('should replace categories when replace = true', async () => {
116+
const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId)
117+
const fileId = datasetFiles.files[0].id
118+
119+
const initialCategories = ['Category 1', 'Category 2', 'Category 3']
120+
const newCategories = ['Category 4', 'Category 5', 'Category 6']
121+
122+
await updateFileCategories.execute(fileId, initialCategories, true)
123+
await updateFileCategories.execute(fileId, newCategories, true)
124+
125+
const fileInfo = (await getFile.execute(fileId, DatasetNotNumberedVersion.LATEST)) as FileModel
126+
127+
expect(fileInfo.categories?.sort()).toEqual(newCategories.sort())
128+
})
129+
130+
test('should throw an error when the file id does not exist', async () => {
131+
let writeError: WriteError | undefined = undefined
132+
const nonExistentFileId = 5
133+
134+
try {
135+
await updateFileCategories.execute(nonExistentFileId, metadataUpdate)
136+
throw new Error('Use case should throw an error')
137+
} catch (error) {
138+
writeError = error as WriteError
139+
} finally {
140+
expect(writeError).toBeInstanceOf(WriteError)
141+
expect(writeError?.message).toEqual(
142+
`There was an error when writing the resource. Reason was: [404] File with ID ${nonExistentFileId} not found.`
143+
)
144+
}
145+
})
146+
})

0 commit comments

Comments
 (0)