Skip to content

Commit 67d3b0e

Browse files
authored
Merge pull request #204 from IQSS/200-update-collection
Adds UpdateCollection use case
2 parents a40c600 + 09f1158 commit 67d3b0e

File tree

10 files changed

+313
-33
lines changed

10 files changed

+313
-33
lines changed

docs/useCases.md

+29
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The different use cases currently available in the package are classified below,
1616
- [List All Collection Items](#list-all-collection-items)
1717
- [Collections write use cases](#collections-write-use-cases)
1818
- [Create a Collection](#create-a-collection)
19+
- [Update a Collection](#update-a-collection)
1920
- [Publish a Collection](#publish-a-collection)
2021
- [Datasets](#Datasets)
2122
- [Datasets read use cases](#datasets-read-use-cases)
@@ -232,6 +233,34 @@ The above example creates the new collection in the root collection since no col
232233

233234
The use case returns a number, which is the identifier of the created collection.
234235

236+
#### Update a Collection
237+
238+
Updates an existing collection, given a collection identifier and a [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) including the updated collection data.
239+
240+
##### Example call:
241+
242+
```typescript
243+
import { updateCollection } from '@iqss/dataverse-client-javascript'
244+
245+
/* ... */
246+
247+
const collectionIdOrAlias = 12345
248+
const collectionDTO: CollectionDTO = {
249+
alias: alias,
250+
name: 'Updated Collection Name',
251+
contacts: ['[email protected]'],
252+
type: CollectionType.DEPARTMENT
253+
}
254+
255+
updateCollection.execute(collectionIdOrAlias, collectionDTO)
256+
257+
/* ... */
258+
```
259+
260+
_See [use case](../src/collections/domain/useCases/UpdateCollection.ts) implementation_.
261+
262+
The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId).
263+
235264
#### Publish a Collection
236265

237266
Publishes a Collection, given the collection identifier.

src/collections/domain/repositories/ICollectionsRepository.ts

+4
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@ export interface ICollectionsRepository {
2222
offset?: number,
2323
collectionSearchCriteria?: CollectionSearchCriteria
2424
): Promise<CollectionItemSubset>
25+
updateCollection(
26+
collectionIdOrAlias: number | string,
27+
updatedCollection: CollectionDTO
28+
): Promise<void>
2529
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { UseCase } from '../../../core/domain/useCases/UseCase'
2+
import { CollectionDTO } from '../dtos/CollectionDTO'
3+
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'
4+
5+
export class UpdateCollection implements UseCase<void> {
6+
private collectionsRepository: ICollectionsRepository
7+
8+
constructor(collectionsRepository: ICollectionsRepository) {
9+
this.collectionsRepository = collectionsRepository
10+
}
11+
12+
/**
13+
* Updates an existing collection, given a collection identifier and a CollectionDTO including the updated collection data.
14+
*
15+
* @param {number | string} [collectionIdOrAlias] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId)
16+
* @param {CollectionDTO} [newCollection] - CollectionDTO object including the updated collection data.
17+
* @returns {Promise<void>} -This method does not return anything upon successful completion.
18+
* @throws {WriteError} - If there are errors while writing data.
19+
*/
20+
async execute(
21+
collectionIdOrAlias: number | string,
22+
updatedCollection: CollectionDTO
23+
): Promise<void> {
24+
return await this.collectionsRepository.updateCollection(collectionIdOrAlias, updatedCollection)
25+
}
26+
}

src/collections/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GetCollectionFacets } from './domain/useCases/GetCollectionFacets'
44
import { GetCollectionUserPermissions } from './domain/useCases/GetCollectionUserPermissions'
55
import { GetCollectionItems } from './domain/useCases/GetCollectionItems'
66
import { PublishCollection } from './domain/useCases/PublishCollection'
7+
import { UpdateCollection } from './domain/useCases/UpdateCollection'
78

89
import { CollectionsRepository } from './infra/repositories/CollectionsRepository'
910

@@ -15,14 +16,16 @@ const getCollectionFacets = new GetCollectionFacets(collectionsRepository)
1516
const getCollectionUserPermissions = new GetCollectionUserPermissions(collectionsRepository)
1617
const getCollectionItems = new GetCollectionItems(collectionsRepository)
1718
const publishCollection = new PublishCollection(collectionsRepository)
19+
const updateCollection = new UpdateCollection(collectionsRepository)
1820

1921
export {
2022
getCollection,
2123
createCollection,
2224
getCollectionFacets,
2325
getCollectionUserPermissions,
2426
getCollectionItems,
25-
publishCollection
27+
publishCollection,
28+
updateCollection
2629
}
2730
export { Collection, CollectionInputLevel } from './domain/models/Collection'
2831
export { CollectionFacet } from './domain/models/CollectionFacet'

src/collections/infra/repositories/CollectionsRepository.ts

+45-30
Original file line numberDiff line numberDiff line change
@@ -67,36 +67,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections
6767
collectionDTO: CollectionDTO,
6868
parentCollectionId: number | string = ROOT_COLLECTION_ID
6969
): Promise<number> {
70-
const dataverseContacts: NewCollectionContactRequestPayload[] = collectionDTO.contacts.map(
71-
(contact) => ({
72-
contactEmail: contact
73-
})
74-
)
75-
76-
const inputLevelsRequestBody: NewCollectionInputLevelRequestPayload[] =
77-
collectionDTO.inputLevels?.map((inputLevel) => ({
78-
datasetFieldTypeName: inputLevel.datasetFieldName,
79-
include: inputLevel.include,
80-
required: inputLevel.required
81-
}))
82-
83-
const requestBody: NewCollectionRequestPayload = {
84-
alias: collectionDTO.alias,
85-
name: collectionDTO.name,
86-
dataverseContacts: dataverseContacts,
87-
dataverseType: collectionDTO.type,
88-
...(collectionDTO.description && {
89-
description: collectionDTO.description
90-
}),
91-
...(collectionDTO.affiliation && {
92-
affiliation: collectionDTO.affiliation
93-
}),
94-
metadataBlocks: {
95-
metadataBlockNames: collectionDTO.metadataBlockNames,
96-
facetIds: collectionDTO.facetIds,
97-
inputLevels: inputLevelsRequestBody
98-
}
99-
}
70+
const requestBody = this.createCreateOrUpdateRequestBody(collectionDTO)
10071

10172
return this.doPost(`/${this.collectionsResourceName}/${parentCollectionId}`, requestBody)
10273
.then((response) => response.data.data.id)
@@ -185,6 +156,50 @@ export class CollectionsRepository extends ApiRepository implements ICollections
185156
})
186157
}
187158

159+
public async updateCollection(
160+
collectionIdOrAlias: string | number,
161+
updatedCollection: CollectionDTO
162+
): Promise<void> {
163+
const requestBody = this.createCreateOrUpdateRequestBody(updatedCollection)
164+
165+
return this.doPut(`/${this.collectionsResourceName}/${collectionIdOrAlias}`, requestBody)
166+
.then(() => undefined)
167+
.catch((error) => {
168+
throw error
169+
})
170+
}
171+
172+
private createCreateOrUpdateRequestBody(
173+
collectionDTO: CollectionDTO
174+
): NewCollectionRequestPayload {
175+
const dataverseContacts: NewCollectionContactRequestPayload[] = collectionDTO.contacts.map(
176+
(contact) => ({
177+
contactEmail: contact
178+
})
179+
)
180+
181+
const inputLevelsRequestBody: NewCollectionInputLevelRequestPayload[] =
182+
collectionDTO.inputLevels?.map((inputLevel) => ({
183+
datasetFieldTypeName: inputLevel.datasetFieldName,
184+
include: inputLevel.include,
185+
required: inputLevel.required
186+
}))
187+
188+
return {
189+
alias: collectionDTO.alias,
190+
name: collectionDTO.name,
191+
dataverseContacts: dataverseContacts,
192+
dataverseType: collectionDTO.type,
193+
...(collectionDTO.description && { description: collectionDTO.description }),
194+
...(collectionDTO.affiliation && { affiliation: collectionDTO.affiliation }),
195+
metadataBlocks: {
196+
metadataBlockNames: collectionDTO.metadataBlockNames,
197+
facetIds: collectionDTO.facetIds,
198+
inputLevels: inputLevelsRequestBody
199+
}
200+
}
201+
}
202+
188203
private applyCollectionSearchCriteriaToQueryParams(
189204
queryParams: GetCollectionItemsQueryParams,
190205
collectionSearchCriteria: CollectionSearchCriteria

test/environment/.env

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
POSTGRES_VERSION=13
22
DATAVERSE_DB_USER=dataverse
33
SOLR_VERSION=9.3.0
4-
DATAVERSE_IMAGE_REGISTRY=docker.io
5-
DATAVERSE_IMAGE_TAG=unstable
4+
DATAVERSE_IMAGE_REGISTRY=ghcr.io
5+
DATAVERSE_IMAGE_TAG=10904-edit-dataverse-collection
66
DATAVERSE_BOOTSTRAP_TIMEOUT=5m
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
ApiConfig,
3+
WriteError,
4+
createCollection,
5+
getCollection,
6+
updateCollection
7+
} from '../../../src'
8+
import { TestConstants } from '../../testHelpers/TestConstants'
9+
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
10+
import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper'
11+
12+
describe('execute', () => {
13+
beforeEach(async () => {
14+
ApiConfig.init(
15+
TestConstants.TEST_API_URL,
16+
DataverseApiAuthMechanism.API_KEY,
17+
process.env.TEST_API_KEY
18+
)
19+
})
20+
21+
test('should successfully update a new collection', async () => {
22+
const testNewCollectionAlias = 'updateCollection-functional-test'
23+
const testNewCollection = createCollectionDTO(testNewCollectionAlias)
24+
await createCollection.execute(testNewCollection)
25+
const testNewName = 'Updated Name'
26+
testNewCollection.name = testNewName
27+
expect.assertions(1)
28+
try {
29+
await updateCollection.execute(testNewCollectionAlias, testNewCollection)
30+
} catch (error) {
31+
throw new Error('Collection should be updated')
32+
} finally {
33+
const updatedCollection = await getCollection.execute(testNewCollectionAlias)
34+
expect(updatedCollection.name).toBe(testNewName)
35+
}
36+
})
37+
38+
test('should throw an error when the parent collection does not exist', async () => {
39+
const testNewCollection = createCollectionDTO()
40+
expect.assertions(2)
41+
let writeError: WriteError
42+
try {
43+
await updateCollection.execute(TestConstants.TEST_DUMMY_COLLECTION_ID, testNewCollection)
44+
throw new Error('Use case should throw an error')
45+
} catch (error) {
46+
writeError = error
47+
} finally {
48+
expect(writeError).toBeInstanceOf(WriteError)
49+
expect(writeError.message).toEqual(
50+
`There was an error when writing the resource. Reason was: [404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
51+
)
52+
}
53+
})
54+
})

test/integration/collections/CollectionsRepository.test.ts

+63
Original file line numberDiff line numberDiff line change
@@ -464,4 +464,67 @@ describe('CollectionsRepository', () => {
464464
).rejects.toThrow(expectedError)
465465
})
466466
})
467+
468+
describe('updateCollection', () => {
469+
const testUpdatedCollectionAlias = 'updateCollection-test-updatedAlias'
470+
471+
afterAll(async () => {
472+
await deleteCollectionViaApi(testUpdatedCollectionAlias)
473+
})
474+
475+
test('should update the collection', async () => {
476+
// First we create a test collection using a CollectionDTO and createCollection method
477+
const collectionDTO = createCollectionDTO('updatedCollection-test-originalAlias')
478+
const testUpdateCollectionId = await sut.createCollection(collectionDTO)
479+
const createdCollection = await sut.getCollection(testUpdateCollectionId)
480+
expect(createdCollection.id).toBe(testUpdateCollectionId)
481+
expect(createdCollection.alias).toBe(collectionDTO.alias)
482+
expect(createdCollection.name).toBe(collectionDTO.name)
483+
expect(createdCollection.affiliation).toBe(collectionDTO.affiliation)
484+
expect(createdCollection.inputLevels?.length).toBe(1)
485+
const inputLevel = createdCollection.inputLevels?.[0]
486+
expect(inputLevel?.datasetFieldName).toBe('geographicCoverage')
487+
expect(inputLevel?.include).toBe(true)
488+
expect(inputLevel?.required).toBe(true)
489+
490+
// Now we update CollectionDTO and verify updates are correctly persisted after calling updateCollection method
491+
collectionDTO.alias = testUpdatedCollectionAlias
492+
const updatedCollectionName = 'updatedCollectionName'
493+
collectionDTO.name = updatedCollectionName
494+
const updatedCollectionAffiliation = 'updatedCollectionAffiliation'
495+
collectionDTO.affiliation = updatedCollectionAffiliation
496+
const updatedInputLevels = [
497+
{
498+
datasetFieldName: 'country',
499+
required: false,
500+
include: true
501+
}
502+
]
503+
collectionDTO.inputLevels = updatedInputLevels
504+
await sut.updateCollection(testUpdateCollectionId, collectionDTO)
505+
const updatedCollection = await sut.getCollection(testUpdateCollectionId)
506+
expect(updatedCollection.id).toBe(testUpdateCollectionId)
507+
expect(updatedCollection.alias).toBe(testUpdatedCollectionAlias)
508+
expect(updatedCollection.name).toBe(updatedCollectionName)
509+
expect(updatedCollection.affiliation).toBe(updatedCollectionAffiliation)
510+
expect(updatedCollection.inputLevels?.length).toBe(1)
511+
const updatedInputLevel = updatedCollection.inputLevels?.[0]
512+
expect(updatedInputLevel?.datasetFieldName).toBe('country')
513+
expect(updatedInputLevel?.include).toBe(true)
514+
expect(updatedInputLevel?.required).toBe(false)
515+
})
516+
517+
test('should return error when collection does not exist', async () => {
518+
const expectedError = new WriteError(
519+
`[404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
520+
)
521+
const testCollectionAlias = 'updateCollection-not-found-test'
522+
await expect(
523+
sut.updateCollection(
524+
TestConstants.TEST_DUMMY_COLLECTION_ID,
525+
createCollectionDTO(testCollectionAlias)
526+
)
527+
).rejects.toThrow(expectedError)
528+
})
529+
})
467530
})

test/unit/collections/CollectionsRepository.test.ts

+58
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,64 @@ describe('CollectionsRepository', () => {
191191
})
192192
})
193193

194+
describe('updateCollection', () => {
195+
const testUpdatedCollection = createCollectionDTO()
196+
const testAlias = 'testCollectionAlias'
197+
198+
const testCreatedCollectionId = 1
199+
const testCreateCollectionResponse = {
200+
data: {
201+
status: 'OK',
202+
data: {
203+
id: testCreatedCollectionId
204+
}
205+
}
206+
}
207+
208+
const expectedUpdatedCollectionRequestPayloadJson = JSON.stringify(
209+
createNewCollectionRequestPayload()
210+
)
211+
const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/${testAlias}`
212+
213+
test('should call the API with a correct request payload', async () => {
214+
jest.spyOn(axios, 'put').mockResolvedValue(testCreateCollectionResponse)
215+
216+
// API Key auth
217+
await sut.updateCollection(testAlias, testUpdatedCollection)
218+
219+
expect(axios.put).toHaveBeenCalledWith(
220+
expectedApiEndpoint,
221+
expectedUpdatedCollectionRequestPayloadJson,
222+
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY
223+
)
224+
225+
// Session cookie auth
226+
ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE)
227+
228+
await sut.updateCollection(testAlias, testUpdatedCollection)
229+
230+
expect(axios.put).toHaveBeenCalledWith(
231+
expectedApiEndpoint,
232+
expectedUpdatedCollectionRequestPayloadJson,
233+
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE
234+
)
235+
})
236+
237+
test('should return error result on error response', async () => {
238+
jest.spyOn(axios, 'put').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE)
239+
240+
let error = undefined as unknown as WriteError
241+
await sut.updateCollection(testAlias, testUpdatedCollection).catch((e) => (error = e))
242+
243+
expect(axios.put).toHaveBeenCalledWith(
244+
expectedApiEndpoint,
245+
expectedUpdatedCollectionRequestPayloadJson,
246+
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY
247+
)
248+
expect(error).toBeInstanceOf(Error)
249+
})
250+
})
251+
194252
describe('getCollectionFacets', () => {
195253
const testFacetsSuccessfulResponse = {
196254
data: {

0 commit comments

Comments
 (0)