From c6a42d4b3e0512a726625945b6368a31af1b059d Mon Sep 17 00:00:00 2001 From: gerlach Date: Fri, 6 Feb 2026 11:51:43 +0100 Subject: [PATCH 1/4] fix: Use Partial CollectionDTO for UpdateCollection --- src/collections/domain/repositories/ICollectionsRepository.ts | 2 +- src/collections/domain/useCases/UpdateCollection.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index cae28415..8569ad13 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -45,7 +45,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) } From ab21d01f84d7230dcd688856bbfbe01a3a5909d6 Mon Sep 17 00:00:00 2001 From: gerlach Date: Fri, 6 Feb 2026 12:51:25 +0100 Subject: [PATCH 2/4] fix: Updated CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e56fe504..19ca1bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Fixed +- Allow partial CollectionDTO for UpdateCollection use case + ### Removed - Removed date fields validations in create and update dataset use cases, since validation is already handled in the backend and SPA frontend (other clients should perform client side validation also). This avoids duplicated logic and keeps the package focused on its core responsibility. From 79b5a8d3d006b7ee12710c68a314cb4c9754c9c9 Mon Sep 17 00:00:00 2001 From: gerlach Date: Thu, 16 Apr 2026 09:19:42 +0200 Subject: [PATCH 3/4] feat(collections): support partial updates in updateCollection via Partial and dedicated request builder @createUpdateRequestBody --- CHANGELOG.md | 4 +- package-lock.json | 11 ++- .../repositories/CollectionsRepository.ts | 84 ++++++++++++++++++- .../collections/UpdateCollection.test.ts | 25 +++++- .../collections/CollectionsRepository.test.ts | 21 +++++ 5 files changed, 138 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea192433..3772758a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,15 +23,13 @@ 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 -- Allow partial CollectionDTO for UpdateCollection use case - - In GetAllNotificationsByUser use case, additionalInfo field is returned as an object instead of a string. - In GetAllNotificationsByUser use case, added support for filtering unread messages and pagination. - ### Removed - Removed date fields validations in create and update dataset use cases, since validation is already handled in the backend and SPA frontend (other clients should perform client side validation also). This avoids duplicated logic and keeps the package focused on its core responsibility. diff --git a/package-lock.json b/package-lock.json index 40941f67..3d6a7d2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz", "integrity": "sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.21.4", @@ -1555,7 +1556,8 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "peer": true }, "node_modules/@types/prettier": { "version": "2.7.2", @@ -1625,6 +1627,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", "integrity": "sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.51.0", "@typescript-eslint/type-utils": "5.51.0", @@ -2141,6 +2144,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2864,6 +2868,7 @@ "url": "https://tidelift.com/funding/github/npm/browserslist" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001449", "electron-to-chromium": "^1.4.284", @@ -3814,6 +3819,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", "dev": true, + "peer": true, "dependencies": { "@eslint/eslintrc": "^1.4.1", "@humanwhocodes/config-array": "^0.11.8", @@ -6889,6 +6895,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "dev": true, + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -7953,6 +7960,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -8170,6 +8178,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" 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) From cbd451b4f0a7a22b4f9e0a86ef3fd7f58791c43c Mon Sep 17 00:00:00 2001 From: gerlach Date: Thu, 16 Apr 2026 09:34:37 +0200 Subject: [PATCH 4/4] chore: revert unintended package-lock changes --- package-lock.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d6a7d2a..40941f67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,7 +78,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz", "integrity": "sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.21.4", @@ -1556,8 +1555,7 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "peer": true + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "node_modules/@types/prettier": { "version": "2.7.2", @@ -1627,7 +1625,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz", "integrity": "sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.51.0", "@typescript-eslint/type-utils": "5.51.0", @@ -2144,7 +2141,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2868,7 +2864,6 @@ "url": "https://tidelift.com/funding/github/npm/browserslist" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001449", "electron-to-chromium": "^1.4.284", @@ -3819,7 +3814,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", "dev": true, - "peer": true, "dependencies": { "@eslint/eslintrc": "^1.4.1", "@humanwhocodes/config-array": "^0.11.8", @@ -6895,7 +6889,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "dev": true, - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -7960,7 +7953,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -8178,7 +8170,6 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"