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
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 c

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_ALIAS
): 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
Loading