Skip to content

Commit

Permalink
Merge pull request #204 from IQSS/200-update-collection
Browse files Browse the repository at this point in the history
Adds UpdateCollection use case
  • Loading branch information
ofahimIQSS authored Nov 6, 2024
2 parents a40c600 + 09f1158 commit 67d3b0e
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 33 deletions.
29 changes: 29 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The different use cases currently available in the package are classified below,
- [List All Collection Items](#list-all-collection-items)
- [Collections write use cases](#collections-write-use-cases)
- [Create a Collection](#create-a-collection)
- [Update a Collection](#update-a-collection)
- [Publish a Collection](#publish-a-collection)
- [Datasets](#Datasets)
- [Datasets read use cases](#datasets-read-use-cases)
Expand Down Expand Up @@ -232,6 +233,34 @@ The above example creates the new collection in the root collection since no col

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

#### Update a Collection

Updates an existing collection, given a collection identifier and a [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) including the updated collection data.

##### Example call:

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

/* ... */

const collectionIdOrAlias = 12345
const collectionDTO: CollectionDTO = {
alias: alias,
name: 'Updated Collection Name',
contacts: ['[email protected]'],
type: CollectionType.DEPARTMENT
}

updateCollection.execute(collectionIdOrAlias, collectionDTO)

/* ... */
```

_See [use case](../src/collections/domain/useCases/UpdateCollection.ts) implementation_.

The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId).

#### Publish a Collection

Publishes a Collection, given the collection identifier.
Expand Down
4 changes: 4 additions & 0 deletions src/collections/domain/repositories/ICollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ export interface ICollectionsRepository {
offset?: number,
collectionSearchCriteria?: CollectionSearchCriteria
): Promise<CollectionItemSubset>
updateCollection(
collectionIdOrAlias: number | string,
updatedCollection: CollectionDTO
): Promise<void>
}
26 changes: 26 additions & 0 deletions src/collections/domain/useCases/UpdateCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { CollectionDTO } from '../dtos/CollectionDTO'
import { ICollectionsRepository } from '../repositories/ICollectionsRepository'

export class UpdateCollection implements UseCase<void> {
private collectionsRepository: ICollectionsRepository

constructor(collectionsRepository: ICollectionsRepository) {
this.collectionsRepository = collectionsRepository
}

/**
* Updates an existing collection, given a collection identifier and a CollectionDTO including the updated collection data.
*
* @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)
* @param {CollectionDTO} [newCollection] - CollectionDTO object including the updated collection data.
* @returns {Promise<void>} -This method does not return anything upon successful completion.
* @throws {WriteError} - If there are errors while writing data.
*/
async execute(
collectionIdOrAlias: number | string,
updatedCollection: CollectionDTO
): Promise<void> {
return await this.collectionsRepository.updateCollection(collectionIdOrAlias, updatedCollection)
}
}
5 changes: 4 additions & 1 deletion src/collections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GetCollectionFacets } from './domain/useCases/GetCollectionFacets'
import { GetCollectionUserPermissions } from './domain/useCases/GetCollectionUserPermissions'
import { GetCollectionItems } from './domain/useCases/GetCollectionItems'
import { PublishCollection } from './domain/useCases/PublishCollection'
import { UpdateCollection } from './domain/useCases/UpdateCollection'

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

Expand All @@ -15,14 +16,16 @@ const getCollectionFacets = new GetCollectionFacets(collectionsRepository)
const getCollectionUserPermissions = new GetCollectionUserPermissions(collectionsRepository)
const getCollectionItems = new GetCollectionItems(collectionsRepository)
const publishCollection = new PublishCollection(collectionsRepository)
const updateCollection = new UpdateCollection(collectionsRepository)

export {
getCollection,
createCollection,
getCollectionFacets,
getCollectionUserPermissions,
getCollectionItems,
publishCollection
publishCollection,
updateCollection
}
export { Collection, CollectionInputLevel } from './domain/models/Collection'
export { CollectionFacet } from './domain/models/CollectionFacet'
Expand Down
75 changes: 45 additions & 30 deletions src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,36 +67,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections
collectionDTO: CollectionDTO,
parentCollectionId: number | string = ROOT_COLLECTION_ID
): Promise<number> {
const dataverseContacts: NewCollectionContactRequestPayload[] = collectionDTO.contacts.map(
(contact) => ({
contactEmail: contact
})
)

const inputLevelsRequestBody: NewCollectionInputLevelRequestPayload[] =
collectionDTO.inputLevels?.map((inputLevel) => ({
datasetFieldTypeName: inputLevel.datasetFieldName,
include: inputLevel.include,
required: inputLevel.required
}))

const requestBody: NewCollectionRequestPayload = {
alias: collectionDTO.alias,
name: collectionDTO.name,
dataverseContacts: dataverseContacts,
dataverseType: collectionDTO.type,
...(collectionDTO.description && {
description: collectionDTO.description
}),
...(collectionDTO.affiliation && {
affiliation: collectionDTO.affiliation
}),
metadataBlocks: {
metadataBlockNames: collectionDTO.metadataBlockNames,
facetIds: collectionDTO.facetIds,
inputLevels: inputLevelsRequestBody
}
}
const requestBody = this.createCreateOrUpdateRequestBody(collectionDTO)

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

public async updateCollection(
collectionIdOrAlias: string | number,
updatedCollection: CollectionDTO
): Promise<void> {
const requestBody = this.createCreateOrUpdateRequestBody(updatedCollection)

return this.doPut(`/${this.collectionsResourceName}/${collectionIdOrAlias}`, requestBody)
.then(() => undefined)
.catch((error) => {
throw error
})
}

private createCreateOrUpdateRequestBody(
collectionDTO: CollectionDTO
): NewCollectionRequestPayload {
const dataverseContacts: NewCollectionContactRequestPayload[] = collectionDTO.contacts.map(
(contact) => ({
contactEmail: contact
})
)

const inputLevelsRequestBody: NewCollectionInputLevelRequestPayload[] =
collectionDTO.inputLevels?.map((inputLevel) => ({
datasetFieldTypeName: inputLevel.datasetFieldName,
include: inputLevel.include,
required: inputLevel.required
}))

return {
alias: collectionDTO.alias,
name: collectionDTO.name,
dataverseContacts: dataverseContacts,
dataverseType: collectionDTO.type,
...(collectionDTO.description && { description: collectionDTO.description }),
...(collectionDTO.affiliation && { affiliation: collectionDTO.affiliation }),
metadataBlocks: {
metadataBlockNames: collectionDTO.metadataBlockNames,
facetIds: collectionDTO.facetIds,
inputLevels: inputLevelsRequestBody
}
}
}

private applyCollectionSearchCriteriaToQueryParams(
queryParams: GetCollectionItemsQueryParams,
collectionSearchCriteria: CollectionSearchCriteria
Expand Down
4 changes: 2 additions & 2 deletions test/environment/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
POSTGRES_VERSION=13
DATAVERSE_DB_USER=dataverse
SOLR_VERSION=9.3.0
DATAVERSE_IMAGE_REGISTRY=docker.io
DATAVERSE_IMAGE_TAG=unstable
DATAVERSE_IMAGE_REGISTRY=ghcr.io
DATAVERSE_IMAGE_TAG=10904-edit-dataverse-collection
DATAVERSE_BOOTSTRAP_TIMEOUT=5m
54 changes: 54 additions & 0 deletions test/functional/collections/UpdateCollection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
ApiConfig,
WriteError,
createCollection,
getCollection,
updateCollection
} from '../../../src'
import { TestConstants } from '../../testHelpers/TestConstants'
import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig'
import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper'

describe('execute', () => {
beforeEach(async () => {
ApiConfig.init(
TestConstants.TEST_API_URL,
DataverseApiAuthMechanism.API_KEY,
process.env.TEST_API_KEY
)
})

test('should successfully update a new collection', async () => {
const testNewCollectionAlias = 'updateCollection-functional-test'
const testNewCollection = createCollectionDTO(testNewCollectionAlias)
await createCollection.execute(testNewCollection)
const testNewName = 'Updated Name'
testNewCollection.name = testNewName
expect.assertions(1)
try {
await updateCollection.execute(testNewCollectionAlias, testNewCollection)
} catch (error) {
throw new Error('Collection should be updated')
} finally {
const updatedCollection = await getCollection.execute(testNewCollectionAlias)
expect(updatedCollection.name).toBe(testNewName)
}
})

test('should throw an error when the parent collection does not exist', async () => {
const testNewCollection = createCollectionDTO()
expect.assertions(2)
let writeError: WriteError
try {
await updateCollection.execute(TestConstants.TEST_DUMMY_COLLECTION_ID, testNewCollection)
throw new Error('Use case should throw an error')
} catch (error) {
writeError = error
} finally {
expect(writeError).toBeInstanceOf(WriteError)
expect(writeError.message).toEqual(
`There was an error when writing the resource. Reason was: [404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
)
}
})
})
63 changes: 63 additions & 0 deletions test/integration/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,4 +464,67 @@ describe('CollectionsRepository', () => {
).rejects.toThrow(expectedError)
})
})

describe('updateCollection', () => {
const testUpdatedCollectionAlias = 'updateCollection-test-updatedAlias'

afterAll(async () => {
await deleteCollectionViaApi(testUpdatedCollectionAlias)
})

test('should update the collection', async () => {
// First we create a test collection using a CollectionDTO and createCollection method
const collectionDTO = createCollectionDTO('updatedCollection-test-originalAlias')
const testUpdateCollectionId = await sut.createCollection(collectionDTO)
const createdCollection = await sut.getCollection(testUpdateCollectionId)
expect(createdCollection.id).toBe(testUpdateCollectionId)
expect(createdCollection.alias).toBe(collectionDTO.alias)
expect(createdCollection.name).toBe(collectionDTO.name)
expect(createdCollection.affiliation).toBe(collectionDTO.affiliation)
expect(createdCollection.inputLevels?.length).toBe(1)
const inputLevel = createdCollection.inputLevels?.[0]
expect(inputLevel?.datasetFieldName).toBe('geographicCoverage')
expect(inputLevel?.include).toBe(true)
expect(inputLevel?.required).toBe(true)

// Now we update CollectionDTO and verify updates are correctly persisted after calling updateCollection method
collectionDTO.alias = testUpdatedCollectionAlias
const updatedCollectionName = 'updatedCollectionName'
collectionDTO.name = updatedCollectionName
const updatedCollectionAffiliation = 'updatedCollectionAffiliation'
collectionDTO.affiliation = updatedCollectionAffiliation
const updatedInputLevels = [
{
datasetFieldName: 'country',
required: false,
include: true
}
]
collectionDTO.inputLevels = updatedInputLevels
await sut.updateCollection(testUpdateCollectionId, collectionDTO)
const updatedCollection = await sut.getCollection(testUpdateCollectionId)
expect(updatedCollection.id).toBe(testUpdateCollectionId)
expect(updatedCollection.alias).toBe(testUpdatedCollectionAlias)
expect(updatedCollection.name).toBe(updatedCollectionName)
expect(updatedCollection.affiliation).toBe(updatedCollectionAffiliation)
expect(updatedCollection.inputLevels?.length).toBe(1)
const updatedInputLevel = updatedCollection.inputLevels?.[0]
expect(updatedInputLevel?.datasetFieldName).toBe('country')
expect(updatedInputLevel?.include).toBe(true)
expect(updatedInputLevel?.required).toBe(false)
})

test('should return error when collection does not exist', async () => {
const expectedError = new WriteError(
`[404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'`
)
const testCollectionAlias = 'updateCollection-not-found-test'
await expect(
sut.updateCollection(
TestConstants.TEST_DUMMY_COLLECTION_ID,
createCollectionDTO(testCollectionAlias)
)
).rejects.toThrow(expectedError)
})
})
})
58 changes: 58 additions & 0 deletions test/unit/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,64 @@ describe('CollectionsRepository', () => {
})
})

describe('updateCollection', () => {
const testUpdatedCollection = createCollectionDTO()
const testAlias = 'testCollectionAlias'

const testCreatedCollectionId = 1
const testCreateCollectionResponse = {
data: {
status: 'OK',
data: {
id: testCreatedCollectionId
}
}
}

const expectedUpdatedCollectionRequestPayloadJson = JSON.stringify(
createNewCollectionRequestPayload()
)
const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/${testAlias}`

test('should call the API with a correct request payload', async () => {
jest.spyOn(axios, 'put').mockResolvedValue(testCreateCollectionResponse)

// API Key auth
await sut.updateCollection(testAlias, testUpdatedCollection)

expect(axios.put).toHaveBeenCalledWith(
expectedApiEndpoint,
expectedUpdatedCollectionRequestPayloadJson,
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY
)

// Session cookie auth
ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE)

await sut.updateCollection(testAlias, testUpdatedCollection)

expect(axios.put).toHaveBeenCalledWith(
expectedApiEndpoint,
expectedUpdatedCollectionRequestPayloadJson,
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE
)
})

test('should return error result on error response', async () => {
jest.spyOn(axios, 'put').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE)

let error = undefined as unknown as WriteError
await sut.updateCollection(testAlias, testUpdatedCollection).catch((e) => (error = e))

expect(axios.put).toHaveBeenCalledWith(
expectedApiEndpoint,
expectedUpdatedCollectionRequestPayloadJson,
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY
)
expect(error).toBeInstanceOf(Error)
})
})

describe('getCollectionFacets', () => {
const testFacetsSuccessfulResponse = {
data: {
Expand Down
Loading

0 comments on commit 67d3b0e

Please sign in to comment.