diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f3a2c92..3772758a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - Templates: Rename `CreateDatasetTemplateDTO` to `CreateTemplateDTO`. - Templates: Rename `createDatasetTemplate` repository method to `createTemplate`. - Templates: Rename `getDatasetTemplates` repository method to `getTemplatesByCollectionId`. +- Collections: `updateCollection` now supports partial updates by accepting `Partial`. Only explicitly provided fields are sent in update requests, aligning with Dataverse API semantics. Metadata blocks handling was adjusted to respect inheritance flags and avoid invalid field combinations. ### Fixed diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index bc8960c8..2b91183b 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -44,7 +44,7 @@ export interface ICollectionsRepository { ): Promise updateCollection( collectionIdOrAlias: number | string, - updatedCollection: CollectionDTO + updatedCollection: Partial ): Promise getCollectionFeaturedItems(collectionIdOrAlias: number | string): Promise updateCollectionFeaturedItems( diff --git a/src/collections/domain/useCases/UpdateCollection.ts b/src/collections/domain/useCases/UpdateCollection.ts index f1068086..b32a6e05 100644 --- a/src/collections/domain/useCases/UpdateCollection.ts +++ b/src/collections/domain/useCases/UpdateCollection.ts @@ -19,7 +19,7 @@ export class UpdateCollection implements UseCase { */ async execute( collectionIdOrAlias: number | string, - updatedCollection: CollectionDTO + updatedCollection: Partial ): Promise { return await this.collectionsRepository.updateCollection(collectionIdOrAlias, updatedCollection) } diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index e0e459b0..2dd746df 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -215,9 +215,9 @@ export class CollectionsRepository extends ApiRepository implements ICollections public async updateCollection( collectionIdOrAlias: string | number, - updatedCollection: CollectionDTO + updatedCollection: Partial ): Promise { - const requestBody = this.createCreateOrUpdateRequestBody(updatedCollection) + const requestBody = this.createUpdateRequestBody(updatedCollection) return this.doPut(`/${this.collectionsResourceName}/${collectionIdOrAlias}`, requestBody) .then(() => undefined) @@ -332,6 +332,86 @@ export class CollectionsRepository extends ApiRepository implements ICollections } } + private createUpdateRequestBody( + collectionDTO: Partial + ): Partial { + const dataverseContacts: NewCollectionContactRequestPayload[] | undefined = + collectionDTO.contacts?.map((contact) => ({ + contactEmail: contact + })) + const inputLevelsRequestBody: NewCollectionInputLevelRequestPayload[] | undefined = + collectionDTO.inputLevels?.map((inputLevel) => ({ + datasetFieldTypeName: inputLevel.datasetFieldName, + include: inputLevel.include, + required: inputLevel.required + })) + let metadataBlocksRequestBody: Partial | undefined + + const hasMetadataBlocksData = + collectionDTO.metadataBlockNames !== undefined || + collectionDTO.facetIds !== undefined || + collectionDTO.inputLevels !== undefined || + collectionDTO.inheritMetadataBlocksFromParent !== undefined || + collectionDTO.inheritFacetsFromParent !== undefined + + if (hasMetadataBlocksData) { + metadataBlocksRequestBody = {} + if (collectionDTO.inheritMetadataBlocksFromParent !== true) { + if (collectionDTO.metadataBlockNames !== undefined) { + metadataBlocksRequestBody.metadataBlockNames = collectionDTO.metadataBlockNames + } + if (inputLevelsRequestBody !== undefined) { + metadataBlocksRequestBody.inputLevels = inputLevelsRequestBody + } + } + if (collectionDTO.inheritFacetsFromParent !== true) { + if (collectionDTO.facetIds !== undefined) { + metadataBlocksRequestBody.facetIds = collectionDTO.facetIds + } + } + if (collectionDTO.inheritMetadataBlocksFromParent !== undefined) { + metadataBlocksRequestBody.inheritMetadataBlocksFromParent = + collectionDTO.inheritMetadataBlocksFromParent + } + if (collectionDTO.inheritFacetsFromParent !== undefined) { + metadataBlocksRequestBody.inheritFacetsFromParent = collectionDTO.inheritFacetsFromParent + } + } + + // Build the final request body, only including defined fields + const requestBody: Partial = {} + + if (collectionDTO.alias !== undefined) { + requestBody.alias = collectionDTO.alias + } + + if (collectionDTO.name !== undefined) { + requestBody.name = collectionDTO.name + } + + if (dataverseContacts !== undefined) { + requestBody.dataverseContacts = dataverseContacts + } + + if (collectionDTO.type !== undefined) { + requestBody.dataverseType = collectionDTO.type + } + + if (collectionDTO.description !== undefined) { + requestBody.description = collectionDTO.description + } + + if (collectionDTO.affiliation !== undefined) { + requestBody.affiliation = collectionDTO.affiliation + } + + if (metadataBlocksRequestBody !== undefined) { + requestBody.metadataBlocks = metadataBlocksRequestBody + } + + return requestBody + } + private applyCollectionSearchCriteriaToQueryParams( queryParams: URLSearchParams, collectionSearchCriteria: CollectionSearchCriteria diff --git a/test/functional/collections/UpdateCollection.test.ts b/test/functional/collections/UpdateCollection.test.ts index ac5f47e9..56ed98d1 100644 --- a/test/functional/collections/UpdateCollection.test.ts +++ b/test/functional/collections/UpdateCollection.test.ts @@ -3,7 +3,8 @@ import { WriteError, createCollection, getCollection, - updateCollection + updateCollection, + CollectionDTO } from '../../../src' import { TestConstants } from '../../testHelpers/TestConstants' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -35,6 +36,28 @@ describe('execute', () => { } }) + test('should successfully update a collection with partial data (name only)', async () => { + const testNewCollectionAlias = 'updateCollection-partial-test' + const testNewCollection = createCollectionDTO(testNewCollectionAlias) + await createCollection.execute(testNewCollection) + + const partialUpdate: Partial = { + name: 'Partially Updated Name' + } + + expect.assertions(3) + try { + await updateCollection.execute(testNewCollectionAlias, partialUpdate) + } catch (error) { + throw new Error('Collection should be updated with partial data') + } finally { + const updatedCollection = await getCollection.execute(testNewCollectionAlias) + expect(updatedCollection.name).toBe('Partially Updated Name') + expect(updatedCollection.alias).toBe(testNewCollectionAlias) + expect(updatedCollection.type).toBe(testNewCollection.type) + } + }) + test('should throw an error when the parent collection does not exist', async () => { const testNewCollection = createCollectionDTO() expect.assertions(2) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 94b62311..69fde7b2 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -1110,6 +1110,27 @@ describe('CollectionsRepository', () => { expect(updatedInputLevel?.required).toBe(false) }) + test('should update collection with only partial fields (name and affiliation)', async () => { + const collectionDTO = createCollectionDTO('partial-update-test') + const testCollectionId = await sut.createCollection(collectionDTO) + const createdCollection = await sut.getCollection(testCollectionId) + const partialUpdate: Partial = { + name: 'Partially Updated Name', + affiliation: 'New Affiliation' + } + + await sut.updateCollection(testCollectionId, partialUpdate) + const updatedCollection = await sut.getCollection(testCollectionId) + + expect(updatedCollection.name).toBe('Partially Updated Name') + expect(updatedCollection.affiliation).toBe('New Affiliation') + expect(updatedCollection.alias).toBe(createdCollection.alias) + expect(updatedCollection.type).toBe(createdCollection.type) + expect(updatedCollection.contacts).toEqual(createdCollection.contacts) + + await deleteCollectionViaApi(collectionDTO.alias) + }) + test('should update the collection to inherit metadata blocks from parent collection', async () => { const parentCollectionAlias = 'inherit-metablocks-parent-update' const parentCollectionDTO = createCollectionDTO(parentCollectionAlias)