From 9a313b55d3166b6eafd30f0abf0eeeed1eccd64c Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 16:55:07 +0100 Subject: [PATCH 001/123] Stash: GetSearchServices WIP --- src/search/domain/models/SearchService.ts | 4 ++++ .../repositories/ISearchServicesRepository.ts | 5 +++++ .../domain/useCases/GetSearchServices.ts | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 src/search/domain/models/SearchService.ts create mode 100644 src/search/domain/repositories/ISearchServicesRepository.ts create mode 100644 src/search/domain/useCases/GetSearchServices.ts diff --git a/src/search/domain/models/SearchService.ts b/src/search/domain/models/SearchService.ts new file mode 100644 index 00000000..b895878e --- /dev/null +++ b/src/search/domain/models/SearchService.ts @@ -0,0 +1,4 @@ +export interface SearchService { + name: string + displayName: string +} diff --git a/src/search/domain/repositories/ISearchServicesRepository.ts b/src/search/domain/repositories/ISearchServicesRepository.ts new file mode 100644 index 00000000..f41a477a --- /dev/null +++ b/src/search/domain/repositories/ISearchServicesRepository.ts @@ -0,0 +1,5 @@ +import { SearchService } from '../models/SearchService' + +export interface ISearchServicesRepository { + getSearchServices(): Promise +} diff --git a/src/search/domain/useCases/GetSearchServices.ts b/src/search/domain/useCases/GetSearchServices.ts new file mode 100644 index 00000000..9fa0a0c0 --- /dev/null +++ b/src/search/domain/useCases/GetSearchServices.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { SearchService } from '../models/SearchService' +import { ISearchServicesRepository } from '../repositories/ISearchServicesRepository' + +export class GetSearchServices implements UseCase { + private searchServicesRepository: ISearchServicesRepository + + constructor(searchServicesRepository: ISearchServicesRepository) { + this.searchServicesRepository = searchServicesRepository + } + + /** + * Returns all search services available in the installation. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.searchServicesRepository.getSearchServices() + } +} From b0e2c665403caedaba7f8a01595a225d354fd3c4 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 17:02:51 +0100 Subject: [PATCH 002/123] Stash: SearchServicesRepository WIP --- src/search/index.ts | 8 ++++++++ .../infra/repositories/SearchServicesRepository.ts | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/search/index.ts create mode 100644 src/search/infra/repositories/SearchServicesRepository.ts diff --git a/src/search/index.ts b/src/search/index.ts new file mode 100644 index 00000000..3efd674e --- /dev/null +++ b/src/search/index.ts @@ -0,0 +1,8 @@ +import { GetSearchServices } from './domain/useCases/GetSearchServices' +import { SearchServicesRepository } from './infra/repositories/SearchServicesRepository' + +const searchServicesRepository = new SearchServicesRepository() + +const getSearchServices = new GetSearchServices(searchServicesRepository) + +export { getSearchServices } diff --git a/src/search/infra/repositories/SearchServicesRepository.ts b/src/search/infra/repositories/SearchServicesRepository.ts new file mode 100644 index 00000000..a1ea03e8 --- /dev/null +++ b/src/search/infra/repositories/SearchServicesRepository.ts @@ -0,0 +1,9 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { SearchService } from '../../domain/models/SearchService' +import { ISearchServicesRepository } from '../../domain/repositories/ISearchServicesRepository' + +export class SearchServicesRepository extends ApiRepository implements ISearchServicesRepository { + public async getSearchServices(): Promise { + throw new Error('Method not implemented.') + } +} From e6dfcfdefb6f71b7b81f1b9cbf085d9fa1d5d827 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 17:31:02 +0100 Subject: [PATCH 003/123] Added: search services repository logic --- .../repositories/SearchServicesRepository.ts | 7 +++++- .../transformers/SearchServicePayload.ts | 4 ++++ .../transformers/searchServiceTransformers.ts | 24 +++++++++++++++++++ test/environment/.env | 2 +- .../search/SearchServicesRepository.test.ts | 24 +++++++++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/search/infra/repositories/transformers/SearchServicePayload.ts create mode 100644 src/search/infra/repositories/transformers/searchServiceTransformers.ts create mode 100644 test/integration/search/SearchServicesRepository.test.ts diff --git a/src/search/infra/repositories/SearchServicesRepository.ts b/src/search/infra/repositories/SearchServicesRepository.ts index a1ea03e8..5c375f4a 100644 --- a/src/search/infra/repositories/SearchServicesRepository.ts +++ b/src/search/infra/repositories/SearchServicesRepository.ts @@ -1,9 +1,14 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { SearchService } from '../../domain/models/SearchService' import { ISearchServicesRepository } from '../../domain/repositories/ISearchServicesRepository' +import { transformSearchServicesResponseToSearchServices } from './transformers/searchServiceTransformers' export class SearchServicesRepository extends ApiRepository implements ISearchServicesRepository { public async getSearchServices(): Promise { - throw new Error('Method not implemented.') + return this.doGet(`/searchServices/`) + .then((response) => transformSearchServicesResponseToSearchServices(response)) + .catch((error) => { + throw error + }) } } diff --git a/src/search/infra/repositories/transformers/SearchServicePayload.ts b/src/search/infra/repositories/transformers/SearchServicePayload.ts new file mode 100644 index 00000000..b5b45fc4 --- /dev/null +++ b/src/search/infra/repositories/transformers/SearchServicePayload.ts @@ -0,0 +1,4 @@ +export interface SearchServicePayload { + name: string + displayName: string +} diff --git a/src/search/infra/repositories/transformers/searchServiceTransformers.ts b/src/search/infra/repositories/transformers/searchServiceTransformers.ts new file mode 100644 index 00000000..722d9138 --- /dev/null +++ b/src/search/infra/repositories/transformers/searchServiceTransformers.ts @@ -0,0 +1,24 @@ +import { AxiosResponse } from 'axios' +import { SearchService } from '../../../domain/models/SearchService' +import { SearchServicePayload } from './SearchServicePayload' + +export const transformSearchServicesResponseToSearchServices = ( + response: AxiosResponse +): SearchService[] => { + const searchServicesPayload = response.data.data + const searchServices: SearchService[] = [] + searchServicesPayload.forEach(function (searchServicePayload: SearchServicePayload) { + searchServices.push(transformSearchServicePayloadToSearchService(searchServicePayload)) + }) + + return searchServices +} + +const transformSearchServicePayloadToSearchService = ( + searchServicePayload: SearchServicePayload +): SearchService => { + return { + name: searchServicePayload.name, + displayName: searchServicePayload.displayName + } +} diff --git a/test/environment/.env b/test/environment/.env index 0c691d9b..06f82c89 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=13 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_REGISTRY=ghcr.io DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/integration/search/SearchServicesRepository.test.ts b/test/integration/search/SearchServicesRepository.test.ts new file mode 100644 index 00000000..829e0e5b --- /dev/null +++ b/test/integration/search/SearchServicesRepository.test.ts @@ -0,0 +1,24 @@ +import { ApiConfig } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { SearchServicesRepository } from '../../../src/search/infra/repositories/SearchServicesRepository' +import { TestConstants } from '../../testHelpers/TestConstants' + +// TODO +describe.skip('SearchServicesRepository', () => { + const sut: SearchServicesRepository = new SearchServicesRepository() + + afterAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + describe('getSearchServices', () => { + test('should return search services', async () => { + const actual = await sut.getSearchServices() + expect(actual.length).toEqual(2) + }) + }) +}) From 6eca761b1c06ca8c2f33af536458702243b02715 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 17:32:19 +0100 Subject: [PATCH 004/123] Fixed: search services endpoint --- src/search/infra/repositories/SearchServicesRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search/infra/repositories/SearchServicesRepository.ts b/src/search/infra/repositories/SearchServicesRepository.ts index 5c375f4a..343fb686 100644 --- a/src/search/infra/repositories/SearchServicesRepository.ts +++ b/src/search/infra/repositories/SearchServicesRepository.ts @@ -5,7 +5,7 @@ import { transformSearchServicesResponseToSearchServices } from './transformers/ export class SearchServicesRepository extends ApiRepository implements ISearchServicesRepository { public async getSearchServices(): Promise { - return this.doGet(`/searchServices/`) + return this.doGet(`/search/services`) .then((response) => transformSearchServicesResponseToSearchServices(response)) .catch((error) => { throw error From 34cb64e41854a6e305952c3362b3be362f3a14c4 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 17:33:53 +0100 Subject: [PATCH 005/123] Changed: registry --- test/environment/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/environment/.env b/test/environment/.env index 06f82c89..0c691d9b 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=13 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_REGISTRY=docker.io DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 05b99a70bada5c35907be3ddcd5613f27f78391f Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 17:40:31 +0100 Subject: [PATCH 006/123] Changed: temporarily commented test steps in deploy_pr action --- .github/workflows/deploy_pr.yml | 69 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/.github/workflows/deploy_pr.yml b/.github/workflows/deploy_pr.yml index f8cba6dd..183a254b 100644 --- a/.github/workflows/deploy_pr.yml +++ b/.github/workflows/deploy_pr.yml @@ -3,50 +3,51 @@ name: deploy_pr on: pull_request jobs: - test-unit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 19 + # test-unit: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/setup-node@v3 + # with: + # node-version: 19 - - name: Install npm dependencies - run: npm ci + # - name: Install npm dependencies + # run: npm ci - - name: Run unit tests - run: npm run test:unit + # - name: Run unit tests + # run: npm run test:unit - test-integration: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 19 + # test-integration: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/setup-node@v3 + # with: + # node-version: 19 - - name: Install npm dependencies - run: npm ci + # - name: Install npm dependencies + # run: npm ci - - name: Run integration tests - run: npm run test:integration + # - name: Run integration tests + # run: npm run test:integration - test-functional: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 19 + # test-functional: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/setup-node@v3 + # with: + # node-version: 19 - - name: Install npm dependencies - run: npm ci + # - name: Install npm dependencies + # run: npm ci - - name: Run functional tests - run: npm run test:functional + # - name: Run functional tests + # run: npm run test:functional publish-gpr: - needs: [test-unit, test-integration, test-functional] + #needs: [test-unit, test-integration, test-functional] + needs: [] runs-on: ubuntu-latest permissions: packages: write From 566cc9e7ce8be612ecc4309ead9e8f1ae8a23419 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 17:47:21 +0100 Subject: [PATCH 007/123] Added: support for searchServiceName in getCollectionItems (testless) --- .../domain/repositories/ICollectionsRepository.ts | 3 ++- src/collections/domain/useCases/GetCollectionItems.ts | 8 +++++--- .../infra/repositories/CollectionsRepository.ts | 10 ++++++++-- test/unit/collections/GetCollectionItems.test.ts | 9 +++++++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index c8fc6549..35b63fb0 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -25,7 +25,8 @@ export interface ICollectionsRepository { collectionId?: string, limit?: number, offset?: number, - collectionSearchCriteria?: CollectionSearchCriteria + collectionSearchCriteria?: CollectionSearchCriteria, + searchServiceName?: string ): Promise getMyDataCollectionItems( roleIds: number[], diff --git a/src/collections/domain/useCases/GetCollectionItems.ts b/src/collections/domain/useCases/GetCollectionItems.ts index aa3e2c55..3c4f2d54 100644 --- a/src/collections/domain/useCases/GetCollectionItems.ts +++ b/src/collections/domain/useCases/GetCollectionItems.ts @@ -10,7 +10,7 @@ export class GetCollectionItems implements UseCase { this.collectionsRepository = collectionsRepository } - /** + /** TODO: document searchServiceName * Returns an instance of CollectionItemSubset that contains reduced information for each item that the calling user can access in the installation. * If the collectionId parameter is not set, the use case will return items starting from the root collection. * @@ -24,13 +24,15 @@ export class GetCollectionItems implements UseCase { collectionId?: string, limit?: number, offset?: number, - collectionSearchCriteria?: CollectionSearchCriteria + collectionSearchCriteria?: CollectionSearchCriteria, + searchServiceName?: string ): Promise { return await this.collectionsRepository.getCollectionItems( collectionId, limit, offset, - collectionSearchCriteria + collectionSearchCriteria, + searchServiceName ) } } diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 2be641b0..bfe33037 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -63,7 +63,8 @@ export enum GetCollectionItemsQueryParams { START = 'start', TYPE = 'type', FILTERQUERY = 'fq', - SHOW_TYPE_COUNTS = 'show_type_counts' + SHOW_TYPE_COUNTS = 'show_type_counts', + SEARCH_SERVICE_NAME = 'search_service' } export enum GetMyDataCollectionItemsQueryParams { @@ -155,7 +156,8 @@ export class CollectionsRepository extends ApiRepository implements ICollections collectionId?: string, limit?: number, offset?: number, - collectionSearchCriteria?: CollectionSearchCriteria + collectionSearchCriteria?: CollectionSearchCriteria, + searchServiceName?: string ): Promise { const queryParams = new URLSearchParams({ [GetCollectionItemsQueryParams.QUERY]: '*', @@ -177,6 +179,10 @@ export class CollectionsRepository extends ApiRepository implements ICollections queryParams.set(GetCollectionItemsQueryParams.START, offset.toString()) } + if (searchServiceName) { + queryParams.set(GetCollectionItemsQueryParams.SEARCH_SERVICE_NAME, searchServiceName) + } + if (collectionSearchCriteria) { this.applyCollectionSearchCriteriaToQueryParams(queryParams, collectionSearchCriteria) } diff --git a/test/unit/collections/GetCollectionItems.test.ts b/test/unit/collections/GetCollectionItems.test.ts index cf2e7448..adbeefa8 100644 --- a/test/unit/collections/GetCollectionItems.test.ts +++ b/test/unit/collections/GetCollectionItems.test.ts @@ -60,6 +60,7 @@ describe('execute', () => { collectionId, undefined, undefined, + undefined, undefined ) expect(actual).toEqual(testItemSubset) @@ -75,6 +76,7 @@ describe('execute', () => { undefined, limit, undefined, + undefined, undefined ) expect(actual).toEqual(testItemSubset) @@ -90,6 +92,7 @@ describe('execute', () => { undefined, undefined, offset, + undefined, undefined ) expect(actual).toEqual(testItemSubset) @@ -112,7 +115,8 @@ describe('execute', () => { undefined, undefined, undefined, - searchCriteria + searchCriteria, + undefined ) expect(actual).toEqual(testItemSubset) }) @@ -133,7 +137,8 @@ describe('execute', () => { collectionId, limit, offset, - searchCriteria + searchCriteria, + undefined ) expect(actual).toEqual(testItemSubset) }) From f042d8daaf1b0b83f7d8e81dcda5997e24a97861 Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 28 May 2025 18:03:27 +0100 Subject: [PATCH 008/123] Added: SCORE to SortType --- src/collections/domain/models/CollectionSearchCriteria.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/collections/domain/models/CollectionSearchCriteria.ts b/src/collections/domain/models/CollectionSearchCriteria.ts index 3f36735b..13454e43 100644 --- a/src/collections/domain/models/CollectionSearchCriteria.ts +++ b/src/collections/domain/models/CollectionSearchCriteria.ts @@ -2,7 +2,8 @@ import { CollectionItemType } from './CollectionItemType' export enum SortType { NAME = 'name', - DATE = 'date' + DATE = 'date', + SCORE = 'score' } export enum OrderType { From e29b28eae3f4d0624a35f87119a9639eea1aa4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 28 May 2025 14:25:23 -0300 Subject: [PATCH 009/123] export search module --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 2abdf089..823db36e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,4 @@ export * from './collections' export * from './metadataBlocks' export * from './files' export * from './contactInfo' +export * from './search' From 0c891f8123d8d867d32794959c2357a478f65933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 6 Jun 2025 11:56:58 -0300 Subject: [PATCH 010/123] keep showTypeCounts as last arg --- .../repositories/ICollectionsRepository.ts | 4 +-- .../domain/useCases/GetCollectionItems.ts | 8 ++--- .../repositories/CollectionsRepository.ts | 4 +-- .../collections/CollectionsRepository.test.ts | 1 + .../collections/GetCollectionItems.test.ts | 31 ++++++++++--------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index 0653aad4..13d6feee 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -27,8 +27,8 @@ export interface ICollectionsRepository { limit?: number, offset?: number, collectionSearchCriteria?: CollectionSearchCriteria, - showTypeCounts?: boolean, - searchServiceName?: string + searchServiceName?: string, + showTypeCounts?: boolean ): Promise getMyDataCollectionItems( roleIds: number[], diff --git a/src/collections/domain/useCases/GetCollectionItems.ts b/src/collections/domain/useCases/GetCollectionItems.ts index 6c57cafc..504d8c7f 100644 --- a/src/collections/domain/useCases/GetCollectionItems.ts +++ b/src/collections/domain/useCases/GetCollectionItems.ts @@ -26,16 +26,16 @@ export class GetCollectionItems implements UseCase { limit?: number, offset?: number, collectionSearchCriteria?: CollectionSearchCriteria, - showTypeCounts = false, - searchServiceName?: string + searchServiceName?: string, + showTypeCounts = false ): Promise { return await this.collectionsRepository.getCollectionItems( collectionId, limit, offset, collectionSearchCriteria, - showTypeCounts, - searchServiceName + searchServiceName, + showTypeCounts ) } } diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index e2351909..59607298 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -158,8 +158,8 @@ export class CollectionsRepository extends ApiRepository implements ICollections limit?: number, offset?: number, collectionSearchCriteria?: CollectionSearchCriteria, - showTypeCounts?: boolean, - searchServiceName?: string + searchServiceName?: string, + showTypeCounts?: boolean ): Promise { const queryParams = new URLSearchParams({ [GetCollectionItemsQueryParams.QUERY]: '*', diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index b741537a..1c0bb714 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -771,6 +771,7 @@ describe('CollectionsRepository', () => { undefined, undefined, undefined, + undefined, true ) expect(actual.countPerObjectType?.collections).toBe(1) diff --git a/test/unit/collections/GetCollectionItems.test.ts b/test/unit/collections/GetCollectionItems.test.ts index e9c645c1..79159da7 100644 --- a/test/unit/collections/GetCollectionItems.test.ts +++ b/test/unit/collections/GetCollectionItems.test.ts @@ -55,8 +55,8 @@ describe('execute', () => { undefined, undefined, undefined, - false, - undefined + undefined, + false ) expect(actual).toEqual(testItemSubset) }) @@ -72,8 +72,8 @@ describe('execute', () => { limit, undefined, undefined, - false, - undefined + undefined, + false ) expect(actual).toEqual(testItemSubset) }) @@ -89,8 +89,8 @@ describe('execute', () => { undefined, offset, undefined, - false, - undefined + undefined, + false ) expect(actual).toEqual(testItemSubset) }) @@ -106,7 +106,7 @@ describe('execute', () => { undefined, undefined, searchCriteria, - false + undefined ) expect(collectionRepositoryStub.getCollectionItems).toHaveBeenCalledWith( @@ -114,8 +114,8 @@ describe('execute', () => { undefined, undefined, searchCriteria, - false, - undefined + undefined, + false ) expect(actual).toEqual(testItemSubset) }) @@ -140,6 +140,7 @@ describe('execute', () => { undefined, undefined, undefined, + undefined, showTypeCounts ) @@ -148,8 +149,8 @@ describe('execute', () => { undefined, undefined, undefined, - showTypeCounts, - undefined + undefined, + showTypeCounts ) expect(actual).toEqual(testItemSubsetWithCount) }) @@ -169,8 +170,8 @@ describe('execute', () => { limit, offset, searchCriteria, - false, - undefined + undefined, + false ) expect(collectionRepositoryStub.getCollectionItems).toHaveBeenCalledWith( @@ -178,8 +179,8 @@ describe('execute', () => { limit, offset, searchCriteria, - false, - undefined + undefined, + false ) expect(actual).toEqual(testItemSubset) }) From b84eba2e5dfad674a4823bde4d30791fe1cd63ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 8 Jul 2025 11:54:20 -0300 Subject: [PATCH 011/123] feat: include isAdvancedSearchFieldType prop --- src/metadataBlocks/domain/models/MetadataBlock.ts | 1 + .../infra/repositories/transformers/MetadataFieldInfoPayload.ts | 1 + .../infra/repositories/transformers/metadataBlockTransformers.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/metadataBlocks/domain/models/MetadataBlock.ts b/src/metadataBlocks/domain/models/MetadataBlock.ts index b958d68d..b5cd5166 100644 --- a/src/metadataBlocks/domain/models/MetadataBlock.ts +++ b/src/metadataBlocks/domain/models/MetadataBlock.ts @@ -20,6 +20,7 @@ export interface MetadataFieldInfo { displayFormat: string childMetadataFields?: Record isRequired: boolean + isAdvancedSearchFieldType: boolean displayOrder: number displayOnCreate: boolean } diff --git a/src/metadataBlocks/infra/repositories/transformers/MetadataFieldInfoPayload.ts b/src/metadataBlocks/infra/repositories/transformers/MetadataFieldInfoPayload.ts index a380a4b7..a3cf6446 100644 --- a/src/metadataBlocks/infra/repositories/transformers/MetadataFieldInfoPayload.ts +++ b/src/metadataBlocks/infra/repositories/transformers/MetadataFieldInfoPayload.ts @@ -12,6 +12,7 @@ export interface MetadataFieldInfoPayload { displayFormat: string displayOrder: number isRequired: boolean + isAdvancedSearchFieldType: boolean controlledVocabularyValues?: string[] childMetadataFields?: Record } diff --git a/src/metadataBlocks/infra/repositories/transformers/metadataBlockTransformers.ts b/src/metadataBlocks/infra/repositories/transformers/metadataBlockTransformers.ts index aae50c18..478a472b 100644 --- a/src/metadataBlocks/infra/repositories/transformers/metadataBlockTransformers.ts +++ b/src/metadataBlocks/infra/repositories/transformers/metadataBlockTransformers.ts @@ -98,6 +98,7 @@ const transformPayloadMetadataFieldInfo = ( }), displayFormat: metadataFieldInfoPayload.displayFormat, isRequired: metadataFieldInfoPayload.isRequired, + isAdvancedSearchFieldType: metadataFieldInfoPayload.isAdvancedSearchFieldType, displayOrder: metadataFieldInfoPayload.displayOrder, typeClass: metadataFieldInfoPayload.typeClass as MetadataFieldTypeClass, displayOnCreate: metadataFieldInfoPayload.displayOnCreate From d3bee426627cf8d1010b0681e008158500adc6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 8 Jul 2025 12:19:10 -0300 Subject: [PATCH 012/123] test: include property in helpers --- test/environment/.env | 4 ++-- test/testHelpers/datasets/datasetHelper.ts | 10 ++++++++++ test/testHelpers/metadataBlocks/metadataBlockHelper.ts | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..5b641de0 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=11614-include-isAdvancedSearchField-property DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/testHelpers/datasets/datasetHelper.ts b/test/testHelpers/datasets/datasetHelper.ts index bed1fc69..65575bdc 100644 --- a/test/testHelpers/datasets/datasetHelper.ts +++ b/test/testHelpers/datasets/datasetHelper.ts @@ -514,6 +514,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: true, displayOrder: 0, displayOnCreate: true, typeClass: MetadataFieldTypeClass.Primitive @@ -529,6 +530,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 1, typeClass: MetadataFieldTypeClass.Compound, displayOnCreate: true, @@ -544,6 +546,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: true, displayOrder: 2, displayOnCreate: true, typeClass: MetadataFieldTypeClass.Primitive @@ -559,6 +562,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: false, + isAdvancedSearchFieldType: true, displayOrder: 3, displayOnCreate: true, typeClass: MetadataFieldTypeClass.Primitive @@ -577,6 +581,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 4, displayOnCreate: true, typeClass: MetadataFieldTypeClass.Primitive @@ -592,6 +597,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#NAME: #VALUE ', isRequired: false, + isAdvancedSearchFieldType: false, displayOrder: 5, displayOnCreate: true, typeClass: MetadataFieldTypeClass.Primitive @@ -607,6 +613,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#NAME: #VALUE ', isRequired: false, + isAdvancedSearchFieldType: false, displayOrder: 5, displayOnCreate: true, typeClass: MetadataFieldTypeClass.Primitive @@ -623,6 +630,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: ':', isRequired: false, + isAdvancedSearchFieldType: false, displayOrder: 6, typeClass: MetadataFieldTypeClass.Compound, displayOnCreate: true, @@ -638,6 +646,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: true, displayFormat: '#VALUE ', isRequired: false, + isAdvancedSearchFieldType: false, displayOrder: 7, displayOnCreate: true, controlledVocabularyValues: [ @@ -673,6 +682,7 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 8, typeClass: MetadataFieldTypeClass.Primitive, displayOnCreate: true diff --git a/test/testHelpers/metadataBlocks/metadataBlockHelper.ts b/test/testHelpers/metadataBlocks/metadataBlockHelper.ts index dd041c4c..2d5418e4 100644 --- a/test/testHelpers/metadataBlocks/metadataBlockHelper.ts +++ b/test/testHelpers/metadataBlocks/metadataBlockHelper.ts @@ -26,6 +26,7 @@ export const createMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: MetadataFieldTypeClass.Primitive, displayOnCreate: true @@ -41,6 +42,7 @@ export const createMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: MetadataFieldTypeClass.Compound, displayOnCreate: true, @@ -56,6 +58,7 @@ export const createMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: MetadataFieldTypeClass.Primitive, displayOnCreate: true @@ -71,6 +74,7 @@ export const createMetadataBlockModel = (): MetadataBlock => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: MetadataFieldTypeClass.Primitive, displayOnCreate: true @@ -99,6 +103,7 @@ export const createMetadataBlockPayload = (): MetadataBlockPayload => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: 'primitive', displayOnCreate: true @@ -114,6 +119,7 @@ export const createMetadataBlockPayload = (): MetadataBlockPayload => { isControlledVocabulary: false, displayFormat: '', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: 'compound', displayOnCreate: true, @@ -129,6 +135,7 @@ export const createMetadataBlockPayload = (): MetadataBlockPayload => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: 'primitive', displayOnCreate: true @@ -144,6 +151,7 @@ export const createMetadataBlockPayload = (): MetadataBlockPayload => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: 'primitive', displayOnCreate: true @@ -166,6 +174,7 @@ export const createMetadataFieldInfoModel = (): MetadataFieldInfo => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: MetadataFieldTypeClass.Primitive, displayOnCreate: true @@ -184,6 +193,7 @@ export const createMetadataFieldInfoPayload = (): MetadataFieldInfoPayload => { isControlledVocabulary: false, displayFormat: '#VALUE', isRequired: true, + isAdvancedSearchFieldType: false, displayOrder: 0, typeClass: 'primitive', displayOnCreate: true From e03fa2154b0a3f9d9dd4e4c12cb1e674f5200eef Mon Sep 17 00:00:00 2001 From: GPortas Date: Fri, 11 Jul 2025 17:06:28 +0100 Subject: [PATCH 013/123] Changed: uncommented deploy_pr test steps --- .github/workflows/deploy_pr.yml | 69 ++++++++++++++++----------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/.github/workflows/deploy_pr.yml b/.github/workflows/deploy_pr.yml index 183a254b..f8cba6dd 100644 --- a/.github/workflows/deploy_pr.yml +++ b/.github/workflows/deploy_pr.yml @@ -3,51 +3,50 @@ name: deploy_pr on: pull_request jobs: - # test-unit: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 - # - uses: actions/setup-node@v3 - # with: - # node-version: 19 + test-unit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 19 - # - name: Install npm dependencies - # run: npm ci + - name: Install npm dependencies + run: npm ci - # - name: Run unit tests - # run: npm run test:unit + - name: Run unit tests + run: npm run test:unit - # test-integration: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 - # - uses: actions/setup-node@v3 - # with: - # node-version: 19 + test-integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 19 - # - name: Install npm dependencies - # run: npm ci + - name: Install npm dependencies + run: npm ci - # - name: Run integration tests - # run: npm run test:integration + - name: Run integration tests + run: npm run test:integration - # test-functional: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 - # - uses: actions/setup-node@v3 - # with: - # node-version: 19 + test-functional: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 19 - # - name: Install npm dependencies - # run: npm ci + - name: Install npm dependencies + run: npm ci - # - name: Run functional tests - # run: npm run test:functional + - name: Run functional tests + run: npm run test:functional publish-gpr: - #needs: [test-unit, test-integration, test-functional] - needs: [] + needs: [test-unit, test-integration, test-functional] runs-on: ubuntu-latest permissions: packages: write From 0c49ec545a6aa3773387e213b4362de5a84b8bf7 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 11 Jul 2025 14:51:29 -0400 Subject: [PATCH 014/123] feat: update notification usecases --- docs/useCases.md | 47 +++++++++++ src/index.ts | 1 + .../domain/models/Notification.ts | 7 ++ .../repositories/INotificationsRepository.ts | 6 ++ .../useCases/DeleteNotificationByUser.ts | 16 ++++ .../useCases/GetAllNotificationsByUser.ts | 16 ++++ src/notifications/index.ts | 12 +++ .../repositories/NotificationsRepository.ts | 25 ++++++ .../DeleteNotificationByUser.test.ts | 30 +++++++ .../GetAllNotificationsByUser.test.ts | 33 ++++++++ .../NotificationsRepository.test.ts | 78 +++++++++++++++++++ .../DeleteNotificationByUser.test.ts | 46 +++++++++++ .../GetAllNotificationsByUser.test.ts | 44 +++++++++++ 13 files changed, 361 insertions(+) create mode 100644 src/notifications/domain/models/Notification.ts create mode 100644 src/notifications/domain/repositories/INotificationsRepository.ts create mode 100644 src/notifications/domain/useCases/DeleteNotificationByUser.ts create mode 100644 src/notifications/domain/useCases/GetAllNotificationsByUser.ts create mode 100644 src/notifications/index.ts create mode 100644 src/notifications/infra/repositories/NotificationsRepository.ts create mode 100644 test/functional/notifications/DeleteNotificationByUser.test.ts create mode 100644 test/functional/notifications/GetAllNotificationsByUser.test.ts create mode 100644 test/integration/notifications/NotificationsRepository.test.ts create mode 100644 test/unit/notifications/DeleteNotificationByUser.test.ts create mode 100644 test/unit/notifications/GetAllNotificationsByUser.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 2ccee9fc..fa4f507e 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -85,6 +85,9 @@ The different use cases currently available in the package are classified below, - [Get Application Terms of Use](#get-application-terms-of-use) - [Contact](#Contact) - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) +- [Notifications](#Notifications) + - [Get All Notifications by User](#get-all-notifications-by-user) + - [Delete Notification by User](#delete-notification-by-user) ## Collections @@ -1991,3 +1994,47 @@ In ContactDTO, it takes the following information: - **subject**: the email subject line. - **body**: the email body to send. - **fromEmail**: the email to list in the reply-to field. + +## Notifications + +#### Get All Notifications by User + +Returns a [Notification](../src/notifications/domain/models/Notification.ts) array containing all notifications for the current authenticated user. + +##### Example call: + +```typescript +import { getAllNotificationsByUser } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getAllNotificationsByUser.execute().then((notifications: Notification[]) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/GetAllNotificationsByUser.ts) implementation_. + +#### Delete Notification by User + +Deletes a specific notification for the current authenticated user by its ID. + +##### Example call: + +```typescript +import { deleteNotificationByUser } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const notificationId = 123 + +deleteNotificationByUser.execute(notificationId: number).then(() => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/DeleteNotificationByUser.ts) implementation_. diff --git a/src/index.ts b/src/index.ts index 89a79af6..7669a4c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,3 +8,4 @@ export * from './collections' export * from './metadataBlocks' export * from './files' export * from './contactInfo' +export * from './notifications' diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts new file mode 100644 index 00000000..f99a2170 --- /dev/null +++ b/src/notifications/domain/models/Notification.ts @@ -0,0 +1,7 @@ +export interface Notification { + id: number + type: string + subjectText: string + messageText: string + sentTimestamp: string +} diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts new file mode 100644 index 00000000..c528122b --- /dev/null +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -0,0 +1,6 @@ +import { Notification } from '../models/Notification' + +export interface INotificationsRepository { + getAllNotificationsByUser(): Promise + deleteNotificationByUser(notificationId: number): Promise +} diff --git a/src/notifications/domain/useCases/DeleteNotificationByUser.ts b/src/notifications/domain/useCases/DeleteNotificationByUser.ts new file mode 100644 index 00000000..2c98d43b --- /dev/null +++ b/src/notifications/domain/useCases/DeleteNotificationByUser.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +/** + * Use case for deleting a specific notification for the current user. + * + * @param notificationId - The ID of the notification to delete. + * @returns {Promise} - A promise that resolves when the notification is deleted. + */ +export class DeleteNotificationByUser implements UseCase { + constructor(private readonly notificationsRepository: INotificationsRepository) {} + + async execute(notificationId: number): Promise { + return this.notificationsRepository.deleteNotificationByUser(notificationId) + } +} diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts new file mode 100644 index 00000000..1eb70ac8 --- /dev/null +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { Notification } from '../models/Notification' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +export class GetAllNotificationsByUser implements UseCase { + constructor(private readonly notificationsRepository: INotificationsRepository) {} + + /** + * Use case for retrieving all notifications for the current user. + * + * @returns {Promise} - A promise that resolves to an array of Notification instances. + */ + async execute(): Promise { + return (await this.notificationsRepository.getAllNotificationsByUser()) as Notification[] + } +} diff --git a/src/notifications/index.ts b/src/notifications/index.ts new file mode 100644 index 00000000..a246c783 --- /dev/null +++ b/src/notifications/index.ts @@ -0,0 +1,12 @@ +import { NotificationsRepository } from './infra/repositories/NotificationsRepository' +import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' +import { DeleteNotificationByUser } from './domain/useCases/DeleteNotificationByUser' + +const notificationsRepository = new NotificationsRepository() + +const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) +const deleteNotificationByUser = new DeleteNotificationByUser(notificationsRepository) + +export { getAllNotificationsByUser, deleteNotificationByUser } + +export { Notification } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts new file mode 100644 index 00000000..341f8526 --- /dev/null +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -0,0 +1,25 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { INotificationsRepository } from '../../domain/repositories/INotificationsRepository' +import { Notification } from '../../domain/models/Notification' + +export class NotificationsRepository extends ApiRepository implements INotificationsRepository { + private readonly notificationsResourceName: string = 'notifications' + + public async getAllNotificationsByUser(): Promise { + return this.doGet(this.buildApiEndpoint(this.notificationsResourceName, 'all'), true) + .then((response) => response.data.data.notifications as Notification[]) + .catch((error) => { + throw error + }) + } + + public async deleteNotificationByUser(notificationId: number): Promise { + return this.doDelete( + this.buildApiEndpoint(this.notificationsResourceName, notificationId.toString()) + ) + .then(() => {}) + .catch((error) => { + throw error + }) + } +} diff --git a/test/functional/notifications/DeleteNotificationByUser.test.ts b/test/functional/notifications/DeleteNotificationByUser.test.ts new file mode 100644 index 00000000..eceb81d4 --- /dev/null +++ b/test/functional/notifications/DeleteNotificationByUser.test.ts @@ -0,0 +1,30 @@ +import { + ApiConfig, + deleteNotificationByUser, + getAllNotificationsByUser, + WriteError +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should successfully delete a notification for authenticated user', async () => { + const notificationId = 1 + await deleteNotificationByUser.execute(notificationId) + + const notifications = await getAllNotificationsByUser.execute() + expect(notifications.length).toBe(0) + }) + + test('should throw an error when the notification id does not exist', async () => { + await expect(deleteNotificationByUser.execute(123)).rejects.toThrow(WriteError) + }) +}) diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts new file mode 100644 index 00000000..2202ff20 --- /dev/null +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -0,0 +1,33 @@ +import { ApiConfig, getAllNotificationsByUser, Notification } from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should successfully return notifications for authenticated user', async () => { + const notifications: Notification[] = await getAllNotificationsByUser.execute() + + expect(notifications).not.toBeNull() + expect(Array.isArray(notifications)).toBe(true) + }) + + test('should have correct notification properties if notifications exist', async () => { + const notifications = await getAllNotificationsByUser.execute() + if (notifications.length === 0) { + return + } + const first = notifications[0] + expect(first).toHaveProperty('id') + expect(first).toHaveProperty('type') + expect(first).toHaveProperty('subjectText') + expect(first).toHaveProperty('messageText') + expect(first).toHaveProperty('sentTimestamp') + }) +}) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts new file mode 100644 index 00000000..873650e2 --- /dev/null +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -0,0 +1,78 @@ +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { NotificationsRepository } from '../../../src/notifications/infra/repositories/NotificationsRepository' +import { Notification } from '../../../src/notifications/domain/models/Notification' +import { createDataset } from '../../../src/datasets' +import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' +import { WriteError } from '../../../src' + +describe('NotificationsRepository', () => { + const sut: NotificationsRepository = new NotificationsRepository() + + beforeEach(() => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return notifications after creating and publishing a dataset', async () => { + // Create a dataset and publish it so that a notification of Dataset published is created + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + + const notifications: Notification[] = await sut.getAllNotificationsByUser() + + expect(Array.isArray(notifications)).toBe(true) + expect(notifications.length).toBe(2) + + const publishedNotification = notifications.find((n) => n.type === 'PUBLISHEDDS') + + expect(publishedNotification).toBeDefined() + + expect(publishedNotification).toHaveProperty('id') + expect(publishedNotification).toHaveProperty('type') + expect(publishedNotification).toHaveProperty('subjectText') + expect(publishedNotification).toHaveProperty('messageText') + expect(publishedNotification).toHaveProperty('sentTimestamp') + + expect(publishedNotification?.subjectText).toContain( + 'Dataset created using the createDataset use case' + ) + expect(publishedNotification?.messageText).toContain( + 'Your dataset named Dataset created using the createDataset use case' + ) + }) + + test('should delete a notification by ID', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser() + + const notificationToDelete = notifications[0] + + await sut.deleteNotificationByUser(notificationToDelete.id) + + const notificationsAfterDelete: Notification[] = await sut.getAllNotificationsByUser() + const deletedNotification = notificationsAfterDelete.find( + (n) => n.id === notificationToDelete.id + ) + expect(deletedNotification).toBeUndefined() + }) + + test('should throw error when trying to delete notification with wrong ID', async () => { + const nonExistentMetadataBlockName = 99999 + + const expectedError = new WriteError( + `[404] Notification ${nonExistentMetadataBlockName} not found.` + ) + + await expect(sut.deleteNotificationByUser(nonExistentMetadataBlockName)).rejects.toThrow( + expectedError + ) + }) +}) diff --git a/test/unit/notifications/DeleteNotificationByUser.test.ts b/test/unit/notifications/DeleteNotificationByUser.test.ts new file mode 100644 index 00000000..e8f064c3 --- /dev/null +++ b/test/unit/notifications/DeleteNotificationByUser.test.ts @@ -0,0 +1,46 @@ +import { DeleteNotificationByUser } from '../../../src/notifications/domain/useCases/DeleteNotificationByUser' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { Notification } from '../../../src/notifications/domain/models/Notification' + +const mockNotifications: Notification[] = [ + { + id: 1, + type: 'PUBLISHEDDS', + subjectText: 'Test notification', + messageText: 'Test message', + sentTimestamp: '2025-01-01T00:00:00Z' + }, + { + id: 2, + type: 'ASSIGNROLE', + subjectText: 'Role assignment', + messageText: 'Role assigned', + sentTimestamp: '2025-01-01T00:00:00Z' + } +] + +describe('execute', () => { + test('should delete notification from repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) + notificationsRepositoryStub.deleteNotificationByUser = jest + .fn() + .mockResolvedValue(mockNotifications) + const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + + await sut.execute(123) + + expect(notificationsRepositoryStub.deleteNotificationByUser).toHaveBeenCalledWith(123) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) + notificationsRepositoryStub.deleteNotificationByUser = jest + .fn() + .mockRejectedValue(new Error('Repository error')) + const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + + await expect(sut.execute(123)).rejects.toThrow('Repository error') + }) +}) diff --git a/test/unit/notifications/GetAllNotificationsByUser.test.ts b/test/unit/notifications/GetAllNotificationsByUser.test.ts new file mode 100644 index 00000000..5779f303 --- /dev/null +++ b/test/unit/notifications/GetAllNotificationsByUser.test.ts @@ -0,0 +1,44 @@ +import { GetAllNotificationsByUser } from '../../../src/notifications/domain/useCases/GetAllNotificationsByUser' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { Notification } from '../../../src/notifications/domain/models/Notification' + +const mockNotifications: Notification[] = [ + { + id: 1, + type: 'PUBLISHEDDS', + subjectText: 'Test notification', + messageText: 'Test message', + sentTimestamp: '2025-01-01T00:00:00Z' + }, + { + id: 2, + type: 'ASSIGNROLE', + subjectText: 'Role assignment', + messageText: 'Role assigned', + sentTimestamp: '2025-01-01T00:00:00Z' + } +] + +describe('execute', () => { + test('should return notifications from repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest + .fn() + .mockResolvedValue(mockNotifications) + const sut = new GetAllNotificationsByUser(notificationsRepositoryStub) + + const result = await sut.execute() + + expect(result).toEqual(mockNotifications) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest + .fn() + .mockRejectedValue(new Error('Repository error')) + const sut = new GetAllNotificationsByUser(notificationsRepositoryStub) + + await expect(sut.execute()).rejects.toThrow('Repository error') + }) +}) From 3c12fbe7887e7bc89d7d9dfa6df78a04bb41e40b Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 11 Jul 2025 15:31:55 -0400 Subject: [PATCH 015/123] fix: testcases --- .../notifications/DeleteNotificationByUser.test.ts | 8 +++++--- .../notifications/GetAllNotificationsByUser.test.ts | 13 ++++--------- .../notifications/NotificationsRepository.test.ts | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/test/functional/notifications/DeleteNotificationByUser.test.ts b/test/functional/notifications/DeleteNotificationByUser.test.ts index eceb81d4..93ff971b 100644 --- a/test/functional/notifications/DeleteNotificationByUser.test.ts +++ b/test/functional/notifications/DeleteNotificationByUser.test.ts @@ -17,11 +17,13 @@ describe('execute', () => { }) test('should successfully delete a notification for authenticated user', async () => { - const notificationId = 1 + const notifications = await getAllNotificationsByUser.execute() + const notificationId = notifications[notifications.length - 1].id + await deleteNotificationByUser.execute(notificationId) - const notifications = await getAllNotificationsByUser.execute() - expect(notifications.length).toBe(0) + const notificationsAfterDelete = await getAllNotificationsByUser.execute() + expect(notificationsAfterDelete.length).toBe(notifications.length - 1) }) test('should throw an error when the notification id does not exist', async () => { diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 2202ff20..59d950d9 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -20,14 +20,9 @@ describe('execute', () => { test('should have correct notification properties if notifications exist', async () => { const notifications = await getAllNotificationsByUser.execute() - if (notifications.length === 0) { - return - } - const first = notifications[0] - expect(first).toHaveProperty('id') - expect(first).toHaveProperty('type') - expect(first).toHaveProperty('subjectText') - expect(first).toHaveProperty('messageText') - expect(first).toHaveProperty('sentTimestamp') + + expect(notifications[0]).toHaveProperty('id') + expect(notifications[0]).toHaveProperty('type') + expect(notifications[0]).toHaveProperty('sentTimestamp') }) }) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 873650e2..b5b026ae 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -30,7 +30,7 @@ describe('NotificationsRepository', () => { const notifications: Notification[] = await sut.getAllNotificationsByUser() expect(Array.isArray(notifications)).toBe(true) - expect(notifications.length).toBe(2) + expect(notifications.length).toBeGreaterThan(0) const publishedNotification = notifications.find((n) => n.type === 'PUBLISHEDDS') From 4b5334c3ba86119849dc5c49151acdd7eaadbca8 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 15 Jul 2025 13:02:17 +0100 Subject: [PATCH 016/123] Added: missing docs for GetCollectionItems use case --- docs/useCases.md | 2 ++ src/collections/domain/useCases/GetCollectionItems.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/useCases.md b/docs/useCases.md index 2ccee9fc..33c8f643 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -222,6 +222,8 @@ This use case supports the following optional parameters depending on the search - **limit**: (number) Limit for pagination. - **offset**: (number) Offset for pagination. - **collectionSearchCriteria**: ([CollectionSearchCriteria](../src/collections/domain/models/CollectionSearchCriteria.ts)) Supports filtering the collection items by different properties. +- **searchServiceName**: The search service name on which to execute the search (Optional). +- **showTypeCounts**: If true, the response will include the count per object type (Optional). #### List My Data Collection Items diff --git a/src/collections/domain/useCases/GetCollectionItems.ts b/src/collections/domain/useCases/GetCollectionItems.ts index 504d8c7f..2d8cd10b 100644 --- a/src/collections/domain/useCases/GetCollectionItems.ts +++ b/src/collections/domain/useCases/GetCollectionItems.ts @@ -10,7 +10,7 @@ export class GetCollectionItems implements UseCase { this.collectionsRepository = collectionsRepository } - /** TODO: document searchServiceName + /** * Returns an instance of CollectionItemSubset that contains reduced information for each item that the calling user can access in the installation. * If the collectionId parameter is not set, the use case will return items starting from the root collection. * @@ -18,6 +18,7 @@ export class GetCollectionItems implements UseCase { * @param {number} [limit] - Limit for pagination (optional). * @param {number} [offset] - Offset for pagination (optional). * @param {CollectionSearchCriteria} [collectionSearchCriteria] - Supports filtering the collection items by different properties (optional). + * @param {string} [searchServiceName] - The search service name on which to execute the search (optional). * @param {boolean} [showTypeCounts] - If true, the response will include the count per object type (optional). * @returns {Promise} */ From 4d61527a4f58c3035cace35b7492387080129f40 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 15 Jul 2025 13:09:25 +0100 Subject: [PATCH 017/123] Added: use case docs for GetSearchServices --- docs/useCases.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/useCases.md b/docs/useCases.md index 33c8f643..aa6b56f5 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -85,6 +85,8 @@ The different use cases currently available in the package are classified below, - [Get Application Terms of Use](#get-application-terms-of-use) - [Contact](#Contact) - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) +- [Search](#Search) + - [Get Search Services](#get-search-services) ## Collections @@ -1993,3 +1995,25 @@ In ContactDTO, it takes the following information: - **subject**: the email subject line. - **body**: the email body to send. - **fromEmail**: the email to list in the reply-to field. + +## Search + +#### Get Search Services + +Returns all [Search Services](../src/search/domain/models/SearchService.ts) available in the installation. + +##### Example call: + +```typescript +import { getSearchServices } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getSearchServices.execute().then((searchServices: SearchService[]) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/search/domain/useCases/GetSearchServices.ts) implementation_. From c355cab14edacf4187bac6c4b9e9e602a50208a8 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 15 Jul 2025 13:15:38 +0100 Subject: [PATCH 018/123] Added: unit tests for GetSearchServices use case --- .../testHelpers/search/searchServiceHelper.ts | 8 ++++++ test/unit/search/GetSearchServices.test.ts | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 test/testHelpers/search/searchServiceHelper.ts create mode 100644 test/unit/search/GetSearchServices.test.ts diff --git a/test/testHelpers/search/searchServiceHelper.ts b/test/testHelpers/search/searchServiceHelper.ts new file mode 100644 index 00000000..b8e1f255 --- /dev/null +++ b/test/testHelpers/search/searchServiceHelper.ts @@ -0,0 +1,8 @@ +import { SearchService } from "../../../src/search/domain/models/SearchService" + +export const createSearchServiceModelArray = (count: number): SearchService[] => { + return Array.from({ length: count }, (_, index) => ({ + name: `role${index + 1}`, + displayName: `Role ${index + 1}`, + })) +} diff --git a/test/unit/search/GetSearchServices.test.ts b/test/unit/search/GetSearchServices.test.ts new file mode 100644 index 00000000..ea3e9a1b --- /dev/null +++ b/test/unit/search/GetSearchServices.test.ts @@ -0,0 +1,25 @@ +import { ReadError } from '../../../src' +import { ISearchServicesRepository } from '../../../src/search/domain/repositories/ISearchServicesRepository' +import { GetSearchServices } from '../../../src/search/domain/useCases/GetSearchServices' +import { createSearchServiceModelArray } from '../../testHelpers/search/searchServiceHelper' + +describe('execute', () => { + test('should return search services array on repository success', async () => { + const searchServicesRepositoryStub: ISearchServicesRepository = {} as ISearchServicesRepository + const testServices = createSearchServiceModelArray(5) + searchServicesRepositoryStub.getSearchServices = jest.fn().mockResolvedValue(testServices) + const sut = new GetSearchServices(searchServicesRepositoryStub) + + const actual = await sut.execute() + + expect(actual).toEqual(testServices) + }) + + test('should return error result on repository error', async () => { + const searchServicesRepositoryStub: ISearchServicesRepository = {} as ISearchServicesRepository + searchServicesRepositoryStub.getSearchServices = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetSearchServices(searchServicesRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + }) +}) From 7dbd02d6cf706bf9e82e2942a9ff6269be56f514 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 15 Jul 2025 13:25:07 +0100 Subject: [PATCH 019/123] Added: reformat --- .../repositories/transformers/searchServiceTransformers.ts | 1 + test/integration/search/SearchServicesRepository.test.ts | 3 +-- test/testHelpers/search/searchServiceHelper.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/search/infra/repositories/transformers/searchServiceTransformers.ts b/src/search/infra/repositories/transformers/searchServiceTransformers.ts index 722d9138..b89c881d 100644 --- a/src/search/infra/repositories/transformers/searchServiceTransformers.ts +++ b/src/search/infra/repositories/transformers/searchServiceTransformers.ts @@ -5,6 +5,7 @@ import { SearchServicePayload } from './SearchServicePayload' export const transformSearchServicesResponseToSearchServices = ( response: AxiosResponse ): SearchService[] => { + console.log(response) const searchServicesPayload = response.data.data const searchServices: SearchService[] = [] searchServicesPayload.forEach(function (searchServicePayload: SearchServicePayload) { diff --git a/test/integration/search/SearchServicesRepository.test.ts b/test/integration/search/SearchServicesRepository.test.ts index 829e0e5b..9b4b302b 100644 --- a/test/integration/search/SearchServicesRepository.test.ts +++ b/test/integration/search/SearchServicesRepository.test.ts @@ -3,8 +3,7 @@ import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ import { SearchServicesRepository } from '../../../src/search/infra/repositories/SearchServicesRepository' import { TestConstants } from '../../testHelpers/TestConstants' -// TODO -describe.skip('SearchServicesRepository', () => { +describe('SearchServicesRepository', () => { const sut: SearchServicesRepository = new SearchServicesRepository() afterAll(async () => { diff --git a/test/testHelpers/search/searchServiceHelper.ts b/test/testHelpers/search/searchServiceHelper.ts index b8e1f255..afa55193 100644 --- a/test/testHelpers/search/searchServiceHelper.ts +++ b/test/testHelpers/search/searchServiceHelper.ts @@ -1,8 +1,8 @@ -import { SearchService } from "../../../src/search/domain/models/SearchService" +import { SearchService } from '../../../src/search/domain/models/SearchService' export const createSearchServiceModelArray = (count: number): SearchService[] => { return Array.from({ length: count }, (_, index) => ({ name: `role${index + 1}`, - displayName: `Role ${index + 1}`, + displayName: `Role ${index + 1}` })) } From 439aa62deb49a4d40c927edcf602f3cf3072eb8f Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 15 Jul 2025 13:50:48 +0100 Subject: [PATCH 020/123] Stash: SearchServicesRepository IT WIP --- .../repositories/transformers/searchServiceTransformers.ts | 1 - test/integration/search/SearchServicesRepository.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/search/infra/repositories/transformers/searchServiceTransformers.ts b/src/search/infra/repositories/transformers/searchServiceTransformers.ts index b89c881d..722d9138 100644 --- a/src/search/infra/repositories/transformers/searchServiceTransformers.ts +++ b/src/search/infra/repositories/transformers/searchServiceTransformers.ts @@ -5,7 +5,6 @@ import { SearchServicePayload } from './SearchServicePayload' export const transformSearchServicesResponseToSearchServices = ( response: AxiosResponse ): SearchService[] => { - console.log(response) const searchServicesPayload = response.data.data const searchServices: SearchService[] = [] searchServicesPayload.forEach(function (searchServicePayload: SearchServicePayload) { diff --git a/test/integration/search/SearchServicesRepository.test.ts b/test/integration/search/SearchServicesRepository.test.ts index 9b4b302b..3e971d2c 100644 --- a/test/integration/search/SearchServicesRepository.test.ts +++ b/test/integration/search/SearchServicesRepository.test.ts @@ -6,7 +6,7 @@ import { TestConstants } from '../../testHelpers/TestConstants' describe('SearchServicesRepository', () => { const sut: SearchServicesRepository = new SearchServicesRepository() - afterAll(async () => { + beforeAll(async () => { ApiConfig.init( TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, From 84350c6f06fe1bfd6ca119edb48b066539f44be9 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 15 Jul 2025 18:07:33 +0100 Subject: [PATCH 021/123] Added: missing search services tests --- .../transformers/searchServiceTransformers.ts | 2 +- .../search/GetSearchServices.test.ts | 23 +++++++++++++++++++ .../search/SearchServicesRepository.test.ts | 4 +++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 test/functional/search/GetSearchServices.test.ts diff --git a/src/search/infra/repositories/transformers/searchServiceTransformers.ts b/src/search/infra/repositories/transformers/searchServiceTransformers.ts index 722d9138..2d379e7c 100644 --- a/src/search/infra/repositories/transformers/searchServiceTransformers.ts +++ b/src/search/infra/repositories/transformers/searchServiceTransformers.ts @@ -5,7 +5,7 @@ import { SearchServicePayload } from './SearchServicePayload' export const transformSearchServicesResponseToSearchServices = ( response: AxiosResponse ): SearchService[] => { - const searchServicesPayload = response.data.data + const searchServicesPayload = response.data.data.services const searchServices: SearchService[] = [] searchServicesPayload.forEach(function (searchServicePayload: SearchServicePayload) { searchServices.push(transformSearchServicePayloadToSearchService(searchServicePayload)) diff --git a/test/functional/search/GetSearchServices.test.ts b/test/functional/search/GetSearchServices.test.ts new file mode 100644 index 00000000..af70aae9 --- /dev/null +++ b/test/functional/search/GetSearchServices.test.ts @@ -0,0 +1,23 @@ +import { ApiConfig, getSearchServices } from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { SearchService } from '../../../src/search/domain/models/SearchService' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should successfully return search services', async () => { + const searchServices: SearchService[] = await getSearchServices.execute() + + expect(searchServices).toBeDefined() + expect(searchServices.length).toBe(1) + expect(searchServices[0].name).toBe('solr') + expect(searchServices[0].displayName).toBe('Dataverse Standard Search') + }) +}) diff --git a/test/integration/search/SearchServicesRepository.test.ts b/test/integration/search/SearchServicesRepository.test.ts index 3e971d2c..198eb0b3 100644 --- a/test/integration/search/SearchServicesRepository.test.ts +++ b/test/integration/search/SearchServicesRepository.test.ts @@ -17,7 +17,9 @@ describe('SearchServicesRepository', () => { describe('getSearchServices', () => { test('should return search services', async () => { const actual = await sut.getSearchServices() - expect(actual.length).toEqual(2) + expect(actual.length).toEqual(1) + expect(actual[0].name).toEqual('solr') + expect(actual[0].displayName).toEqual('Dataverse Standard Search') }) }) }) From 011c4adf97d7285e7898da875e5f0b42b32bc786 Mon Sep 17 00:00:00 2001 From: GPortas Date: Tue, 15 Jul 2025 18:12:01 +0100 Subject: [PATCH 022/123] Added: missing export in search subdomain --- src/search/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/search/index.ts b/src/search/index.ts index 3efd674e..56735ae0 100644 --- a/src/search/index.ts +++ b/src/search/index.ts @@ -6,3 +6,5 @@ const searchServicesRepository = new SearchServicesRepository() const getSearchServices = new GetSearchServices(searchServicesRepository) export { getSearchServices } + +export { SearchService } from './domain/models/SearchService' From 271546000319e8428a0bac07e7f8f3edf00ddfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 16 Jul 2025 15:44:41 -0300 Subject: [PATCH 023/123] feat: add link and unlink use cases --- docs/useCases.md | 44 +++++++++++++++++++ .../repositories/IDatasetsRepository.ts | 2 + src/datasets/domain/useCases/LinkDataset.ts | 21 +++++++++ src/datasets/domain/useCases/UnlinkDataset.ts | 21 +++++++++ src/datasets/index.ts | 8 +++- .../infra/repositories/DatasetsRepository.ts | 24 ++++++++++ 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/datasets/domain/useCases/LinkDataset.ts create mode 100644 src/datasets/domain/useCases/UnlinkDataset.ts diff --git a/docs/useCases.md b/docs/useCases.md index 2ccee9fc..47b064cd 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -42,6 +42,8 @@ The different use cases currently available in the package are classified below, - [Publish a Dataset](#publish-a-dataset) - [Deaccession a Dataset](#deaccession-a-dataset) - [Delete a Draft Dataset](#delete-a-draft-dataset) + - [Link a Dataset](#link-a-dataset) + - [Unlink a Dataset](#unlink-a-dataset) - [Files](#Files) - [Files read use cases](#files-read-use-cases) - [Get a File](#get-a-file) @@ -944,6 +946,48 @@ The `datasetId` parameter is a number for numeric identifiers or string for pers If you try to delete a dataset without draft version, you will get a not found error. +#### Link a Dataset + +Creates a link between a Dataset and a Collection. + +##### Example call: + +```typescript +import { linkDataset } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 +const collectionIdOrAlias = 12345 + +linkDataset.execute(datasetId, collectionIdOrAlias) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/LinkDataset.ts) implementation_. + +#### Unlink a Dataset + +Removes a link between a Dataset and a Collection. + +##### Example call: + +```typescript +import { unlinkDataset } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 +const collectionIdOrAlias = 12345 + +unlinkDataset.execute(datasetId, collectionIdOrAlias) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/UnlinkDataset.ts) implementation_. + #### Get Download Count of a Dataset Total number of downloads requested for a dataset, given a dataset numeric identifier, diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 66fa4587..660f7e73 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -61,4 +61,6 @@ export interface IDatasetsRepository { ): Promise getDatasetVersionsSummaries(datasetId: number | string): Promise deleteDatasetDraft(datasetId: number | string): Promise + linkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise + unlinkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise } diff --git a/src/datasets/domain/useCases/LinkDataset.ts b/src/datasets/domain/useCases/LinkDataset.ts new file mode 100644 index 00000000..d521d43b --- /dev/null +++ b/src/datasets/domain/useCases/LinkDataset.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class LinkDataset implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Creates a link between a Dataset and a Collection. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @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) + * @returns {Promise} - This method does not return anything upon successful completion. + */ + async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { + return await this.datasetsRepository.linkDataset(datasetId, collectionIdOrAlias) + } +} diff --git a/src/datasets/domain/useCases/UnlinkDataset.ts b/src/datasets/domain/useCases/UnlinkDataset.ts new file mode 100644 index 00000000..c5122997 --- /dev/null +++ b/src/datasets/domain/useCases/UnlinkDataset.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class UnlinkDataset implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Removes a link between a Dataset and a Collection. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @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) + * @returns {Promise} - This method does not return anything upon successful completion. + */ + async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { + return await this.datasetsRepository.unlinkDataset(datasetId, collectionIdOrAlias) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index ba1fe5d5..e5044121 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -20,6 +20,8 @@ import { DeaccessionDataset } from './domain/useCases/DeaccessionDataset' import { GetDatasetDownloadCount } from './domain/useCases/GetDatasetDownloadCount' import { GetDatasetVersionsSummaries } from './domain/useCases/GetDatasetVersionsSummaries' import { DeleteDatasetDraft } from './domain/useCases/DeleteDatasetDraft' +import { LinkDataset } from './domain/useCases/LinkDataset' +import { UnlinkDataset } from './domain/useCases/UnlinkDataset' const datasetsRepository = new DatasetsRepository() @@ -54,6 +56,8 @@ const deaccessionDataset = new DeaccessionDataset(datasetsRepository) const getDatasetDownloadCount = new GetDatasetDownloadCount(datasetsRepository) const getDatasetVersionsSummaries = new GetDatasetVersionsSummaries(datasetsRepository) const deleteDatasetDraft = new DeleteDatasetDraft(datasetsRepository) +const linkDataset = new LinkDataset(datasetsRepository) +const unlinkDataset = new UnlinkDataset(datasetsRepository) export { getDataset, @@ -71,7 +75,9 @@ export { deaccessionDataset, getDatasetDownloadCount, getDatasetVersionsSummaries, - deleteDatasetDraft + deleteDatasetDraft, + linkDataset, + unlinkDataset } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 036872d6..c912902a 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -287,4 +287,28 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async linkDataset( + datasetId: number | string, + collectionIdOrAlias: number | string + ): Promise { + return this.doPut(`/${this.datasetsResourceName}/${datasetId}/link/${collectionIdOrAlias}`, {}) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async unlinkDataset( + datasetId: number | string, + collectionIdOrAlias: number | string + ): Promise { + return this.doDelete( + `/${this.datasetsResourceName}/${datasetId}/deleteLink/${collectionIdOrAlias}` + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } From 17f5a24b966862c953b52c8f72f1e066a4573cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 16 Jul 2025 17:45:18 -0300 Subject: [PATCH 024/123] test: add functional and integration --- test/functional/datasets/LinkDataset.test.ts | 59 +++++++++++++++ .../functional/datasets/UnlinkDataset.test.ts | 51 +++++++++++++ .../datasets/DatasetsRepository.test.ts | 73 +++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 test/functional/datasets/LinkDataset.test.ts create mode 100644 test/functional/datasets/UnlinkDataset.test.ts diff --git a/test/functional/datasets/LinkDataset.test.ts b/test/functional/datasets/LinkDataset.test.ts new file mode 100644 index 00000000..4171d6a1 --- /dev/null +++ b/test/functional/datasets/LinkDataset.test.ts @@ -0,0 +1,59 @@ +import { ApiConfig, createDataset, linkDataset, WriteError } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { + createCollectionViaApi, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' +import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('execute', () => { + const testCollectionAlias = 'linkDatasetFunctionalTestCollection' + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + it('should link a dataset to another collection', async () => { + const createdDatasetIdentifiers = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO + ) + await createCollectionViaApi(testCollectionAlias) + + const result = await linkDataset.execute( + createdDatasetIdentifiers.numericId, + testCollectionAlias + ) + + expect(result).toBeUndefined() + + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + await deleteCollectionViaApi(testCollectionAlias) + }) + + it('should throw an error when trying to link a dataset to a non-existent collection', async () => { + const createdDatasetIdentifiers = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO + ) + const nonExistentCollectionAlias = 'nonExistentCollection' + + await expect( + linkDataset.execute(createdDatasetIdentifiers.numericId, nonExistentCollectionAlias) + ).rejects.toBeInstanceOf(WriteError) + + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + }) + + it('should throw an error when trying to link a dataset that does not exist', async () => { + await createCollectionViaApi(testCollectionAlias) + const nonExistentDatasetId = 'nonExistentDatasetId' + await expect( + linkDataset.execute(nonExistentDatasetId, testCollectionAlias) + ).rejects.toBeInstanceOf(WriteError) + + await deleteCollectionViaApi(testCollectionAlias) + }) +}) diff --git a/test/functional/datasets/UnlinkDataset.test.ts b/test/functional/datasets/UnlinkDataset.test.ts new file mode 100644 index 00000000..c02a1127 --- /dev/null +++ b/test/functional/datasets/UnlinkDataset.test.ts @@ -0,0 +1,51 @@ +import { ApiConfig, createDataset, linkDataset, unlinkDataset, WriteError } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { + createCollectionViaApi, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' +import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('execute', () => { + const testCollectionAlias = 'unlinkDatasetFunctionalTestCollection' + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + it('should unlink a dataset from a collection', async () => { + const createdDatasetIdentifiers = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO + ) + await createCollectionViaApi(testCollectionAlias) + + await linkDataset.execute(createdDatasetIdentifiers.numericId, testCollectionAlias) + + const result = await unlinkDataset.execute( + createdDatasetIdentifiers.numericId, + testCollectionAlias + ) + + expect(result).toBeUndefined() + + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + await deleteCollectionViaApi(testCollectionAlias) + }) + + it('should throw error when dataset is not linked to the collection', async () => { + const createdDatasetIdentifiers = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO + ) + await createCollectionViaApi(testCollectionAlias) + + await expect( + unlinkDataset.execute(createdDatasetIdentifiers.numericId, testCollectionAlias) + ).rejects.toBeInstanceOf(WriteError) + + await deleteCollectionViaApi(testCollectionAlias) + }) +}) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index c5c93dcd..83566b7f 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1388,4 +1388,77 @@ describe('DatasetsRepository', () => { await expect(sut.deleteDatasetDraft(nonExistentTestDatasetId)).rejects.toThrow(expectedError) }) }) + + describe('linkDataset', () => { + let testDatasetIds: CreatedDatasetIdentifiers + const testCollectionAlias = 'testLinkDatasetCollection' + + beforeAll(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await createCollectionViaApi(testCollectionAlias) + }) + + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + await deleteCollectionViaApi(testCollectionAlias) + }) + + test('should link a dataset to another collection', async () => { + const actual = await sut.linkDataset(testDatasetIds.numericId, testCollectionAlias) + + expect(actual).toBeUndefined() + + // TODO:ME - Once we get linked dataset collections use case assert that the collection exists + }) + + test('should return error when dataset does not exist', async () => { + await expect(sut.linkDataset(nonExistentTestDatasetId, testCollectionAlias)).rejects.toThrow() + }) + + test('should return error when collection does not exist', async () => { + await expect( + sut.linkDataset(testDatasetIds.numericId, 'nonExistentCollectionAlias') + ).rejects.toThrow() + }) + }) + + describe('unlinkDataset', () => { + let testDatasetIds: CreatedDatasetIdentifiers + const testCollectionAlias = 'testUnlinkDatasetCollection' + + beforeAll(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await createCollectionViaApi(testCollectionAlias) + }) + + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + await deleteCollectionViaApi(testCollectionAlias) + }) + + test('should unlink a dataset from a collection', async () => { + await sut.linkDataset(testDatasetIds.numericId, testCollectionAlias) + const actual = await sut.unlinkDataset(testDatasetIds.numericId, testCollectionAlias) + + expect(actual).toBeUndefined() + + // TODO:ME - Once we get linked dataset collections use case assert that the collection exists + }) + + test('should return error when dataset does not exist', async () => { + await expect(sut.linkDataset(nonExistentTestDatasetId, testCollectionAlias)).rejects.toThrow() + }) + + test('should return error when collection does not exist', async () => { + await expect( + sut.linkDataset(testDatasetIds.numericId, 'nonExistentCollectionAlias') + ).rejects.toThrow() + }) + + test('should return error when dataset is not linked to the collection', async () => { + await expect( + sut.unlinkDataset(testDatasetIds.numericId, testCollectionAlias) + ).rejects.toThrow() + }) + }) }) From df0ffa894fc5eb563457f2728806438039d8f336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 17 Jul 2025 09:19:04 -0300 Subject: [PATCH 025/123] fix: numbers for dataset id and string for collection alias --- docs/useCases.md | 8 +++--- .../repositories/IDatasetsRepository.ts | 6 +++-- src/datasets/domain/useCases/LinkDataset.ts | 8 +++--- src/datasets/domain/useCases/UnlinkDataset.ts | 8 +++--- .../infra/repositories/DatasetsRepository.ts | 27 ++++++++++--------- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index 47b064cd..be8410d9 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -958,9 +958,9 @@ import { linkDataset } from '@iqss/dataverse-client-javascript' /* ... */ const datasetId = 1 -const collectionIdOrAlias = 12345 +const collectionAlias = 'collection-alias' -linkDataset.execute(datasetId, collectionIdOrAlias) +linkDataset.execute(datasetId, collectionAlias) /* ... */ ``` @@ -979,9 +979,9 @@ import { unlinkDataset } from '@iqss/dataverse-client-javascript' /* ... */ const datasetId = 1 -const collectionIdOrAlias = 12345 +const collectionAlias = 'collection-alias' -unlinkDataset.execute(datasetId, collectionIdOrAlias) +unlinkDataset.execute(datasetId, collectionAlias) /* ... */ ``` diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 660f7e73..52a6c1cd 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -9,6 +9,7 @@ import { MetadataBlock } from '../../../metadataBlocks' import { DatasetVersionDiff } from '../models/DatasetVersionDiff' import { DatasetDownloadCount } from '../models/DatasetDownloadCount' import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' +import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' export interface IDatasetsRepository { getDataset( @@ -61,6 +62,7 @@ export interface IDatasetsRepository { ): Promise getDatasetVersionsSummaries(datasetId: number | string): Promise deleteDatasetDraft(datasetId: number | string): Promise - linkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise - unlinkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise + linkDataset(datasetId: number, collectionAlias: string): Promise + unlinkDataset(datasetId: number, collectionAlias: string): Promise + getDatasetLinkedCollections(datasetId: number | string): Promise } diff --git a/src/datasets/domain/useCases/LinkDataset.ts b/src/datasets/domain/useCases/LinkDataset.ts index d521d43b..be7f732f 100644 --- a/src/datasets/domain/useCases/LinkDataset.ts +++ b/src/datasets/domain/useCases/LinkDataset.ts @@ -11,11 +11,11 @@ export class LinkDataset implements UseCase { /** * Creates a link between a Dataset and a Collection. * - * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). - * @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 {number} [datasetId] - The dataset id. + * @param {string} [collectionAlias] - The collection alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { - return await this.datasetsRepository.linkDataset(datasetId, collectionIdOrAlias) + async execute(datasetId: number, collectionAlias: string): Promise { + return await this.datasetsRepository.linkDataset(datasetId, collectionAlias) } } diff --git a/src/datasets/domain/useCases/UnlinkDataset.ts b/src/datasets/domain/useCases/UnlinkDataset.ts index c5122997..d2d8eff5 100644 --- a/src/datasets/domain/useCases/UnlinkDataset.ts +++ b/src/datasets/domain/useCases/UnlinkDataset.ts @@ -11,11 +11,11 @@ export class UnlinkDataset implements UseCase { /** * Removes a link between a Dataset and a Collection. * - * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). - * @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 {number} [datasetId] - The dataset id. + * @param {string} [collectionAlias] - The collection alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { - return await this.datasetsRepository.unlinkDataset(datasetId, collectionIdOrAlias) + async execute(datasetId: number, collectionAlias: string): Promise { + return await this.datasetsRepository.unlinkDataset(datasetId, collectionAlias) } } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index c912902a..92a70f6a 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -20,6 +20,7 @@ import { DatasetVersionDiff } from '../../domain/models/DatasetVersionDiff' import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './transformers/datasetVersionDiffTransformers' import { DatasetDownloadCount } from '../../domain/models/DatasetDownloadCount' import { DatasetVersionSummaryInfo } from '../../domain/models/DatasetVersionSummaryInfo' +import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollection' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -288,27 +289,29 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - public async linkDataset( - datasetId: number | string, - collectionIdOrAlias: number | string - ): Promise { - return this.doPut(`/${this.datasetsResourceName}/${datasetId}/link/${collectionIdOrAlias}`, {}) + public async linkDataset(datasetId: number, collectionAlias: string): Promise { + return this.doPut(`/${this.datasetsResourceName}/${datasetId}/link/${collectionAlias}`, {}) .then(() => undefined) .catch((error) => { throw error }) } - public async unlinkDataset( - datasetId: number | string, - collectionIdOrAlias: number | string - ): Promise { - return this.doDelete( - `/${this.datasetsResourceName}/${datasetId}/deleteLink/${collectionIdOrAlias}` - ) + public async unlinkDataset(datasetId: number, collectionAlias: string): Promise { + return this.doDelete(`/${this.datasetsResourceName}/${datasetId}/deleteLink/${collectionAlias}`) .then(() => undefined) .catch((error) => { throw error }) } + + public async getDatasetLinkedCollections( + datasetId: number | string + ): Promise { + return this.doGet(this.buildApiEndpoint(this.datasetsResourceName, 'links', datasetId), true) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } } From 2434086d398dce98593431a33c79c3c2623c910a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 17 Jul 2025 13:14:17 -0300 Subject: [PATCH 026/123] feat: GetDatasetLinkedCollections use case --- docs/useCases.md | 25 +++++++++++++++++++ .../domain/models/DatasetLinkedCollection.ts | 5 ++++ .../useCases/GetDatasetLinkedCollections.ts | 21 ++++++++++++++++ src/datasets/index.ts | 6 ++++- .../infra/repositories/DatasetsRepository.ts | 5 +++- .../DatasetLinkedCollectionsPayload.ts | 9 +++++++ .../datasetLinkedCollectionsTransformers.ts | 12 +++++++++ 7 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/datasets/domain/models/DatasetLinkedCollection.ts create mode 100644 src/datasets/domain/useCases/GetDatasetLinkedCollections.ts create mode 100644 src/datasets/infra/repositories/transformers/DatasetLinkedCollectionsPayload.ts create mode 100644 src/datasets/infra/repositories/transformers/datasetLinkedCollectionsTransformers.ts diff --git a/docs/useCases.md b/docs/useCases.md index be8410d9..5fb4af77 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -36,6 +36,7 @@ The different use cases currently available in the package are classified below, - [Get Differences between Two Dataset Versions](#get-differences-between-two-dataset-versions) - [List All Datasets](#list-all-datasets) - [Get Dataset Versions Summaries](#get-dataset-versions-summaries) + - [Get Dataset Linked Collections](#get-dataset-linked-collections) - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) @@ -737,6 +738,30 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts) The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. +#### Get Dataset Linked Collections + +Returns an array of [DatasetLinkedCollection](../src/datasets/domain/models/DatasetLinkedCollection.ts) that contains the collections linked to a dataset. + +##### Example call: + +```typescript +import { getDatasetLinkedCollections } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 'doi:10.77777/FK2/AAAAAA' + +getDatasetLinkedCollections + .execute(datasetId) + .then((datasetLinkedCollections: DatasetLinkedCollection[]) => { + /* ... */ + }) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetLinkedCollections.ts) implementation_. + ### Datasets Write Use Cases #### Create a Dataset diff --git a/src/datasets/domain/models/DatasetLinkedCollection.ts b/src/datasets/domain/models/DatasetLinkedCollection.ts new file mode 100644 index 00000000..a07e142a --- /dev/null +++ b/src/datasets/domain/models/DatasetLinkedCollection.ts @@ -0,0 +1,5 @@ +export interface DatasetLinkedCollection { + id: number + alias: string + displayName: string +} diff --git a/src/datasets/domain/useCases/GetDatasetLinkedCollections.ts b/src/datasets/domain/useCases/GetDatasetLinkedCollections.ts new file mode 100644 index 00000000..ff2a448c --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetLinkedCollections.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class GetDatasetLinkedCollections implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns a list of collections linked to a dataset. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @returns {Promise} + */ + async execute(datasetId: number | string): Promise { + return await this.datasetsRepository.getDatasetLinkedCollections(datasetId) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index e5044121..a7a7a14b 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -22,6 +22,7 @@ import { GetDatasetVersionsSummaries } from './domain/useCases/GetDatasetVersion import { DeleteDatasetDraft } from './domain/useCases/DeleteDatasetDraft' import { LinkDataset } from './domain/useCases/LinkDataset' import { UnlinkDataset } from './domain/useCases/UnlinkDataset' +import { GetDatasetLinkedCollections } from './domain/useCases/GetDatasetLinkedCollections' const datasetsRepository = new DatasetsRepository() @@ -58,6 +59,7 @@ const getDatasetVersionsSummaries = new GetDatasetVersionsSummaries(datasetsRepo const deleteDatasetDraft = new DeleteDatasetDraft(datasetsRepository) const linkDataset = new LinkDataset(datasetsRepository) const unlinkDataset = new UnlinkDataset(datasetsRepository) +const getDatasetLinkedCollections = new GetDatasetLinkedCollections(datasetsRepository) export { getDataset, @@ -77,7 +79,8 @@ export { getDatasetVersionsSummaries, deleteDatasetDraft, linkDataset, - unlinkDataset + unlinkDataset, + getDatasetLinkedCollections } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' @@ -111,3 +114,4 @@ export { DatasetVersionSummaryInfo, DatasetVersionSummaryStringValues } from './domain/models/DatasetVersionSummaryInfo' +export { DatasetLinkedCollection } from './domain/models/DatasetLinkedCollection' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 92a70f6a..95e82b77 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -21,6 +21,7 @@ import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './trans import { DatasetDownloadCount } from '../../domain/models/DatasetDownloadCount' import { DatasetVersionSummaryInfo } from '../../domain/models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollection' +import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -309,7 +310,9 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi datasetId: number | string ): Promise { return this.doGet(this.buildApiEndpoint(this.datasetsResourceName, 'links', datasetId), true) - .then((response) => response.data.data) + .then((response) => + transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection(response.data.data) + ) .catch((error) => { throw error }) diff --git a/src/datasets/infra/repositories/transformers/DatasetLinkedCollectionsPayload.ts b/src/datasets/infra/repositories/transformers/DatasetLinkedCollectionsPayload.ts new file mode 100644 index 00000000..addff397 --- /dev/null +++ b/src/datasets/infra/repositories/transformers/DatasetLinkedCollectionsPayload.ts @@ -0,0 +1,9 @@ +export interface DatasetLinkedCollectionsPayload { + id: number + identifier: string + 'linked-dataverses': { + id: number + alias: string + displayName: string + }[] +} diff --git a/src/datasets/infra/repositories/transformers/datasetLinkedCollectionsTransformers.ts b/src/datasets/infra/repositories/transformers/datasetLinkedCollectionsTransformers.ts new file mode 100644 index 00000000..66ae1ee5 --- /dev/null +++ b/src/datasets/infra/repositories/transformers/datasetLinkedCollectionsTransformers.ts @@ -0,0 +1,12 @@ +import { DatasetLinkedCollection } from '../../../domain/models/DatasetLinkedCollection' +import { DatasetLinkedCollectionsPayload } from './DatasetLinkedCollectionsPayload' + +export const transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection = ( + payload: DatasetLinkedCollectionsPayload +): DatasetLinkedCollection[] => { + return payload['linked-dataverses'].map((linkedDataverse) => ({ + id: linkedDataverse.id, + alias: linkedDataverse.alias, + displayName: linkedDataverse.displayName + })) +} From 0d859698c61813f0d8c31b8ad649bc0e5619d8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 17 Jul 2025 13:14:37 -0300 Subject: [PATCH 027/123] test: add tests --- .../GetDatasetLinkedCollections.test.ts | 62 +++++++++++++++++++ test/functional/datasets/LinkDataset.test.ts | 2 +- .../datasets/DatasetsRepository.test.ts | 46 +++++++++++++- 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 test/functional/datasets/GetDatasetLinkedCollections.test.ts diff --git a/test/functional/datasets/GetDatasetLinkedCollections.test.ts b/test/functional/datasets/GetDatasetLinkedCollections.test.ts new file mode 100644 index 00000000..645506a6 --- /dev/null +++ b/test/functional/datasets/GetDatasetLinkedCollections.test.ts @@ -0,0 +1,62 @@ +import { + ApiConfig, + createDataset, + getDatasetLinkedCollections, + linkDataset, + WriteError +} from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { + createCollectionViaApi, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' +import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('execute', () => { + const testCollectionAlias = 'getDatasetLinkedCollectionsFunctionalTestCollection' + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + it('should return empty array when no collections are linked', async () => { + const createdDatasetIdentifiers = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO + ) + const linkedCollections = await getDatasetLinkedCollections.execute( + createdDatasetIdentifiers.numericId + ) + expect(linkedCollections.length).toBe(0) + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + }) + + it('should return linked collections for a dataset', async () => { + const createdDatasetIdentifiers = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO + ) + await createCollectionViaApi(testCollectionAlias) + + await linkDataset.execute(createdDatasetIdentifiers.numericId, testCollectionAlias) + + const linkedCollections = await getDatasetLinkedCollections.execute( + createdDatasetIdentifiers.numericId + ) + expect(linkedCollections.length).toBe(1) + expect(linkedCollections[0].alias).toBe(testCollectionAlias) + + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + await deleteCollectionViaApi(testCollectionAlias) + }) + + it('should return error when dataset does not exist', async () => { + const nonExistentDatasetId = 99999 + + await expect(getDatasetLinkedCollections.execute(nonExistentDatasetId)).rejects.toBeInstanceOf( + WriteError + ) + }) +}) diff --git a/test/functional/datasets/LinkDataset.test.ts b/test/functional/datasets/LinkDataset.test.ts index 4171d6a1..2d514e4f 100644 --- a/test/functional/datasets/LinkDataset.test.ts +++ b/test/functional/datasets/LinkDataset.test.ts @@ -49,7 +49,7 @@ describe('execute', () => { it('should throw an error when trying to link a dataset that does not exist', async () => { await createCollectionViaApi(testCollectionAlias) - const nonExistentDatasetId = 'nonExistentDatasetId' + const nonExistentDatasetId = 999999 await expect( linkDataset.execute(nonExistentDatasetId, testCollectionAlias) ).rejects.toBeInstanceOf(WriteError) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 83566b7f..4cc25c68 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1408,7 +1408,8 @@ describe('DatasetsRepository', () => { expect(actual).toBeUndefined() - // TODO:ME - Once we get linked dataset collections use case assert that the collection exists + const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) + expect(linkedCollections[0].alias).toBe(testCollectionAlias) }) test('should return error when dataset does not exist', async () => { @@ -1438,11 +1439,16 @@ describe('DatasetsRepository', () => { test('should unlink a dataset from a collection', async () => { await sut.linkDataset(testDatasetIds.numericId, testCollectionAlias) + const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) + expect(linkedCollections[0].alias).toBe(testCollectionAlias) + const actual = await sut.unlinkDataset(testDatasetIds.numericId, testCollectionAlias) expect(actual).toBeUndefined() - - // TODO:ME - Once we get linked dataset collections use case assert that the collection exists + const updatedLinkedCollections = await sut.getDatasetLinkedCollections( + testDatasetIds.numericId + ) + expect(updatedLinkedCollections.length).toBe(0) }) test('should return error when dataset does not exist', async () => { @@ -1461,4 +1467,38 @@ describe('DatasetsRepository', () => { ).rejects.toThrow() }) }) + + describe('getDatasetLinkedCollections', () => { + let testDatasetIds: CreatedDatasetIdentifiers + const testCollectionAlias = 'testGetLinkedCollections' + + beforeAll(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await createCollectionViaApi(testCollectionAlias) + }) + + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + await deleteCollectionViaApi(testCollectionAlias) + }) + + test('should return empty array when no collections are linked', async () => { + const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) + + expect(linkedCollections.length).toBe(0) + }) + + test('should return linked collections for a dataset', async () => { + await sut.linkDataset(testDatasetIds.numericId, testCollectionAlias) + + const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) + + expect(linkedCollections.length).toBe(1) + expect(linkedCollections[0].alias).toBe(testCollectionAlias) + }) + + test('should return error when dataset does not exist', async () => { + await expect(sut.getDatasetLinkedCollections(nonExistentTestDatasetId)).rejects.toThrow() + }) + }) }) From 851f269dca2acdf249f36ae56ac760a470f10e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 17 Jul 2025 15:00:49 -0300 Subject: [PATCH 028/123] fix: replace WriteError with ReadError in GetDatasetLinkedCollections tests --- test/functional/datasets/GetDatasetLinkedCollections.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/datasets/GetDatasetLinkedCollections.test.ts b/test/functional/datasets/GetDatasetLinkedCollections.test.ts index 645506a6..564bedfe 100644 --- a/test/functional/datasets/GetDatasetLinkedCollections.test.ts +++ b/test/functional/datasets/GetDatasetLinkedCollections.test.ts @@ -3,7 +3,7 @@ import { createDataset, getDatasetLinkedCollections, linkDataset, - WriteError + ReadError } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' import { @@ -56,7 +56,7 @@ describe('execute', () => { const nonExistentDatasetId = 99999 await expect(getDatasetLinkedCollections.execute(nonExistentDatasetId)).rejects.toBeInstanceOf( - WriteError + ReadError ) }) }) From 252e08c09499db93abe0c4b247bf8a698bbe0487 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 17 Jul 2025 15:40:11 -0400 Subject: [PATCH 029/123] fix: naming of delete notification --- docs/useCases.md | 10 +++++----- .../repositories/INotificationsRepository.ts | 2 +- ...NotificationByUser.ts => DeleteNotification.ts} | 4 ++-- src/notifications/index.ts | 6 +++--- .../infra/repositories/NotificationsRepository.ts | 2 +- ...onByUser.test.ts => DeleteNotification.test.ts} | 11 +++-------- .../notifications/NotificationsRepository.test.ts | 10 ++++------ ...onByUser.test.ts => DeleteNotification.test.ts} | 14 ++++++-------- 8 files changed, 25 insertions(+), 34 deletions(-) rename src/notifications/domain/useCases/{DeleteNotificationByUser.ts => DeleteNotification.ts} (78%) rename test/functional/notifications/{DeleteNotificationByUser.test.ts => DeleteNotification.test.ts} (76%) rename test/unit/notifications/{DeleteNotificationByUser.test.ts => DeleteNotification.test.ts} (70%) diff --git a/docs/useCases.md b/docs/useCases.md index fa4f507e..9d7cf314 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -87,7 +87,7 @@ The different use cases currently available in the package are classified below, - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) - [Notifications](#Notifications) - [Get All Notifications by User](#get-all-notifications-by-user) - - [Delete Notification by User](#delete-notification-by-user) + - [Delete Notification](#delete-notification) ## Collections @@ -2017,24 +2017,24 @@ getAllNotificationsByUser.execute().then((notifications: Notification[]) => { _See [use case](../src/notifications/domain/useCases/GetAllNotificationsByUser.ts) implementation_. -#### Delete Notification by User +#### Delete Notification Deletes a specific notification for the current authenticated user by its ID. ##### Example call: ```typescript -import { deleteNotificationByUser } from '@iqss/dataverse-client-javascript' +import { deleteNotification } from '@iqss/dataverse-client-javascript' /* ... */ const notificationId = 123 -deleteNotificationByUser.execute(notificationId: number).then(() => { +deleteNotification.execute(notificationId: number).then(() => { /* ... */ }) /* ... */ ``` -_See [use case](../src/notifications/domain/useCases/DeleteNotificationByUser.ts) implementation_. +_See [use case](../src/notifications/domain/useCases/DeleteNotification.ts) implementation_. diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index c528122b..dd40d071 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -2,5 +2,5 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { getAllNotificationsByUser(): Promise - deleteNotificationByUser(notificationId: number): Promise + deleteNotification(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/DeleteNotificationByUser.ts b/src/notifications/domain/useCases/DeleteNotification.ts similarity index 78% rename from src/notifications/domain/useCases/DeleteNotificationByUser.ts rename to src/notifications/domain/useCases/DeleteNotification.ts index 2c98d43b..ed57fc0b 100644 --- a/src/notifications/domain/useCases/DeleteNotificationByUser.ts +++ b/src/notifications/domain/useCases/DeleteNotification.ts @@ -7,10 +7,10 @@ import { INotificationsRepository } from '../repositories/INotificationsReposito * @param notificationId - The ID of the notification to delete. * @returns {Promise} - A promise that resolves when the notification is deleted. */ -export class DeleteNotificationByUser implements UseCase { +export class DeleteNotification implements UseCase { constructor(private readonly notificationsRepository: INotificationsRepository) {} async execute(notificationId: number): Promise { - return this.notificationsRepository.deleteNotificationByUser(notificationId) + return this.notificationsRepository.deleteNotification(notificationId) } } diff --git a/src/notifications/index.ts b/src/notifications/index.ts index a246c783..72ae6e82 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -1,12 +1,12 @@ import { NotificationsRepository } from './infra/repositories/NotificationsRepository' import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' -import { DeleteNotificationByUser } from './domain/useCases/DeleteNotificationByUser' +import { DeleteNotification } from './domain/useCases/DeleteNotification' const notificationsRepository = new NotificationsRepository() const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) -const deleteNotificationByUser = new DeleteNotificationByUser(notificationsRepository) +const deleteNotification = new DeleteNotification(notificationsRepository) -export { getAllNotificationsByUser, deleteNotificationByUser } +export { getAllNotificationsByUser, deleteNotification } export { Notification } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 341f8526..7cd608b9 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -13,7 +13,7 @@ export class NotificationsRepository extends ApiRepository implements INotificat }) } - public async deleteNotificationByUser(notificationId: number): Promise { + public async deleteNotification(notificationId: number): Promise { return this.doDelete( this.buildApiEndpoint(this.notificationsResourceName, notificationId.toString()) ) diff --git a/test/functional/notifications/DeleteNotificationByUser.test.ts b/test/functional/notifications/DeleteNotification.test.ts similarity index 76% rename from test/functional/notifications/DeleteNotificationByUser.test.ts rename to test/functional/notifications/DeleteNotification.test.ts index 93ff971b..093fa637 100644 --- a/test/functional/notifications/DeleteNotificationByUser.test.ts +++ b/test/functional/notifications/DeleteNotification.test.ts @@ -1,9 +1,4 @@ -import { - ApiConfig, - deleteNotificationByUser, - getAllNotificationsByUser, - WriteError -} from '../../../src' +import { ApiConfig, deleteNotification, getAllNotificationsByUser, WriteError } from '../../../src' import { TestConstants } from '../../testHelpers/TestConstants' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -20,13 +15,13 @@ describe('execute', () => { const notifications = await getAllNotificationsByUser.execute() const notificationId = notifications[notifications.length - 1].id - await deleteNotificationByUser.execute(notificationId) + await deleteNotification.execute(notificationId) const notificationsAfterDelete = await getAllNotificationsByUser.execute() expect(notificationsAfterDelete.length).toBe(notifications.length - 1) }) test('should throw an error when the notification id does not exist', async () => { - await expect(deleteNotificationByUser.execute(123)).rejects.toThrow(WriteError) + await expect(deleteNotification.execute(123)).rejects.toThrow(WriteError) }) }) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index b5b026ae..df619136 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -55,7 +55,7 @@ describe('NotificationsRepository', () => { const notificationToDelete = notifications[0] - await sut.deleteNotificationByUser(notificationToDelete.id) + await sut.deleteNotification(notificationToDelete.id) const notificationsAfterDelete: Notification[] = await sut.getAllNotificationsByUser() const deletedNotification = notificationsAfterDelete.find( @@ -65,14 +65,12 @@ describe('NotificationsRepository', () => { }) test('should throw error when trying to delete notification with wrong ID', async () => { - const nonExistentMetadataBlockName = 99999 + const nonExistentNotificationId = 99999 const expectedError = new WriteError( - `[404] Notification ${nonExistentMetadataBlockName} not found.` + `[404] Notification ${nonExistentNotificationId} not found.` ) - await expect(sut.deleteNotificationByUser(nonExistentMetadataBlockName)).rejects.toThrow( - expectedError - ) + await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) }) diff --git a/test/unit/notifications/DeleteNotificationByUser.test.ts b/test/unit/notifications/DeleteNotification.test.ts similarity index 70% rename from test/unit/notifications/DeleteNotificationByUser.test.ts rename to test/unit/notifications/DeleteNotification.test.ts index e8f064c3..4ad62444 100644 --- a/test/unit/notifications/DeleteNotificationByUser.test.ts +++ b/test/unit/notifications/DeleteNotification.test.ts @@ -1,4 +1,4 @@ -import { DeleteNotificationByUser } from '../../../src/notifications/domain/useCases/DeleteNotificationByUser' +import { DeleteNotification } from '../../../src/notifications/domain/useCases/DeleteNotification' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' import { Notification } from '../../../src/notifications/domain/models/Notification' @@ -23,23 +23,21 @@ describe('execute', () => { test('should delete notification from repository', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) - notificationsRepositoryStub.deleteNotificationByUser = jest - .fn() - .mockResolvedValue(mockNotifications) - const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + notificationsRepositoryStub.deleteNotification = jest.fn().mockResolvedValue(mockNotifications) + const sut = new DeleteNotification(notificationsRepositoryStub) await sut.execute(123) - expect(notificationsRepositoryStub.deleteNotificationByUser).toHaveBeenCalledWith(123) + expect(notificationsRepositoryStub.deleteNotification).toHaveBeenCalledWith(123) }) test('should throw error when repository throws error', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) - notificationsRepositoryStub.deleteNotificationByUser = jest + notificationsRepositoryStub.deleteNotification = jest .fn() .mockRejectedValue(new Error('Repository error')) - const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + const sut = new DeleteNotification(notificationsRepositoryStub) await expect(sut.execute(123)).rejects.toThrow('Repository error') }) From b9604c61b432a6918733bba89e9476c675964f8e Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 22 Jul 2025 16:16:14 -0400 Subject: [PATCH 030/123] feat: update get citation in other format --- docs/useCases.md | 31 ++++++ src/datasets/domain/models/CitationFormats.ts | 7 ++ .../domain/models/CitationResponse.ts | 7 ++ .../repositories/IDatasetsRepository.ts | 8 ++ .../GetDatasetCitationInOtherFormats.ts | 36 ++++++ .../infra/repositories/DatasetsRepository.ts | 30 +++++ .../datasets/DatasetsRepository.test.ts | 103 ++++++++++++++++++ .../GetDatasetCitationInOtherFormats.test.ts | 39 +++++++ 8 files changed, 261 insertions(+) create mode 100644 src/datasets/domain/models/CitationFormats.ts create mode 100644 src/datasets/domain/models/CitationResponse.ts create mode 100644 src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts create mode 100644 test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 372704a8..b1a00c6c 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -564,6 +564,37 @@ The `datasetId` parameter can be a string, for persistent identifiers, or a numb There is an optional third parameter called `includeDeaccessioned`, which indicates whether to consider deaccessioned versions or not in the dataset search. If not set, the default value is `false`. +#### Get Dataset Citation In Other Formats + +Retrieves the citation for a dataset in a specified bibliographic format. + +##### Example call: + +```typescript +import { getDatasetCitationInOtherFormats } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 2 +const datasetVersionId = '1.0' + +getDatasetCitationInOtherFormats + .execute(datasetId, datasetVersionId, format) + .then((citationText: CitationResponse) => { + /* ... */ + }) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts) implementation_. + +Supported formats include 'EndNote' (XML), 'RIS' (plain text), 'BibTeX' (plain text), 'CSLJson' (JSON), and 'Internal' (HTML). The response contains the raw citation content in the requested format, the format type, and the content type (MIME type). + +The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. + +There is an optional third parameter called `includeDeaccessioned`, which indicates whether to consider deaccessioned versions or not in the dataset search. If not set, the default value is `false`. + #### Get Dataset Citation Text By Private URL Token Returns the Dataset citation text, given an associated Private URL Token. diff --git a/src/datasets/domain/models/CitationFormats.ts b/src/datasets/domain/models/CitationFormats.ts new file mode 100644 index 00000000..8fde9898 --- /dev/null +++ b/src/datasets/domain/models/CitationFormats.ts @@ -0,0 +1,7 @@ +export enum CitationFormats { + Internal = 'Internal', + EndNote = 'EndNote', + RIS = 'RIS', + BibTeX = 'BibTeX', + CSLJson = 'CSL' +} diff --git a/src/datasets/domain/models/CitationResponse.ts b/src/datasets/domain/models/CitationResponse.ts new file mode 100644 index 00000000..f30349b9 --- /dev/null +++ b/src/datasets/domain/models/CitationResponse.ts @@ -0,0 +1,7 @@ +import { CitationFormats } from './CitationFormats' + +export type CitationResponse = { + content: string + format: CitationFormats + contentType: string +} diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 52a6c1cd..4096be44 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -10,6 +10,8 @@ import { DatasetVersionDiff } from '../models/DatasetVersionDiff' import { DatasetDownloadCount } from '../models/DatasetDownloadCount' import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' +import { CitationFormats } from '../models/CitationFormats' +import { CitationResponse } from '../models/CitationResponse' export interface IDatasetsRepository { getDataset( @@ -65,4 +67,10 @@ export interface IDatasetsRepository { linkDataset(datasetId: number, collectionAlias: string): Promise unlinkDataset(datasetId: number, collectionAlias: string): Promise getDatasetLinkedCollections(datasetId: number | string): Promise + getDatasetCitationInOtherFormats( + datasetId: number, + datasetVersionId: string, + format: CitationFormats, + includeDeaccessioned?: boolean + ): Promise } diff --git a/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts b/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts new file mode 100644 index 00000000..0f470043 --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts @@ -0,0 +1,36 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion' +import { CitationResponse } from '../models/CitationResponse' +import { CitationFormats } from '../models/CitationFormats' + +export class GetDatasetCitationInOtherFormats implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns the dataset citation in the specified format. + * + * @param {number} datasetId - The dataset identifier. + * @param {string | DatasetNotNumberedVersion} [datasetVersionId=DatasetNotNumberedVersion.LATEST] - The dataset version identifier, which can be a version-specific string (e.g., '1.0') or a DatasetNotNumberedVersion enum value. Defaults to LATEST. + * @param {CitationFormats} format - The citation format to return. One of: 'EndNote', 'RIS', 'BibTeX', 'CSLJson', 'Internal'. + * @param {boolean} [includeDeaccessioned=false] - Whether to include deaccessioned versions in the search. Defaults to false. + * @returns {Promise} The citation content, format, and content type. + */ + async execute( + datasetId: number, + datasetVersionId: string | DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST, + format: CitationFormats, + includeDeaccessioned = false + ): Promise { + return await this.datasetsRepository.getDatasetCitationInOtherFormats( + datasetId, + datasetVersionId, + format, + includeDeaccessioned + ) + } +} diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 95e82b77..ec311008 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -21,7 +21,9 @@ import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './trans import { DatasetDownloadCount } from '../../domain/models/DatasetDownloadCount' import { DatasetVersionSummaryInfo } from '../../domain/models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollection' +import { CitationFormats } from '../../domain/models/CitationFormats' import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers' +import { CitationResponse } from '../../domain/models/CitationResponse' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -95,6 +97,34 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } + public async getDatasetCitationInOtherFormats( + datasetId: number, + datasetVersionId: string | 'LATEST' = 'LATEST', + format: CitationFormats, + includeDeaccessioned = false + ): Promise { + const endpoint = this.buildApiEndpoint( + this.datasetsResourceName, + `versions/${datasetVersionId}/citation/${format}`, + datasetId + ) + + const queryParams = { includeDeaccessioned } + + try { + const response = await this.doGet(endpoint, true, queryParams) + + return { + content: response.data, + format, + contentType: response.headers['content-type'] ?? 'text/plain' + } + } catch (error) { + console.error(`[DatasetsRepository] Error fetching citation:`, error) + throw error + } + } + public async getPrivateUrlDatasetCitation(token: string): Promise { return this.doGet( this.buildApiEndpoint(this.datasetsResourceName, `privateUrlDatasetVersion/${token}/citation`) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 4cc25c68..5c7d97ce 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -51,6 +51,7 @@ import { import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepository' import { DirectUploadClient } from '../../../src/files/infra/clients/DirectUploadClient' import { createTestFileUploadDestination } from '../../testHelpers/files/fileUploadDestinationHelper' +import { CitationFormats } from '../../../src/datasets/domain/models/CitationFormats' const TEST_DIFF_DATASET_DTO: DatasetDTO = { license: { @@ -492,6 +493,108 @@ describe('DatasetsRepository', () => { }) }) + describe('getDatasetCitationInOtherFormats', () => { + let testDatasetIds: CreatedDatasetIdentifiers + + beforeAll(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + }) + + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + }) + + test('should return citation in BibTeX format', async () => { + const citation = await sut.getDatasetCitationInOtherFormats( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + CitationFormats.BibTeX + ) + + expect(typeof citation.content).toBe('string') + expect(citation.format).toBe(CitationFormats.BibTeX) + expect(citation.contentType).toMatch(/text\/plain/) + }) + + test('should return citation in RIS format', async () => { + const citation = await sut.getDatasetCitationInOtherFormats( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + CitationFormats.RIS + ) + + expect(typeof citation.content).toBe('string') + expect(citation.format).toBe(CitationFormats.RIS) + expect(citation.contentType).toMatch(/text\/plain/) + }) + + test('should return citation in CSLJson format', async () => { + const citation = await sut.getDatasetCitationInOtherFormats( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + CitationFormats.CSLJson + ) + + expect(typeof citation.content).toBe('object') + expect(citation.format).toBe(CitationFormats.CSLJson) + expect(citation.contentType).toMatch(/application\/json/) + }) + + test('should return citation in EndNote format', async () => { + const citation = await sut.getDatasetCitationInOtherFormats( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + CitationFormats.EndNote + ) + + expect(typeof citation.content).toBe('string') + expect(citation.format).toBe(CitationFormats.EndNote) + expect(citation.contentType).toMatch('text/xml;charset=UTF-8') + }) + + test('should return citation in Internal format', async () => { + const citation = await sut.getDatasetCitationInOtherFormats( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + CitationFormats.Internal + ) + + expect(typeof citation.content).toBe('string') + expect(citation.format).toBe(CitationFormats.Internal) + expect(citation.contentType).toMatch(/text\/html/) + }) + + test('should return error when dataset does not exist', async () => { + const nonExistentId = 9999999 + const expectedError = new ReadError(`[404] Dataset with ID ${nonExistentId} not found.`) + + await expect( + sut.getDatasetCitationInOtherFormats( + nonExistentId, + DatasetNotNumberedVersion.LATEST, + CitationFormats.RIS + ) + ).rejects.toThrow(expectedError) + }) + + test('should return citation for deaccessioned dataset when includeDeaccessioned = true', async () => { + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + await deaccessionDatasetViaApi(testDatasetIds.numericId, '1.0') + + const citation = await sut.getDatasetCitationInOtherFormats( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + CitationFormats.RIS, + true + ) + + expect(typeof citation.content).toBe('string') + expect(citation.format).toBe(CitationFormats.RIS) + expect(citation.contentType).toMatch(/text\/plain/) + }) + }) + describe('getDatasetVersionDiff', () => { let testDatasetIds: CreatedDatasetIdentifiers diff --git a/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts b/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts new file mode 100644 index 00000000..0e4d15cc --- /dev/null +++ b/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts @@ -0,0 +1,39 @@ +import { GetDatasetCitationInOtherFormats } from '../../../src/datasets/domain/useCases/GetDatasetCitationInOtherFormats' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { ReadError } from '../../../src/core/domain/repositories/ReadError' +import { CitationFormats } from '../../../src/datasets/domain/models/CitationFormats' +import { DatasetNotNumberedVersion } from '../../../src/datasets/domain/models/DatasetNotNumberedVersion' +import { CitationResponse } from '../../../src/datasets/domain/models/CitationResponse' + +describe('GetDatasetCitationInOtherFormats.execute', () => { + const testDatasetId = 1 + const testFormat: CitationFormats = CitationFormats.BibTeX + const testVersion: DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST + + test('should return citation response on repository success', async () => { + const expectedCitation: CitationResponse = { + content: '@data{example, ...}', + format: CitationFormats.BibTeX, + contentType: 'text/plain' + } + + const datasetsRepositoryStub: IDatasetsRepository = { + getDatasetCitationInOtherFormats: jest.fn().mockResolvedValue(expectedCitation) + } as unknown as IDatasetsRepository + + const sut = new GetDatasetCitationInOtherFormats(datasetsRepositoryStub) + + const actual = await sut.execute(testDatasetId, testVersion, testFormat as CitationFormats) + expect(actual).toEqual(expectedCitation) + }) + + test('should throw ReadError on repository failure', async () => { + const datasetsRepositoryStub: IDatasetsRepository = { + getDatasetCitationInOtherFormats: jest.fn().mockRejectedValue(new ReadError()) + } as unknown as IDatasetsRepository + + const sut = new GetDatasetCitationInOtherFormats(datasetsRepositoryStub) + + await expect(sut.execute(testDatasetId, testVersion, testFormat)).rejects.toThrow(ReadError) + }) +}) From 76784603dbf198eca8b0379ee05ccfc5073b197f Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 23 Jul 2025 15:01:30 -0400 Subject: [PATCH 031/123] fix: update the response format --- src/datasets/domain/models/CitationResponse.ts | 5 +---- .../infra/repositories/DatasetsRepository.ts | 17 ++++------------- .../datasets/DatasetsRepository.test.ts | 6 ------ .../GetDatasetCitationInOtherFormats.test.ts | 1 - 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/datasets/domain/models/CitationResponse.ts b/src/datasets/domain/models/CitationResponse.ts index f30349b9..8bb8f2da 100644 --- a/src/datasets/domain/models/CitationResponse.ts +++ b/src/datasets/domain/models/CitationResponse.ts @@ -1,7 +1,4 @@ -import { CitationFormats } from './CitationFormats' - export type CitationResponse = { - content: string - format: CitationFormats + content: string | object contentType: string } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index ec311008..117d797f 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -108,20 +108,11 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi `versions/${datasetVersionId}/citation/${format}`, datasetId ) + const response = await this.doGet(endpoint, true, { includeDeaccessioned }) - const queryParams = { includeDeaccessioned } - - try { - const response = await this.doGet(endpoint, true, queryParams) - - return { - content: response.data, - format, - contentType: response.headers['content-type'] ?? 'text/plain' - } - } catch (error) { - console.error(`[DatasetsRepository] Error fetching citation:`, error) - throw error + return { + content: response.data, + contentType: response.headers['content-type'] } } diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 5c7d97ce..bf336ec1 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -512,7 +512,6 @@ describe('DatasetsRepository', () => { ) expect(typeof citation.content).toBe('string') - expect(citation.format).toBe(CitationFormats.BibTeX) expect(citation.contentType).toMatch(/text\/plain/) }) @@ -524,7 +523,6 @@ describe('DatasetsRepository', () => { ) expect(typeof citation.content).toBe('string') - expect(citation.format).toBe(CitationFormats.RIS) expect(citation.contentType).toMatch(/text\/plain/) }) @@ -536,7 +534,6 @@ describe('DatasetsRepository', () => { ) expect(typeof citation.content).toBe('object') - expect(citation.format).toBe(CitationFormats.CSLJson) expect(citation.contentType).toMatch(/application\/json/) }) @@ -548,7 +545,6 @@ describe('DatasetsRepository', () => { ) expect(typeof citation.content).toBe('string') - expect(citation.format).toBe(CitationFormats.EndNote) expect(citation.contentType).toMatch('text/xml;charset=UTF-8') }) @@ -560,7 +556,6 @@ describe('DatasetsRepository', () => { ) expect(typeof citation.content).toBe('string') - expect(citation.format).toBe(CitationFormats.Internal) expect(citation.contentType).toMatch(/text\/html/) }) @@ -590,7 +585,6 @@ describe('DatasetsRepository', () => { ) expect(typeof citation.content).toBe('string') - expect(citation.format).toBe(CitationFormats.RIS) expect(citation.contentType).toMatch(/text\/plain/) }) }) diff --git a/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts b/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts index 0e4d15cc..7e13e029 100644 --- a/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts +++ b/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts @@ -13,7 +13,6 @@ describe('GetDatasetCitationInOtherFormats.execute', () => { test('should return citation response on repository success', async () => { const expectedCitation: CitationResponse = { content: '@data{example, ...}', - format: CitationFormats.BibTeX, contentType: 'text/plain' } From b464cecc27b575b0d1161e889cfae47e3f56b1f3 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 23 Jul 2025 16:29:26 -0400 Subject: [PATCH 032/123] revert the dataverse image to docker.io, unstable --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index 5b641de0..e7b54bde 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=11614-include-isAdvancedSearchField-property +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m From d492fea846751ee1cdfcb43e9158ed4bbe9baa5a Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 23 Jul 2025 16:42:07 -0400 Subject: [PATCH 033/123] fix RolesRepository integration test --- test/testHelpers/roles/roleHelper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index cf48cc3c..3e641cda 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -42,7 +42,9 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataverse', + 'LinkDataverse', 'PublishDataset', + 'LinkDataset', 'DeleteDataverse', 'DeleteDatasetDraft' ], @@ -99,10 +101,11 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataset', + 'LinkDataset', 'DeleteDatasetDraft' ], description: - 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.', + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.', id: 7 }, { From eb9ffc3ade7f6489f6848c8c1f8527352542da79 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 23 Jul 2025 17:58:10 -0400 Subject: [PATCH 034/123] skip failing test until fix is in place --- test/integration/datasets/DatasetsRepository.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 4cc25c68..02144c15 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -954,8 +954,8 @@ describe('DatasetsRepository', () => { } ]) }) - - test('should throw error if trying to update an outdated internal version dataset', async () => { + // TODO: add this test when https://github.com/IQSS/dataverse-client-javascript/issues/343 is fixed + test.skip('should throw error if trying to update an outdated internal version dataset', async () => { const testDataset = { metadataBlockValues: [ { From 09282005e88b4956ca295cbdaa5721cd350059ad Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 24 Jul 2025 16:16:13 -0400 Subject: [PATCH 035/123] feat: update use case get available categories --- docs/useCases.md | 23 ++++++++++++ .../repositories/IDatasetsRepository.ts | 1 + .../useCases/GetDatasetAvailableCategories.ts | 20 ++++++++++ src/datasets/index.ts | 5 ++- .../infra/repositories/DatasetsRepository.ts | 11 ++++++ .../domain/useCases/UpdateFileCategories.ts | 2 +- .../domain/useCases/UpdateFileTabularTags.ts | 2 +- .../GetDatasetAvailableCategories.test.ts | 37 +++++++++++++++++++ .../datasets/DatasetsRepository.test.ts | 28 ++++++++++++++ 9 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/datasets/domain/useCases/GetDatasetAvailableCategories.ts create mode 100644 test/functional/datasets/GetDatasetAvailableCategories.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 372704a8..b0f90d55 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -37,6 +37,7 @@ The different use cases currently available in the package are classified below, - [List All Datasets](#list-all-datasets) - [Get Dataset Versions Summaries](#get-dataset-versions-summaries) - [Get Dataset Linked Collections](#get-dataset-linked-collections) + - [Get Dataset Available Categories](#get-dataset-available-categories) - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) @@ -1049,6 +1050,28 @@ The `includeMDC` parameter is optional. - If MDC isn't enabled, the download count will return a total count, without `MDCStartDate`. - If MDC is enabled but the `includeMDC` is false, the count will be limited to the time before `MDCStartDate` +#### Get Dataset Available Categories + +Returns a list of available file categories that may be applied to the files of a given dataset. + +###### Example call: + +```typescript +import { getDatasetAvailableCategories } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 + +getDatasetAvailableCategories.execute(datasetId).then((categories: String[]) => { + /* ... */ +}) +``` + +_See [use case](../src/files/domain/useCases/GetDatasetAvailableCategories.ts) implementation_. + +The `datasetId` parameter is a number for numeric identifiers or string for persistent identifiers. + ## Files ### Files read use cases diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 52a6c1cd..6e55b297 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -65,4 +65,5 @@ export interface IDatasetsRepository { linkDataset(datasetId: number, collectionAlias: string): Promise unlinkDataset(datasetId: number, collectionAlias: string): Promise getDatasetLinkedCollections(datasetId: number | string): Promise + getDatasetAvailableCategories(datasetId: number | string): Promise } diff --git a/src/datasets/domain/useCases/GetDatasetAvailableCategories.ts b/src/datasets/domain/useCases/GetDatasetAvailableCategories.ts new file mode 100644 index 00000000..e51544d6 --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetAvailableCategories.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class GetDatasetAvailableCategories implements UseCase { + private readonly datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Retrieves the available file categories for a dataset. + * + * @param {number | string} [datasetId] - Persistent dataset identifier + * @returns {Promise} - List of available file categories + */ + async execute(datasetId: number | string): Promise { + return this.datasetsRepository.getDatasetAvailableCategories(datasetId) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index a7a7a14b..17426cf3 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -23,6 +23,7 @@ import { DeleteDatasetDraft } from './domain/useCases/DeleteDatasetDraft' import { LinkDataset } from './domain/useCases/LinkDataset' import { UnlinkDataset } from './domain/useCases/UnlinkDataset' import { GetDatasetLinkedCollections } from './domain/useCases/GetDatasetLinkedCollections' +import { GetDatasetAvailableCategories } from './domain/useCases/GetDatasetAvailableCategories' const datasetsRepository = new DatasetsRepository() @@ -60,6 +61,7 @@ const deleteDatasetDraft = new DeleteDatasetDraft(datasetsRepository) const linkDataset = new LinkDataset(datasetsRepository) const unlinkDataset = new UnlinkDataset(datasetsRepository) const getDatasetLinkedCollections = new GetDatasetLinkedCollections(datasetsRepository) +const getDatasetAvailableCategories = new GetDatasetAvailableCategories(datasetsRepository) export { getDataset, @@ -80,7 +82,8 @@ export { deleteDatasetDraft, linkDataset, unlinkDataset, - getDatasetLinkedCollections + getDatasetLinkedCollections, + getDatasetAvailableCategories } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 95e82b77..2464a74c 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -317,4 +317,15 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async getDatasetAvailableCategories(datasetId: number | string): Promise { + return this.doGet( + this.buildApiEndpoint(this.datasetsResourceName, 'availableFileCategories', datasetId), + true + ) + .then((response) => response.data.data as string[]) + .catch((error) => { + throw error + }) + } } diff --git a/src/files/domain/useCases/UpdateFileCategories.ts b/src/files/domain/useCases/UpdateFileCategories.ts index ea568073..fc2e496d 100644 --- a/src/files/domain/useCases/UpdateFileCategories.ts +++ b/src/files/domain/useCases/UpdateFileCategories.ts @@ -10,7 +10,7 @@ export class UpdateFileCategories implements UseCase { /** * Updates the categories for a particular File. - * More detailed information about updating a file's categories behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#updating-file-metadata + * More detailed information about updating a file's categories behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#updating-file-metadata-categories * * @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {string[]} [categories] - The categories to be added to the file. diff --git a/src/files/domain/useCases/UpdateFileTabularTags.ts b/src/files/domain/useCases/UpdateFileTabularTags.ts index 3b777a97..2f6eb543 100644 --- a/src/files/domain/useCases/UpdateFileTabularTags.ts +++ b/src/files/domain/useCases/UpdateFileTabularTags.ts @@ -10,7 +10,7 @@ export class UpdateFileTabularTags implements UseCase { /** * Updates the tabular tabular Tags for a particular File. - * More detailed information about updating a file's tabularTags behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#updating-file-metadata + * More detailed information about updating a file's tabularTags behavior can be found in https://guides.dataverse.org/en/latest/api/native-api.html#updating-file-tabular-tags * * @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {string[]} [tabularTags] - The tabular tags to be added to the file. diff --git a/test/functional/datasets/GetDatasetAvailableCategories.test.ts b/test/functional/datasets/GetDatasetAvailableCategories.test.ts new file mode 100644 index 00000000..39c8cef2 --- /dev/null +++ b/test/functional/datasets/GetDatasetAvailableCategories.test.ts @@ -0,0 +1,37 @@ +import { ApiConfig, createDataset, getDatasetAvailableCategories, ReadError } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' +import { CreatedDatasetIdentifiers } from '../../../src/datasets/domain/models/CreatedDatasetIdentifiers' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('execute', () => { + let createdDatasetIdentifiers: CreatedDatasetIdentifiers + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + createdDatasetIdentifiers = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + }) + + afterEach(async () => { + deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + }) + + it('should return categories array when a dataset has files categories', async () => { + const defaultCategories = ['Code', 'Data', 'Documentation'] + const categoriesList = await getDatasetAvailableCategories.execute( + createdDatasetIdentifiers.numericId + ) + expect(categoriesList.sort()).toEqual(defaultCategories.sort()) + }) + + it('should return error when dataset does not exist', async () => { + const nonExistentDatasetId = 99999 + + await expect( + getDatasetAvailableCategories.execute(nonExistentDatasetId) + ).rejects.toBeInstanceOf(ReadError) + }) +}) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 02144c15..3ef98d9e 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1501,4 +1501,32 @@ describe('DatasetsRepository', () => { await expect(sut.getDatasetLinkedCollections(nonExistentTestDatasetId)).rejects.toThrow() }) }) + + describe('getDatasetAvailableCategories', () => { + let testDatasetIds: CreatedDatasetIdentifiers + + beforeEach(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + // Dataset is in draft, so we need to publish it first + await sut.publishDataset(testDatasetIds.numericId, VersionUpdateType.MAJOR) + await waitForNoLocks(testDatasetIds.numericId, 10) + }) + + test('should get available categories', async () => { + const fileMetadata = { + description: 'test description', + directoryLabel: 'directoryLabel', + categories: ['category1', 'category2', 'Documentation', 'Data', 'Code'] + } + + await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name, fileMetadata) + + const actual = await sut.getDatasetAvailableCategories(testDatasetIds.numericId) + expect(actual.sort()).toEqual(fileMetadata.categories.sort()) + }) + + test('should return error when dataset does not exist', async () => { + await expect(sut.getDatasetAvailableCategories(nonExistentTestDatasetId)).rejects.toThrow() + }) + }) }) From 7ce1c2fb3ddf9328a9ff19a61601235f73332938 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 24 Jul 2025 16:19:43 -0400 Subject: [PATCH 036/123] feat: update useCases.md --- docs/useCases.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/useCases.md b/docs/useCases.md index b0f90d55..0df4b5c5 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -65,6 +65,8 @@ The different use cases currently available in the package are classified below, - [Replace a File](#replace-a-file) - [Restrict or Unrestrict a File](#restrict-or-unrestrict-a-file) - [Update File Metadata](#update-file-metadata) + - [Update File Categories](#update-file-categories) + - [Update File Tabular Tags](#update-file-tabular-tags) - [Metadata Blocks](#metadata-blocks) - [Metadata Blocks read use cases](#metadata-blocks-read-use-cases) - [Get All Facetable Metadata Fields](#get-all-facetable-metadata-fields) From 997a4fb0e3386c8297db36d018a2bf8331fc4479 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 25 Jul 2025 10:23:34 -0400 Subject: [PATCH 037/123] Revert "fix RolesRepository integration test" This reverts commit d492fea846751ee1cdfcb43e9158ed4bbe9baa5a. --- test/testHelpers/roles/roleHelper.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index 3e641cda..cf48cc3c 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -42,9 +42,7 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataverse', - 'LinkDataverse', 'PublishDataset', - 'LinkDataset', 'DeleteDataverse', 'DeleteDatasetDraft' ], @@ -101,11 +99,10 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataset', - 'LinkDataset', 'DeleteDatasetDraft' ], description: - 'For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.', + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.', id: 7 }, { From 329831e2f5d3f837dd85ccc59df1f4965efd020e Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 25 Jul 2025 10:36:54 -0400 Subject: [PATCH 038/123] Revert "fix RolesRepository integration test" This reverts commit d492fea846751ee1cdfcb43e9158ed4bbe9baa5a. --- test/testHelpers/roles/roleHelper.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index 3e641cda..cf48cc3c 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -42,9 +42,7 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataverse', - 'LinkDataverse', 'PublishDataset', - 'LinkDataset', 'DeleteDataverse', 'DeleteDatasetDraft' ], @@ -101,11 +99,10 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataset', - 'LinkDataset', 'DeleteDatasetDraft' ], description: - 'For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.', + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.', id: 7 }, { From 273947cecde6aacad924191351d1007695eb1778 Mon Sep 17 00:00:00 2001 From: Cheng Shi <91049239+ChengShi-1@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:18:17 -0400 Subject: [PATCH 039/123] Update docs/useCases.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/useCases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/useCases.md b/docs/useCases.md index 0df4b5c5..3a6b544b 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -1070,7 +1070,7 @@ getDatasetAvailableCategories.execute(datasetId).then((categories: String[]) => }) ``` -_See [use case](../src/files/domain/useCases/GetDatasetAvailableCategories.ts) implementation_. +_See [use case](../src/datasets/domain/useCases/GetDatasetAvailableCategories.ts) implementation_. The `datasetId` parameter is a number for numeric identifiers or string for persistent identifiers. From a3abce006eeb03998f10f2f5d2938b0202bc255f Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 25 Jul 2025 11:30:18 -0400 Subject: [PATCH 040/123] fix: test --- test/integration/datasets/DatasetsRepository.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 17ae2f89..11a9295d 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -545,7 +545,7 @@ describe('DatasetsRepository', () => { ) expect(typeof citation.content).toBe('string') - expect(citation.contentType).toMatch('text/xml;charset=UTF-8') + expect(citation.contentType).toMatch(/text\/xml/) }) test('should return citation in Internal format', async () => { From 136e12591adacfa33adf17e32ac250dc9f84d995 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 25 Jul 2025 11:32:58 -0400 Subject: [PATCH 041/123] fix: test --- test/integration/datasets/DatasetsRepository.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 3ef98d9e..961ae6a2 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1505,11 +1505,12 @@ describe('DatasetsRepository', () => { describe('getDatasetAvailableCategories', () => { let testDatasetIds: CreatedDatasetIdentifiers - beforeEach(async () => { + beforeAll(async () => { testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) - // Dataset is in draft, so we need to publish it first - await sut.publishDataset(testDatasetIds.numericId, VersionUpdateType.MAJOR) - await waitForNoLocks(testDatasetIds.numericId, 10) + }) + + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }) test('should get available categories', async () => { From 5fd4982a9972090e6088fb76b983ffa18002aa60 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 31 Jul 2025 10:25:19 -0400 Subject: [PATCH 042/123] feat: add test case for persistent id --- test/integration/datasets/DatasetsRepository.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 961ae6a2..1b86866e 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1526,6 +1526,17 @@ describe('DatasetsRepository', () => { expect(actual.sort()).toEqual(fileMetadata.categories.sort()) }) + test('should get available categorie if dataset id is persistent id', async () => { + const fileMetadata = { + description: 'test description', + directoryLabel: 'directoryLabel', + categories: ['category1', 'category2', 'Documentation', 'Data', 'Code'] + } + + const actual = await sut.getDatasetAvailableCategories(testDatasetIds.persistentId) + expect(actual.sort()).toEqual(fileMetadata.categories.sort()) + }) + test('should return error when dataset does not exist', async () => { await expect(sut.getDatasetAvailableCategories(nonExistentTestDatasetId)).rejects.toThrow() }) From e9c4f98c1f7f57c94bec4e54b6f97abd9c11a4c9 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 31 Jul 2025 13:52:58 -0400 Subject: [PATCH 043/123] fix: change the names and stringify json object --- docs/useCases.md | 2 +- .../{CitationFormats.ts => CitationFormat.ts} | 2 +- .../domain/models/CitationResponse.ts | 4 ---- .../domain/models/FormattedCitation.ts | 4 ++++ .../repositories/IDatasetsRepository.ts | 8 ++++---- .../GetDatasetCitationInOtherFormats.ts | 14 ++++++------- .../infra/repositories/DatasetsRepository.ts | 20 +++++++++++++------ .../datasets/DatasetsRepository.test.ts | 18 ++++++++--------- .../GetDatasetCitationInOtherFormats.test.ts | 10 +++++----- 9 files changed, 45 insertions(+), 37 deletions(-) rename src/datasets/domain/models/{CitationFormats.ts => CitationFormat.ts} (77%) delete mode 100644 src/datasets/domain/models/CitationResponse.ts create mode 100644 src/datasets/domain/models/FormattedCitation.ts diff --git a/docs/useCases.md b/docs/useCases.md index b1a00c6c..44b25e02 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -580,7 +580,7 @@ const datasetVersionId = '1.0' getDatasetCitationInOtherFormats .execute(datasetId, datasetVersionId, format) - .then((citationText: CitationResponse) => { + .then((citationText: FormattedCitation) => { /* ... */ }) diff --git a/src/datasets/domain/models/CitationFormats.ts b/src/datasets/domain/models/CitationFormat.ts similarity index 77% rename from src/datasets/domain/models/CitationFormats.ts rename to src/datasets/domain/models/CitationFormat.ts index 8fde9898..56d923d6 100644 --- a/src/datasets/domain/models/CitationFormats.ts +++ b/src/datasets/domain/models/CitationFormat.ts @@ -1,4 +1,4 @@ -export enum CitationFormats { +export enum CitationFormat { Internal = 'Internal', EndNote = 'EndNote', RIS = 'RIS', diff --git a/src/datasets/domain/models/CitationResponse.ts b/src/datasets/domain/models/CitationResponse.ts deleted file mode 100644 index 8bb8f2da..00000000 --- a/src/datasets/domain/models/CitationResponse.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type CitationResponse = { - content: string | object - contentType: string -} diff --git a/src/datasets/domain/models/FormattedCitation.ts b/src/datasets/domain/models/FormattedCitation.ts new file mode 100644 index 00000000..1057db64 --- /dev/null +++ b/src/datasets/domain/models/FormattedCitation.ts @@ -0,0 +1,4 @@ +export type FormattedCitation = { + content: string + contentType: string +} diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 4096be44..72e66fd4 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -10,8 +10,8 @@ import { DatasetVersionDiff } from '../models/DatasetVersionDiff' import { DatasetDownloadCount } from '../models/DatasetDownloadCount' import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' -import { CitationFormats } from '../models/CitationFormats' -import { CitationResponse } from '../models/CitationResponse' +import { CitationFormat } from '../models/CitationFormat' +import { FormattedCitation } from '../models/FormattedCitation' export interface IDatasetsRepository { getDataset( @@ -70,7 +70,7 @@ export interface IDatasetsRepository { getDatasetCitationInOtherFormats( datasetId: number, datasetVersionId: string, - format: CitationFormats, + format: CitationFormat, includeDeaccessioned?: boolean - ): Promise + ): Promise } diff --git a/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts b/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts index 0f470043..a447c107 100644 --- a/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts +++ b/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts @@ -1,10 +1,10 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { IDatasetsRepository } from '../repositories/IDatasetsRepository' import { DatasetNotNumberedVersion } from '../models/DatasetNotNumberedVersion' -import { CitationResponse } from '../models/CitationResponse' -import { CitationFormats } from '../models/CitationFormats' +import { FormattedCitation } from '../models/FormattedCitation' +import { CitationFormat } from '../models/CitationFormat' -export class GetDatasetCitationInOtherFormats implements UseCase { +export class GetDatasetCitationInOtherFormats implements UseCase { private datasetsRepository: IDatasetsRepository constructor(datasetsRepository: IDatasetsRepository) { @@ -16,16 +16,16 @@ export class GetDatasetCitationInOtherFormats implements UseCase} The citation content, format, and content type. + * @returns {Promise} The citation content, format, and content type. */ async execute( datasetId: number, datasetVersionId: string | DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST, - format: CitationFormats, + format: CitationFormat, includeDeaccessioned = false - ): Promise { + ): Promise { return await this.datasetsRepository.getDatasetCitationInOtherFormats( datasetId, datasetVersionId, diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 117d797f..9c585eb6 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -21,9 +21,9 @@ import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './trans import { DatasetDownloadCount } from '../../domain/models/DatasetDownloadCount' import { DatasetVersionSummaryInfo } from '../../domain/models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollection' -import { CitationFormats } from '../../domain/models/CitationFormats' +import { CitationFormat } from '../../domain/models/CitationFormat' import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers' -import { CitationResponse } from '../../domain/models/CitationResponse' +import { FormattedCitation } from '../../domain/models/FormattedCitation' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -100,9 +100,9 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi public async getDatasetCitationInOtherFormats( datasetId: number, datasetVersionId: string | 'LATEST' = 'LATEST', - format: CitationFormats, + format: CitationFormat, includeDeaccessioned = false - ): Promise { + ): Promise { const endpoint = this.buildApiEndpoint( this.datasetsResourceName, `versions/${datasetVersionId}/citation/${format}`, @@ -110,9 +110,17 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi ) const response = await this.doGet(endpoint, true, { includeDeaccessioned }) + const contentType = response.headers['content-type'] + let content: string + if (contentType && contentType.includes('application/json')) { + content = JSON.stringify(response.data) + } else { + content = response.data + } + return { - content: response.data, - contentType: response.headers['content-type'] + content, + contentType } } diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 11a9295d..7d14dfc3 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -51,7 +51,7 @@ import { import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepository' import { DirectUploadClient } from '../../../src/files/infra/clients/DirectUploadClient' import { createTestFileUploadDestination } from '../../testHelpers/files/fileUploadDestinationHelper' -import { CitationFormats } from '../../../src/datasets/domain/models/CitationFormats' +import { CitationFormat } from '../../../src/datasets/domain/models/CitationFormat' const TEST_DIFF_DATASET_DTO: DatasetDTO = { license: { @@ -508,7 +508,7 @@ describe('DatasetsRepository', () => { const citation = await sut.getDatasetCitationInOtherFormats( testDatasetIds.numericId, DatasetNotNumberedVersion.LATEST, - CitationFormats.BibTeX + CitationFormat.BibTeX ) expect(typeof citation.content).toBe('string') @@ -519,7 +519,7 @@ describe('DatasetsRepository', () => { const citation = await sut.getDatasetCitationInOtherFormats( testDatasetIds.numericId, DatasetNotNumberedVersion.LATEST, - CitationFormats.RIS + CitationFormat.RIS ) expect(typeof citation.content).toBe('string') @@ -530,10 +530,10 @@ describe('DatasetsRepository', () => { const citation = await sut.getDatasetCitationInOtherFormats( testDatasetIds.numericId, DatasetNotNumberedVersion.LATEST, - CitationFormats.CSLJson + CitationFormat.CSLJson ) - expect(typeof citation.content).toBe('object') + expect(typeof citation.content).toBe('string') expect(citation.contentType).toMatch(/application\/json/) }) @@ -541,7 +541,7 @@ describe('DatasetsRepository', () => { const citation = await sut.getDatasetCitationInOtherFormats( testDatasetIds.numericId, DatasetNotNumberedVersion.LATEST, - CitationFormats.EndNote + CitationFormat.EndNote ) expect(typeof citation.content).toBe('string') @@ -552,7 +552,7 @@ describe('DatasetsRepository', () => { const citation = await sut.getDatasetCitationInOtherFormats( testDatasetIds.numericId, DatasetNotNumberedVersion.LATEST, - CitationFormats.Internal + CitationFormat.Internal ) expect(typeof citation.content).toBe('string') @@ -567,7 +567,7 @@ describe('DatasetsRepository', () => { sut.getDatasetCitationInOtherFormats( nonExistentId, DatasetNotNumberedVersion.LATEST, - CitationFormats.RIS + CitationFormat.RIS ) ).rejects.toThrow(expectedError) }) @@ -580,7 +580,7 @@ describe('DatasetsRepository', () => { const citation = await sut.getDatasetCitationInOtherFormats( testDatasetIds.numericId, DatasetNotNumberedVersion.LATEST, - CitationFormats.RIS, + CitationFormat.RIS, true ) diff --git a/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts b/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts index 7e13e029..60f462f6 100644 --- a/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts +++ b/test/unit/datasets/GetDatasetCitationInOtherFormats.test.ts @@ -1,17 +1,17 @@ import { GetDatasetCitationInOtherFormats } from '../../../src/datasets/domain/useCases/GetDatasetCitationInOtherFormats' import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' import { ReadError } from '../../../src/core/domain/repositories/ReadError' -import { CitationFormats } from '../../../src/datasets/domain/models/CitationFormats' +import { CitationFormat } from '../../../src/datasets/domain/models/CitationFormat' import { DatasetNotNumberedVersion } from '../../../src/datasets/domain/models/DatasetNotNumberedVersion' -import { CitationResponse } from '../../../src/datasets/domain/models/CitationResponse' +import { FormattedCitation } from '../../../src/datasets/domain/models/FormattedCitation' describe('GetDatasetCitationInOtherFormats.execute', () => { const testDatasetId = 1 - const testFormat: CitationFormats = CitationFormats.BibTeX + const testFormat: CitationFormat = CitationFormat.BibTeX const testVersion: DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST test('should return citation response on repository success', async () => { - const expectedCitation: CitationResponse = { + const expectedCitation: FormattedCitation = { content: '@data{example, ...}', contentType: 'text/plain' } @@ -22,7 +22,7 @@ describe('GetDatasetCitationInOtherFormats.execute', () => { const sut = new GetDatasetCitationInOtherFormats(datasetsRepositoryStub) - const actual = await sut.execute(testDatasetId, testVersion, testFormat as CitationFormats) + const actual = await sut.execute(testDatasetId, testVersion, testFormat as CitationFormat) expect(actual).toEqual(expectedCitation) }) From 6df006e1862c213d7e0d694b38b631e122069a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 1 Aug 2025 12:29:38 -0300 Subject: [PATCH 044/123] feat: initial work, types methods --- .../models/CollectionDatasetTemplate.ts | 0 .../repositories/ICollectionsRepository.ts | 1 + .../repositories/CollectionsRepository.ts | 8 +++ .../CollectionDatasetTemplatePayload.ts | 72 +++++++++++++++++++ test/environment/.env | 4 +- 5 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/collections/domain/models/CollectionDatasetTemplate.ts create mode 100644 src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts diff --git a/src/collections/domain/models/CollectionDatasetTemplate.ts b/src/collections/domain/models/CollectionDatasetTemplate.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index a9ec708d..e614729e 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -50,4 +50,5 @@ export interface ICollectionsRepository { ): Promise deleteCollectionFeaturedItems(collectionIdOrAlias: number | string): Promise deleteCollectionFeaturedItem(featuredItemId: number): Promise + getDatasetTemplates(collectionIdOrAlias: number | string): Promise } diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index b200fa37..86e170a4 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -446,4 +446,12 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + + public async getDatasetTemplates(collectionIdOrAlias: number | string): Promise { + return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, true) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } } diff --git a/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts b/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts new file mode 100644 index 00000000..1172ee5a --- /dev/null +++ b/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts @@ -0,0 +1,72 @@ +// TODO:ME - Adding custom terms makes the get dataset templates endpoint throw internal server error + +export interface CollectionDatasetTemplatePayload { + id: number + name: string + isDefault: boolean + usageCount: number + createTime: string + createDate: string + termsOfUseAndAccess: TermsOfUseAndAccess + datasetFields: DatasetFields + instructions: Instruction[] + dataverseAlias: string +} + +export interface TermsOfUseAndAccess { + id: number + license: License + // Below fields are going to be present if are added in "Restricted Files + Terms of Access" + termsOfAccess?: string // This is terms of access for restricted files in the JSF UI + dataAccessPlace?: string + originalArchive?: string + availabilityStatus?: string + sizeOfCollection?: string + studyCompletion?: string + // Below fields are going to be present if custom terms are added in the JSF UI + termsOfUse?: string + confidentialityDeclaration?: string + specialPermissions?: string + restrictions?: string + citationRequirements?: string + depositorRequirements?: string + conditions?: string + disclaimer?: string +} + +export interface License { + id: number + name: string + shortDescription: string + uri: string + iconUrl: string + active: boolean + isDefault: boolean + sortOrder: number + rightsIdentifier: string + rightsIdentifierScheme: string + schemeUri: string + languageCode: string +} + +export interface DatasetFields { + citation: Citation +} + +export interface Citation { + displayName: string + name: string + fields: Field[] +} + +export interface Field { + typeName: string + multiple: boolean + typeClass: string + value: string +} + +export interface Instruction { + instructionField: string + instructionText: string +} diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..406a9ae1 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=11703-return-isDefault-property-get-dataset-templates DATAVERSE_BOOTSTRAP_TIMEOUT=5m From f199086480a3fe4d0ce0c4cd79822afb3410f801 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 4 Aug 2025 18:15:12 -0400 Subject: [PATCH 045/123] add linkCollection and unlinkCollection use cases --- .DS_Store | Bin 0 -> 8196 bytes .../repositories/ICollectionsRepository.ts | 8 ++ .../domain/useCases/LinkCollection.ts | 27 ++++++ .../domain/useCases/UnLinkCollection.ts | 27 ++++++ .../repositories/CollectionsRepository.ts | 26 ++++++ test/.DS_Store | Bin 0 -> 6148 bytes test/integration/.DS_Store | Bin 0 -> 6148 bytes .../collections/CollectionsRepository.test.ts | 80 ++++++++++++++++++ 8 files changed, 168 insertions(+) create mode 100644 .DS_Store create mode 100644 src/collections/domain/useCases/LinkCollection.ts create mode 100644 src/collections/domain/useCases/UnLinkCollection.ts create mode 100644 test/.DS_Store create mode 100644 test/integration/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4f3b130854e695abc9ec4e7c35549d4183cb6733 GIT binary patch literal 8196 zcmeHMJ!lj`6n>MtkVPY=JkiKPQisIS#xtB2HVQ#R6!d;V61j7aBxhr@Bnpa!jaG`t zfk-$JixeV=f~cj~Sy>1Q7D6m61Pi}6Gj}_ay*ok*QG5e4Z*RW$_WSsDZue%7h}3kW zGDValq5z%a$W9Dh8spqkZNScqBP!HWb#7{=9M$TnOqmb|!~tWgJbrUi3#d#HT>}3i6>&voJr$#e)}bPm<7zqn;^d9N`)}BjE_sQfetDj*u}Pc? z>!Si+mNu=A;Ml5*xjJ5r2L~^#YaN~1T9oj2!+qY3Hg5z^mOLNSfd`gXV=nKPtCz30 z_SfZI_CF{=M8ZA=Ub1U0ClOV39Yhhe`p3|CaovR>?x_q=-^JQt7 zUD>oQa^IeTv4gpsM?dCHJ!)ScTkEoR!#&=y4g4_BO`iY82N}hI?K+S#g*o2;AL;%6 zf4irbI&nZ8_y-(N*}_a=8ba-}H6@vUiihau(YbM6Y@#l~z$rYhx8sPNKMZjkS}bp4 TY$6aec@dy(kU<>yqYnHA<%1>d literal 0 HcmV?d00001 diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index a9ec708d..af37b091 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -50,4 +50,12 @@ export interface ICollectionsRepository { ): Promise deleteCollectionFeaturedItems(collectionIdOrAlias: number | string): Promise deleteCollectionFeaturedItem(featuredItemId: number): Promise + linkCollection( + linkedCollectionIdOrAlias: number | string, + linkingCollectionIdOrAlias: number | string + ): Promise + unlinkCollection( + linkedCollectionIdOrAlias: number | string, + linkingCollectionIdOrAlias: number | string + ): Promise } diff --git a/src/collections/domain/useCases/LinkCollection.ts b/src/collections/domain/useCases/LinkCollection.ts new file mode 100644 index 00000000..e2882af1 --- /dev/null +++ b/src/collections/domain/useCases/LinkCollection.ts @@ -0,0 +1,27 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' + +export class LinkCollection implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Deletes the Dataverse collection whose database ID or alias is given: + * + * @param {number| string} [linkedCollectionIdOrAlias] - The collection to be linked. Can be either a string (collection alias), or a number (collection id) + * @param { number | string} [linkingCollectionIdOrAlias] - The collection that will be linking to the linked collection. Can be either a string (collection alias), or a number (collection id) + * @returns {Promise} -This method does not return anything upon successful completion. + */ + async execute( + linkedCollectionIdOrAlias: string, + linkingCollectionIdOrAlias: string + ): Promise { + return await this.collectionsRepository.linkCollection( + linkedCollectionIdOrAlias, + linkingCollectionIdOrAlias + ) + } +} diff --git a/src/collections/domain/useCases/UnLinkCollection.ts b/src/collections/domain/useCases/UnLinkCollection.ts new file mode 100644 index 00000000..d36ad051 --- /dev/null +++ b/src/collections/domain/useCases/UnLinkCollection.ts @@ -0,0 +1,27 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' + +export class UnLinkCollection implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Unlinks a collection from the collection that links to it + * + * @param {number| string} [linkedCollectionIdOrAlias] - The collection that is linked. Can be either a string (collection alias), or a number (collection id) + * @param { number | string} [linkingCollectionIdOrAlias] - The collection that links to the linked collection. Can be either a string (collection alias), or a number (collection id) + * @returns {Promise} -This method does not return anything upon successful completion. + */ + async execute( + linkedCollectionIdOrAlias: string, + linkingCollectionIdOrAlias: string + ): Promise { + return await this.collectionsRepository.unlinkCollection( + linkedCollectionIdOrAlias, + linkingCollectionIdOrAlias + ) + } +} diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index b200fa37..b0537de3 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -446,4 +446,30 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + public async linkCollection( + linkedCollectionIdOrAlias: number | string, + linkingCollectionIdOrAlias: number | string + ): Promise { + console.log(linkedCollectionIdOrAlias, linkingCollectionIdOrAlias) + return this.doPut( + `/dataverses/${linkedCollectionIdOrAlias}` + `/link/${linkingCollectionIdOrAlias}`, + {} // No data is needed for this operation + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + public async unlinkCollection( + linkedCollectionIdOrAlias: number | string, + linkingCollectionIdOrAlias: number | string + ): Promise { + return this.doDelete( + `/dataverses/${linkedCollectionIdOrAlias}` + `/deleteLink/${linkingCollectionIdOrAlias}` + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/test/.DS_Store b/test/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fa0df19f23a6f5ff26a75ce0debda8b375f7ecb5 GIT binary patch literal 6148 zcmeHK!Ab)$5S_6-EcDW&$NWP7AeQwD`U6@kRcO1V=zS5d;)i(i7d-e|zR8TGi>n}l zh`fR1O)@jd?1N1*BBImV#YAK*A{CmUn$jaS-D^4u=OIwb8cW&9x!lOLG(8Lb#VN~u zgnT#1`ce+_->jCC*+sM7mb3>CkDq(4o4TH_nmIhG`fDDjK80TnY#lvZG>Q+6o!^TWExTj=RNO_gBYQGH2D%Ikd^ooI{}jJW zZ;{^(;fM^7fq%w;4(nMx#Ye^6`r-5Ju1#oXXeQ>@ngW5|c?4i!=g4s}x;>c=zZ}>) U+A20*!hw7U6hhdMfnQ+Y4Zx~3pa1{> literal 0 HcmV?d00001 diff --git a/test/integration/.DS_Store b/test/integration/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..032a606244d1fb0ee4b6585872591be9845f009c GIT binary patch literal 6148 zcmeHKy-EW?5T4N#2We7Tu8)v6IKxR=Auk|FM1_kR(DaK>U~ge9mcD_FujMyCh8&)0 z1QD4zcD~u&nZ3CWZg+=>c(PfJiAF?Jp$W1WJtD)sQwQ!m0J5%eK?}O0Ysz%fv(R50 zlH7;LI#Y{hH~-uAay(n))mqc$>C^X~tK2m6HqYTv9p~q_*Ytj}&wIJ`P7jawFMGY$ zSG!r9X)>XLfnXpQ2nK?IA25J3TcjEqh7JaTfnZ?Efb0(mO|W(>hPrh?X$b(7YqSb% zsU;*PIo6KF5Hk?AP@sjfml$l}7*Fn3I~GF=C-&ll{mq}n3+vmleo}Yh+Awr55Dat~ zIJE9a&i_;VGQCB9Hzaz&Krryn7~nxOYbN+8KU=?io}9G_?Ho-+{E8S5*quuNI7>mkvEf%c7DHJ@#x)!m7Xc+CR50)h4154-@-%k< literal 0 HcmV?d00001 diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index b215f681..d3c0d82a 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -1911,4 +1911,84 @@ describe('CollectionsRepository', () => { ).rejects.toThrow(expectedError) }) }) + describe('linkCollection', () => { + const firstCollectionAlias = 'linkCollectionFirst' + const secondCollectionAlias = 'linkCollectionSecond' + + beforeAll(async () => { + await createCollectionViaApi(firstCollectionAlias) + await createCollectionViaApi(secondCollectionAlias) + }) + + afterAll(async () => { + await deleteCollectionViaApi(firstCollectionAlias) + await deleteCollectionViaApi(secondCollectionAlias) + }) + + test('should link a collection successfully', async () => { + const firstCollection = await sut.getCollection(firstCollectionAlias) + await sut.getCollection(secondCollectionAlias) + + await sut.linkCollection(secondCollectionAlias, firstCollectionAlias) + + await sut.getCollection(secondCollectionAlias) + await new Promise((res) => setTimeout(res, 2000)) + const collectionItemSubset = await sut.getCollectionItems(firstCollection.alias) + expect(collectionItemSubset.items.length).toBe(1) + }) + + test('should throw error when linking a non-existent collection', async () => { + const invalidCollectionId = 99999 + const firstCollection = await sut.getCollection(firstCollectionAlias) + + const expectedError = new WriteError("[404] Can't find dataverse with identifier='99999'") + + await expect(sut.linkCollection(invalidCollectionId, firstCollection.id)).rejects.toThrow( + expectedError + ) + }) + }) + + describe('unlinkCollection', () => { + const firstCollectionAlias = 'unlinkCollectionFirst' + const secondCollectionAlias = 'unlinkCollectionSecond' + + beforeAll(async () => { + await createCollectionViaApi(firstCollectionAlias) + await createCollectionViaApi(secondCollectionAlias) + + const firstCollection = await sut.getCollection(firstCollectionAlias) + const secondCollection = await sut.getCollection(secondCollectionAlias) + + await sut.linkCollection(secondCollection.id, firstCollection.id) + }) + + afterAll(async () => { + await deleteCollectionViaApi(firstCollectionAlias) + await deleteCollectionViaApi(secondCollectionAlias) + }) + + test('should unlink a collection successfully', async () => { + const firstCollection = await sut.getCollection(firstCollectionAlias) + const secondCollection = await sut.getCollection(secondCollectionAlias) + + await sut.unlinkCollection(secondCollection.id, firstCollection.id) + await new Promise((res) => setTimeout(res, 2000)) + + await sut.getCollection(secondCollectionAlias) + const collectionItemSubset = await sut.getCollectionItems(firstCollection.alias) + expect(collectionItemSubset.items).toStrictEqual([]) + }) + + test('should throw error when unlinking a non-existent collection', async () => { + const invalidCollectionId = 99999 + const firstCollection = await sut.getCollection(firstCollectionAlias) + + const expectedError = new WriteError("[404] Can't find dataverse with identifier='99999'") + + await expect(sut.unlinkCollection(invalidCollectionId, firstCollection.id)).rejects.toThrow( + expectedError + ) + }) + }) }) From 3f01e901ef667b58998490b0ce879f93512654ad Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 5 Aug 2025 09:29:18 -0400 Subject: [PATCH 046/123] fix: update to dataset index.tx --- src/datasets/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/datasets/index.ts b/src/datasets/index.ts index a7a7a14b..51e7a844 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -23,6 +23,7 @@ import { DeleteDatasetDraft } from './domain/useCases/DeleteDatasetDraft' import { LinkDataset } from './domain/useCases/LinkDataset' import { UnlinkDataset } from './domain/useCases/UnlinkDataset' import { GetDatasetLinkedCollections } from './domain/useCases/GetDatasetLinkedCollections' +import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' const datasetsRepository = new DatasetsRepository() @@ -60,6 +61,7 @@ const deleteDatasetDraft = new DeleteDatasetDraft(datasetsRepository) const linkDataset = new LinkDataset(datasetsRepository) const unlinkDataset = new UnlinkDataset(datasetsRepository) const getDatasetLinkedCollections = new GetDatasetLinkedCollections(datasetsRepository) +const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) export { getDataset, @@ -80,7 +82,8 @@ export { deleteDatasetDraft, linkDataset, unlinkDataset, - getDatasetLinkedCollections + getDatasetLinkedCollections, + getDatasetCitationInOtherFormats } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' From a95794648937457a47746db1e18b14671caa9f90 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 5 Aug 2025 10:31:58 -0400 Subject: [PATCH 047/123] fix: update datasetID to allow persistent id --- .../domain/repositories/IDatasetsRepository.ts | 2 +- .../useCases/GetDatasetCitationInOtherFormats.ts | 4 ++-- src/datasets/infra/repositories/DatasetsRepository.ts | 4 ++-- test/integration/datasets/DatasetsRepository.test.ts | 11 +++++++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 72e66fd4..83390444 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -68,7 +68,7 @@ export interface IDatasetsRepository { unlinkDataset(datasetId: number, collectionAlias: string): Promise getDatasetLinkedCollections(datasetId: number | string): Promise getDatasetCitationInOtherFormats( - datasetId: number, + datasetId: number | string, datasetVersionId: string, format: CitationFormat, includeDeaccessioned?: boolean diff --git a/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts b/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts index a447c107..07bc4fdb 100644 --- a/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts +++ b/src/datasets/domain/useCases/GetDatasetCitationInOtherFormats.ts @@ -14,14 +14,14 @@ export class GetDatasetCitationInOtherFormats implements UseCase} The citation content, format, and content type. */ async execute( - datasetId: number, + datasetId: number | string, datasetVersionId: string | DatasetNotNumberedVersion = DatasetNotNumberedVersion.LATEST, format: CitationFormat, includeDeaccessioned = false diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 9c585eb6..b3b0b91e 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -78,7 +78,7 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi } public async getDatasetCitation( - datasetId: number, + datasetId: number | string, datasetVersionId: string, includeDeaccessioned: boolean ): Promise { @@ -98,7 +98,7 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi } public async getDatasetCitationInOtherFormats( - datasetId: number, + datasetId: number | string, datasetVersionId: string | 'LATEST' = 'LATEST', format: CitationFormat, includeDeaccessioned = false diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 7d14dfc3..c0251eb5 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -515,6 +515,17 @@ describe('DatasetsRepository', () => { expect(citation.contentType).toMatch(/text\/plain/) }) + test('should return citation in BibTeX format using persistent id', async () => { + const citation = await sut.getDatasetCitationInOtherFormats( + testDatasetIds.persistentId, + DatasetNotNumberedVersion.LATEST, + CitationFormat.BibTeX + ) + + expect(typeof citation.content).toBe('string') + expect(citation.contentType).toMatch(/text\/plain/) + }) + test('should return citation in RIS format', async () => { const citation = await sut.getDatasetCitationInOtherFormats( testDatasetIds.numericId, From 548ca2c9ac573b32ca60d41380a09a3ec6e879b9 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 5 Aug 2025 14:56:04 -0400 Subject: [PATCH 048/123] add functional tests --- .DS_Store | Bin 8196 -> 8196 bytes .../domain/useCases/LinkCollection.ts | 4 +- .../domain/useCases/UnLinkCollection.ts | 4 +- src/collections/index.ts | 8 +- .../repositories/CollectionsRepository.ts | 1 - .../collections/LinkCollection.test.ts | 79 ++++++++++++++++++ .../collections/UnLinkCollection.test.ts | 79 ++++++++++++++++++ 7 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 test/functional/collections/LinkCollection.test.ts create mode 100644 test/functional/collections/UnLinkCollection.test.ts diff --git a/.DS_Store b/.DS_Store index 4f3b130854e695abc9ec4e7c35549d4183cb6733..a1aeee6b5b38a1556a51ae2f3089f47fdb418bc2 100644 GIT binary patch delta 169 zcmZp1XmQw}DiF6gXBGnk0}F#5LpnnyLrHGFi%U{YeiBfOL*-KPB3H%Zj;Qh}c;yQ+ z41<&Na|?ia7??O2CN~SLRhnN2#m7}~bCBRHCZ@$XlivyF VGYc>IFgZaal1a8;^KKDOZU9)EEV%#x delta 169 zcmZp1XmQw}DiF8m;!g$!1{MZAhIEEZhLYTT7nh`*{3M_lM@!E6M=KSMJEF>`;FT}P zFbq!4&n*DzVPIlcnA|L|R%zZ-AcqxbLncECLn=cevK@1#ENWV { * @returns {Promise} -This method does not return anything upon successful completion. */ async execute( - linkedCollectionIdOrAlias: string, - linkingCollectionIdOrAlias: string + linkedCollectionIdOrAlias: number | string, + linkingCollectionIdOrAlias: number | string ): Promise { return await this.collectionsRepository.linkCollection( linkedCollectionIdOrAlias, diff --git a/src/collections/domain/useCases/UnLinkCollection.ts b/src/collections/domain/useCases/UnLinkCollection.ts index d36ad051..909c9424 100644 --- a/src/collections/domain/useCases/UnLinkCollection.ts +++ b/src/collections/domain/useCases/UnLinkCollection.ts @@ -16,8 +16,8 @@ export class UnLinkCollection implements UseCase { * @returns {Promise} -This method does not return anything upon successful completion. */ async execute( - linkedCollectionIdOrAlias: string, - linkingCollectionIdOrAlias: string + linkedCollectionIdOrAlias: number | string, + linkingCollectionIdOrAlias: number | string ): Promise { return await this.collectionsRepository.unlinkCollection( linkedCollectionIdOrAlias, diff --git a/src/collections/index.ts b/src/collections/index.ts index c275402e..31a038b5 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -12,6 +12,8 @@ import { DeleteCollectionFeaturedItems } from './domain/useCases/DeleteCollectio import { DeleteCollection } from './domain/useCases/DeleteCollection' import { GetMyDataCollectionItems } from './domain/useCases/GetMyDataCollectionItems' import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollectionFeaturedItem' +import { LinkCollection } from './domain/useCases/LinkCollection' +import { UnLinkCollection } from './domain/useCases/UnLinkCollection' const collectionsRepository = new CollectionsRepository() @@ -28,6 +30,8 @@ const updateCollectionFeaturedItems = new UpdateCollectionFeaturedItems(collecti const deleteCollectionFeaturedItems = new DeleteCollectionFeaturedItems(collectionsRepository) const deleteCollection = new DeleteCollection(collectionsRepository) const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collectionsRepository) +const linkCollection = new LinkCollection(collectionsRepository) +const unlinkCollection = new UnLinkCollection(collectionsRepository) export { getCollection, @@ -42,7 +46,9 @@ export { updateCollectionFeaturedItems, deleteCollectionFeaturedItems, deleteCollection, - deleteCollectionFeaturedItem + deleteCollectionFeaturedItem, + linkCollection, + unlinkCollection } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index b0537de3..e095920d 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -450,7 +450,6 @@ export class CollectionsRepository extends ApiRepository implements ICollections linkedCollectionIdOrAlias: number | string, linkingCollectionIdOrAlias: number | string ): Promise { - console.log(linkedCollectionIdOrAlias, linkingCollectionIdOrAlias) return this.doPut( `/dataverses/${linkedCollectionIdOrAlias}` + `/link/${linkingCollectionIdOrAlias}`, {} // No data is needed for this operation diff --git a/test/functional/collections/LinkCollection.test.ts b/test/functional/collections/LinkCollection.test.ts new file mode 100644 index 00000000..dd89d84d --- /dev/null +++ b/test/functional/collections/LinkCollection.test.ts @@ -0,0 +1,79 @@ +import { + ApiConfig, + WriteError, + createCollection, + getCollection, + linkCollection, + deleteCollection, + getCollectionItems +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper' + +describe('execute', () => { + const firstCollectionAlias = 'linkCollection-functional-test-first' + const secondCollectionAlias = 'linkCollection-functional-test-second' + + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + const firstCollection = createCollectionDTO(firstCollectionAlias) + const secondCollection = createCollectionDTO(secondCollectionAlias) + await createCollection.execute(firstCollection) + await createCollection.execute(secondCollection) + }) + + afterEach(async () => { + await Promise.all([ + getCollection + .execute(firstCollectionAlias) + .then((collection) => + collection && collection.id ? deleteCollection.execute(collection.id) : null + ), + getCollection + .execute(secondCollectionAlias) + .then((collection) => + collection && collection.id ? deleteCollection.execute(collection.id) : null + ) + ]) + }) + + test('should successfully link two collections', async () => { + const firstCollection = await getCollection.execute(firstCollectionAlias) + const secondCollection = await getCollection.execute(secondCollectionAlias) + expect.assertions(1) + try { + await linkCollection.execute(secondCollection.alias, firstCollection.alias) + } catch (error) { + throw new Error('Collections should be linked successfully') + } finally { + await new Promise((resolve) => setTimeout(resolve, 5000)) + const collectionItemSubset = await getCollectionItems.execute(firstCollectionAlias) + + expect(collectionItemSubset.items.length).toBe(1) + } + }) + + test('should throw an error when linking a non-existent collection', async () => { + const invalidCollectionId = 99999 + const firstCollection = await getCollection.execute(firstCollectionAlias) + + expect.assertions(2) + let writeError: WriteError | undefined = undefined + try { + await linkCollection.execute(invalidCollectionId, firstCollection.id) + throw new Error('Use case should throw an error') + } catch (error) { + writeError = error as WriteError + } 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='${invalidCollectionId}'` + ) + } + }) +}) diff --git a/test/functional/collections/UnLinkCollection.test.ts b/test/functional/collections/UnLinkCollection.test.ts new file mode 100644 index 00000000..e8d3a0a8 --- /dev/null +++ b/test/functional/collections/UnLinkCollection.test.ts @@ -0,0 +1,79 @@ +import { + ApiConfig, + WriteError, + createCollection, + getCollection, + linkCollection, + deleteCollection, + getCollectionItems, + unlinkCollection +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { createCollectionDTO } from '../../testHelpers/collections/collectionHelper' + +describe('execute', () => { + const firstCollectionAlias = 'unlinkCollection-functional-test-first' + const secondCollectionAlias = 'unlinkCollection-functional-test-second' + + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + const firstCollection = createCollectionDTO(firstCollectionAlias) + const secondCollection = createCollectionDTO(secondCollectionAlias) + await createCollection.execute(firstCollection) + await createCollection.execute(secondCollection) + await linkCollection.execute(secondCollection.alias, firstCollection.alias) + }) + + afterEach(async () => { + await Promise.all([ + getCollection + .execute(firstCollectionAlias) + .then((collection) => + collection && collection.id ? deleteCollection.execute(collection.id) : null + ), + getCollection + .execute(secondCollectionAlias) + .then((collection) => + collection && collection.id ? deleteCollection.execute(collection.id) : null + ) + ]) + }) + + test('should successfully unlink two collections', async () => { + const firstCollection = await getCollection.execute(firstCollectionAlias) + const secondCollection = await getCollection.execute(secondCollectionAlias) + // Give enough time to Solr for indexing + await new Promise((resolve) => setTimeout(resolve, 5000)) + const collectionItemSubset = await getCollectionItems.execute(firstCollectionAlias) + expect(collectionItemSubset.items.length).toBe(1) + + await unlinkCollection.execute(secondCollection.alias, firstCollection.alias) + await new Promise((resolve) => setTimeout(resolve, 5000)) + const collectionItemSubset2 = await getCollectionItems.execute(firstCollectionAlias) + expect(collectionItemSubset2.items.length).toBe(0) + }) + + test('should throw an error when linking a non-existent collection', async () => { + const invalidCollectionId = 99999 + const firstCollection = await getCollection.execute(firstCollectionAlias) + + expect.assertions(2) + let writeError: WriteError | undefined = undefined + try { + await unlinkCollection.execute(invalidCollectionId, firstCollection.id) + throw new Error('Use case should throw an error') + } catch (error) { + writeError = error as WriteError + } 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='${invalidCollectionId}'` + ) + } + }) +}) From 9db4c479ad24287648519ba7e3b4c03053582043 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 5 Aug 2025 15:19:58 -0400 Subject: [PATCH 049/123] use consistent names for UnlinkCollection --- .DS_Store | Bin 8196 -> 8196 bytes ...nLinkCollection.ts => UnlinkCollection.ts} | 2 +- src/collections/index.ts | 4 +-- ...ction.test.ts => UnlinkCollection.test.ts} | 0 test/unit/collections/LinkCollection.test.ts | 25 ++++++++++++++++++ .../unit/collections/UnlinkCollection.test.ts | 25 ++++++++++++++++++ 6 files changed, 53 insertions(+), 3 deletions(-) rename src/collections/domain/useCases/{UnLinkCollection.ts => UnlinkCollection.ts} (95%) rename test/functional/collections/{UnLinkCollection.test.ts => UnlinkCollection.test.ts} (100%) create mode 100644 test/unit/collections/LinkCollection.test.ts create mode 100644 test/unit/collections/UnlinkCollection.test.ts diff --git a/.DS_Store b/.DS_Store index a1aeee6b5b38a1556a51ae2f3089f47fdb418bc2..ddb8dee25d26d40029aba870d54886cd5f873410 100644 GIT binary patch delta 160 zcmZp1XmQw}CJ=kwoPmLXg+Y%YogtHru`=M@7vtUwzw8B!Qh84{802sB(-;ITPSkcXLB-&=8Vf=DEj LwZZ1yBA(m;fZ->c delta 160 zcmZp1XmQw}CJ>u5i-CcGg+Y%YogtHjpn+4V?&MyRVSb;WVGNdr1G9)6~;o#ij { +export class UnlinkCollection implements UseCase { private collectionsRepository: ICollectionsRepository constructor(collectionsRepository: ICollectionsRepository) { diff --git a/src/collections/index.ts b/src/collections/index.ts index 31a038b5..598a4e0f 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -13,7 +13,7 @@ import { DeleteCollection } from './domain/useCases/DeleteCollection' import { GetMyDataCollectionItems } from './domain/useCases/GetMyDataCollectionItems' import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollectionFeaturedItem' import { LinkCollection } from './domain/useCases/LinkCollection' -import { UnLinkCollection } from './domain/useCases/UnLinkCollection' +import { UnlinkCollection } from './domain/useCases/UnLinkCollection' const collectionsRepository = new CollectionsRepository() @@ -31,7 +31,7 @@ const deleteCollectionFeaturedItems = new DeleteCollectionFeaturedItems(collecti const deleteCollection = new DeleteCollection(collectionsRepository) const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collectionsRepository) const linkCollection = new LinkCollection(collectionsRepository) -const unlinkCollection = new UnLinkCollection(collectionsRepository) +const unlinkCollection = new UnlinkCollection(collectionsRepository) export { getCollection, diff --git a/test/functional/collections/UnLinkCollection.test.ts b/test/functional/collections/UnlinkCollection.test.ts similarity index 100% rename from test/functional/collections/UnLinkCollection.test.ts rename to test/functional/collections/UnlinkCollection.test.ts diff --git a/test/unit/collections/LinkCollection.test.ts b/test/unit/collections/LinkCollection.test.ts new file mode 100644 index 00000000..7555caa2 --- /dev/null +++ b/test/unit/collections/LinkCollection.test.ts @@ -0,0 +1,25 @@ +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { WriteError } from '../../../src' +import { LinkCollection } from '../../../src/collections/domain/useCases/LinkCollection' + +describe('execute', () => { + test('should link collection successfully on repository success', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.linkCollection = jest.fn().mockResolvedValue(undefined) + + const testLinkCollection = new LinkCollection(collectionRepositoryStub) + + await expect(testLinkCollection.execute(1, 2)).resolves.toBeUndefined() + expect(collectionRepositoryStub.linkCollection).toHaveBeenCalledWith(1, 2) + }) + + test('should throw error on repository failure', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.linkCollection = jest.fn().mockRejectedValue(new WriteError()) + + const testLinkCollection = new LinkCollection(collectionRepositoryStub) + + await expect(testLinkCollection.execute(1, 2)).rejects.toThrow(WriteError) + expect(collectionRepositoryStub.linkCollection).toHaveBeenCalledWith(1, 2) + }) +}) diff --git a/test/unit/collections/UnlinkCollection.test.ts b/test/unit/collections/UnlinkCollection.test.ts new file mode 100644 index 00000000..907aa769 --- /dev/null +++ b/test/unit/collections/UnlinkCollection.test.ts @@ -0,0 +1,25 @@ +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { WriteError } from '../../../src' +import { UnlinkCollection } from '../../../src/collections/domain/useCases/UnlinkCollection' + +describe('execute', () => { + test('should unlink collection successfully on repository success', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.unlinkCollection = jest.fn().mockResolvedValue(undefined) + + const testUnlinkCollection = new UnlinkCollection(collectionRepositoryStub) + + await expect(testUnlinkCollection.execute(1, 2)).resolves.toBeUndefined() + expect(collectionRepositoryStub.unlinkCollection).toHaveBeenCalledWith(1, 2) + }) + + test('should throw error on repository failure', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.unlinkCollection = jest.fn().mockRejectedValue(new WriteError()) + + const testUnlinkCollection = new UnlinkCollection(collectionRepositoryStub) + + await expect(testUnlinkCollection.execute(1, 2)).rejects.toThrow(WriteError) + expect(collectionRepositoryStub.unlinkCollection).toHaveBeenCalledWith(1, 2) + }) +}) From 380dedcd7de42192567e7c1a52ca198abbe85ef7 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 5 Aug 2025 15:34:24 -0400 Subject: [PATCH 050/123] fix import --- .DS_Store | Bin 8196 -> 8196 bytes src/collections/index.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.DS_Store b/.DS_Store index ddb8dee25d26d40029aba870d54886cd5f873410..7dcd531e3428528964e134752e77224cd05aa277 100644 GIT binary patch delta 144 zcmZp1XmQw}CJ_644FdxM3xgg*IzuKyNp8N2OHxjL5>Sj|w-1kkm*R0pRQVLV@&y@& v!O8i#1wcIvOgs#en+4V?&OQj_umWw!WJqC1Wk^J}<3miZve)K7!OeUC0N5j$ delta 144 zcmZp1XmQw}CJ=kwoPmLXg+Y%YogtHru`=M@7vtUwzw8B!Qh84{802sB(-;ITPSa5EnODz+pI diff --git a/src/collections/index.ts b/src/collections/index.ts index 598a4e0f..4589c5d3 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -13,7 +13,7 @@ import { DeleteCollection } from './domain/useCases/DeleteCollection' import { GetMyDataCollectionItems } from './domain/useCases/GetMyDataCollectionItems' import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollectionFeaturedItem' import { LinkCollection } from './domain/useCases/LinkCollection' -import { UnlinkCollection } from './domain/useCases/UnLinkCollection' +import { UnlinkCollection } from './domain/useCases/UnlinkCollection' const collectionsRepository = new CollectionsRepository() From ed2ebf365e1152634616389a63ae8a4bc745115e Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 5 Aug 2025 16:23:07 -0400 Subject: [PATCH 051/123] fix roles test data --- .DS_Store | Bin 8196 -> 8196 bytes test/testHelpers/roles/roleHelper.ts | 5 +---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.DS_Store b/.DS_Store index 7dcd531e3428528964e134752e77224cd05aa277..609b1bae2563c40e4e04e3d02aa49c05b5583912 100644 GIT binary patch delta 156 zcmZp1XmQw}CJ_6P0SH(a^cd0^G8sy8^Icq$a`KaaVjMm@BsIMhk2|8ur{I+@$S@2} z&d)6X8Op?CFu7S^oxBu~%?h+2lOcs6l_3$?iWi}KKW+{XSj|w-1kkm*R0pRQVLV@&y@& z!O8i#1wcIvOgs#en+4X%9|W>lffi&kq%fp1BqCe!A*NS(bATWpGn2RC> diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index 3e641cda..cf48cc3c 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -42,9 +42,7 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataverse', - 'LinkDataverse', 'PublishDataset', - 'LinkDataset', 'DeleteDataverse', 'DeleteDatasetDraft' ], @@ -101,11 +99,10 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataset', - 'LinkDataset', 'DeleteDatasetDraft' ], description: - 'For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.', + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.', id: 7 }, { From 2f24d326bf1338742e50f88180ceb7f63ccf68ec Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 11 Aug 2025 16:45:07 -0400 Subject: [PATCH 052/123] add GetCollectionLinks use case --- .DS_Store | Bin 8196 -> 8196 bytes .../domain/models/CollectionLinks.ts | 8 +++ .../domain/models/CollectionSummary.ts | 5 ++ .../repositories/ICollectionsRepository.ts | 2 + .../domain/useCases/GetCollectionLinks.ts | 22 +++++++ .../repositories/CollectionsRepository.ts | 12 ++++ .../transformers/collectionTransformers.ts | 18 +++++- src/datasets/domain/models/DatasetSummary.ts | 4 ++ test/.DS_Store | Bin 6148 -> 6148 bytes test/environment/.env | 4 +- test/integration/.DS_Store | Bin 6148 -> 6148 bytes .../collections/CollectionsRepository.test.ts | 56 +++++++++++++++++- 12 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 src/collections/domain/models/CollectionLinks.ts create mode 100644 src/collections/domain/models/CollectionSummary.ts create mode 100644 src/collections/domain/useCases/GetCollectionLinks.ts create mode 100644 src/datasets/domain/models/DatasetSummary.ts diff --git a/.DS_Store b/.DS_Store index 609b1bae2563c40e4e04e3d02aa49c05b5583912..7d0139850f8734b6fcee3de98b3d38b34a82c22f 100644 GIT binary patch delta 265 zcmZp1XmQw}DiFIgYZU_n0}F#5LpnnyLrHGFi%U{YeiBfOU;i*sPY9FhQZ1CxdlKy3`~5Jn*`RSEcy;)vjQ#1WJqC1Wk^J}BCKyKpNlHk2ox0v zOSI1Nv%5eXfusW28U{uIhRF{EMK-SxbYWszA2j)$aIn090LTR_3?)Dtiy2CC(h<&L Tkc{n{oFEd(sIqyNh!-~iN<~1s delta 265 zcmZp1XmQw}DiFK)BLfhyFz7L)Gh{N9cKsGDTf=q@KhE#?`WGh~T?)?b00!0PFk|w>WE1PA&jzCg@ zYz+e=1HVe{{YAZEDR+;8;coAa?%mbV$k=xI5|NilF@SW IE)g$o0HRAlsQ>@~ diff --git a/src/collections/domain/models/CollectionLinks.ts b/src/collections/domain/models/CollectionLinks.ts new file mode 100644 index 00000000..40b7799b --- /dev/null +++ b/src/collections/domain/models/CollectionLinks.ts @@ -0,0 +1,8 @@ +import { CollectionSummary } from './CollectionSummary' +import { DatasetSummary } from '../../../datasets/domain/models/DatasetSummary' + +export interface CollectionLinks { + linkedCollections: CollectionSummary[] + collectionsLinkingToThis: CollectionSummary[] + linkedDatasets: DatasetSummary[] +} diff --git a/src/collections/domain/models/CollectionSummary.ts b/src/collections/domain/models/CollectionSummary.ts new file mode 100644 index 00000000..bb4ee24d --- /dev/null +++ b/src/collections/domain/models/CollectionSummary.ts @@ -0,0 +1,5 @@ +export interface CollectionSummary { + id: number + alias: string + displayName: string +} diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index af37b091..820a1356 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -9,6 +9,7 @@ import { CollectionSearchCriteria } from '../models/CollectionSearchCriteria' import { CollectionUserPermissions } from '../models/CollectionUserPermissions' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType' +import { CollectionLinks } from '../models/CollectionLinks' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -58,4 +59,5 @@ export interface ICollectionsRepository { linkedCollectionIdOrAlias: number | string, linkingCollectionIdOrAlias: number | string ): Promise + getCollectionLinks(collectionIdOrAlias: number | string): Promise } diff --git a/src/collections/domain/useCases/GetCollectionLinks.ts b/src/collections/domain/useCases/GetCollectionLinks.ts new file mode 100644 index 00000000..3028e779 --- /dev/null +++ b/src/collections/domain/useCases/GetCollectionLinks.ts @@ -0,0 +1,22 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { CollectionLinks } from '../models/CollectionLinks' + +export class GetCollectionItems implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Returns a CollectionLinks object containing other collections this collection is linked to, the other collections linking to this collection, and datasets linked to this collection, given the collection identifier or alias. + * + * @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) + * If this parameter is not set, the default value is: ':root' + * @returns {Promise} + */ + async execute(collectionId: number | string): Promise { + return await this.collectionsRepository.getCollectionLinks(collectionId) + } +} diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index e095920d..0f96f8ba 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -3,6 +3,7 @@ import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRe import { transformCollectionFacetsResponseToCollectionFacets, transformCollectionItemsResponseToCollectionItemSubset, + transformCollectionLinksResponseToCollectionLinks, transformCollectionResponseToCollection, transformMyDataResponseToCollectionItemSubset } from './transformers/collectionTransformers' @@ -36,6 +37,7 @@ import { import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { ReadError } from '../../../core/domain/repositories/ReadError' +import { CollectionLinks } from '../../domain/models/CollectionLinks' export interface NewCollectionRequestPayload { alias: string @@ -471,4 +473,14 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + public async getCollectionLinks(collectionIdOrAlias: number | string): Promise { + return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/links`, true) + .then((response) => { + console.log('getCollectionLinks response:', response.data.data) // Print the response + return transformCollectionLinksResponseToCollectionLinks(response) + }) + .catch((error) => { + throw error + }) + } } diff --git a/src/collections/infra/repositories/transformers/collectionTransformers.ts b/src/collections/infra/repositories/transformers/collectionTransformers.ts index 43802706..56b152d8 100644 --- a/src/collections/infra/repositories/transformers/collectionTransformers.ts +++ b/src/collections/infra/repositories/transformers/collectionTransformers.ts @@ -44,6 +44,7 @@ import { PublicationStatusCount } from '../../../domain/models/MyDataCollectionItemSubset' import { PublicationStatus } from '../../../../core/domain/models/PublicationStatus' +import { CollectionLinks } from '../../../domain/models/CollectionLinks' export const transformCollectionResponseToCollection = (response: AxiosResponse): Collection => { const collectionPayload = response.data.data @@ -152,7 +153,22 @@ export const transformCollectionItemsResponseToCollectionItemSubset = ( ...(countPerObjectType && { countPerObjectType }) } } - +export const transformCollectionLinksResponseToCollectionLinks = ( + response: AxiosResponse +): CollectionLinks => { + const responseDataPayload = response.data.data + const linkedCollections = responseDataPayload.linkedDataverses + const collectionsLinkingToThis = responseDataPayload.dataversesLinkingToThis + const linkedDatasets = responseDataPayload.linkedDatasets + console.log('linkedCollections', linkedCollections) + console.log('collectionsLinkedToThis', collectionsLinkingToThis) + console.log('linkedDatasets', linkedDatasets) + return { + linkedCollections, + collectionsLinkingToThis, + linkedDatasets + } +} export const transformMyDataResponseToCollectionItemSubset = ( response: AxiosResponse ): MyDataCollectionItemSubset => { diff --git a/src/datasets/domain/models/DatasetSummary.ts b/src/datasets/domain/models/DatasetSummary.ts new file mode 100644 index 00000000..a868ad32 --- /dev/null +++ b/src/datasets/domain/models/DatasetSummary.ts @@ -0,0 +1,4 @@ +export interface DatasetSummary { + persistentId: string + title: string +} diff --git a/test/.DS_Store b/test/.DS_Store index fa0df19f23a6f5ff26a75ce0debda8b375f7ecb5..0ecc76d5bde691dd63753fa1a15d6fd87890c7bd 100644 GIT binary patch delta 33 pcmZoMXffDe!pJmz_GBJLsmTG1S&Rak4>G#3O>AJ>%+B$b9{{g{3PAt> delta 31 ncmZoMXffDe!pPLyKbeP7YH|Q$*5-qZZfp}9*f+Ct{N)D#p_d99 diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..92b485ab 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=11724-extend-list-dataverse-collection-links DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/integration/.DS_Store b/test/integration/.DS_Store index 032a606244d1fb0ee4b6585872591be9845f009c..ee0e96a4e69d35a729cb53a9ae7da53d297a89e8 100644 GIT binary patch delta 59 zcmZoMXffDe!pIc3dNL2A)Z_q09yVvg+4J30Cr@CMnOq0t{&uj*cK~s<7!x)hWOQSj M*ub`#o#QV*01@;O82|tP delta 59 zcmZoMXffDe!pP)4e=-lF)Z_q09yZ1Nm4`MfOrF3fGr11PRhTflX*Gze#Tc;pAfp@G M#0Iv_>>Pjj0V`7y^#A|> diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index d3c0d82a..d1afd76d 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -15,7 +15,8 @@ import { createCollection, getDatasetFiles, restrictFile, - deleteFile + deleteFile, + linkDataset } from '../../../src' import { ApiConfig } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -1991,4 +1992,57 @@ describe('CollectionsRepository', () => { ) }) }) + describe('getCollectionLinks', () => { + const firstCollectionAlias = 'getCollectionLinksFirst' + const secondCollectionAlias = 'getCollectionLinksSecond' + const thirdCollectionAlias = 'getCollectionLinksThird' + const fourthCollectionAlias = 'getCollectionLinksFourth' + let childDatasetNumericId: number + beforeAll(async () => { + await createCollectionViaApi(firstCollectionAlias) + await createCollectionViaApi(secondCollectionAlias) + await createCollectionViaApi(thirdCollectionAlias) + await createCollectionViaApi(fourthCollectionAlias) + const { numericId: createdId } = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + fourthCollectionAlias + ) + childDatasetNumericId = createdId + await sut.linkCollection(secondCollectionAlias, firstCollectionAlias) + await sut.linkCollection(firstCollectionAlias, thirdCollectionAlias) + await sut.linkCollection(firstCollectionAlias, fourthCollectionAlias) + await linkDataset.execute(childDatasetNumericId, firstCollectionAlias) + }) + + afterAll(async () => { + await deleteUnpublishedDatasetViaApi(childDatasetNumericId) + await deleteCollectionViaApi(firstCollectionAlias) + await deleteCollectionViaApi(secondCollectionAlias) + await deleteCollectionViaApi(thirdCollectionAlias) + await deleteCollectionViaApi(fourthCollectionAlias) + }) + + test('should return collection links successfully', async () => { + const firstCollection = await sut.getCollection(firstCollectionAlias) + const collectionLinks = await sut.getCollectionLinks(firstCollection.id) + + expect(collectionLinks.linkedCollections).toHaveLength(1) + + expect(collectionLinks.linkedCollections[0].alias).toBe(secondCollectionAlias) + expect(collectionLinks.collectionsLinkingToThis).toHaveLength(2) + expect(collectionLinks.collectionsLinkingToThis[0].alias).toBe(thirdCollectionAlias) + expect(collectionLinks.collectionsLinkingToThis[1].alias).toBe(fourthCollectionAlias) + expect(collectionLinks.linkedDatasets).toHaveLength(1) + expect(collectionLinks.linkedDatasets[0].title).toBe( + 'Dataset created using the createDataset use case' + ) + }) + + test('should return error when collection does not exist', async () => { + const invalidCollectionId = 99999 + const expectedError = new ReadError("[404] Can't find dataverse with identifier='99999'") + + await expect(sut.getCollectionLinks(invalidCollectionId)).rejects.toThrow(expectedError) + }) + }) }) From c2564a1fd00564ebf3867796a8f41543b990eb96 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 12 Aug 2025 14:14:04 -0400 Subject: [PATCH 053/123] remove console.log() --- .DS_Store | Bin 8196 -> 8196 bytes .../repositories/CollectionsRepository.ts | 1 - .../transformers/collectionTransformers.ts | 3 --- 3 files changed, 4 deletions(-) diff --git a/.DS_Store b/.DS_Store index 7d0139850f8734b6fcee3de98b3d38b34a82c22f..f5fea97756ffd74e77ac6f33202d1aee9fb2b100 100644 GIT binary patch delta 200 zcmZp1XmQw}DiF7fQ-^_pfrUYjA)O(Up(Hoo#U&{xKM5$tq5o6y+g;V;j;Qh}c;yQ+ z41<&Na|?ia7?=beOl}rft2mbj$YBNAkjaq3kjjvVY{#0V2Ls$E2MWrnfmsmSkklfZ b$H2(IF!_O?$mSJ-E=)}8yf?oS7UKZ`sU;i*sPY9FhQZ1CxdlKy3`~3mlbZ$BDlYmCE1%2cKtWkG mt+V{>E)d(0)FPY5z$m~l`GKIw<`sf2Oib&8Hop@V;{gEXpfXAT diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 0f96f8ba..135ffecc 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -476,7 +476,6 @@ export class CollectionsRepository extends ApiRepository implements ICollections public async getCollectionLinks(collectionIdOrAlias: number | string): Promise { return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/links`, true) .then((response) => { - console.log('getCollectionLinks response:', response.data.data) // Print the response return transformCollectionLinksResponseToCollectionLinks(response) }) .catch((error) => { diff --git a/src/collections/infra/repositories/transformers/collectionTransformers.ts b/src/collections/infra/repositories/transformers/collectionTransformers.ts index 56b152d8..a26c4718 100644 --- a/src/collections/infra/repositories/transformers/collectionTransformers.ts +++ b/src/collections/infra/repositories/transformers/collectionTransformers.ts @@ -160,9 +160,6 @@ export const transformCollectionLinksResponseToCollectionLinks = ( const linkedCollections = responseDataPayload.linkedDataverses const collectionsLinkingToThis = responseDataPayload.dataversesLinkingToThis const linkedDatasets = responseDataPayload.linkedDatasets - console.log('linkedCollections', linkedCollections) - console.log('collectionsLinkedToThis', collectionsLinkingToThis) - console.log('linkedDatasets', linkedDatasets) return { linkedCollections, collectionsLinkingToThis, From 74321bfe94b8ddc3d6952bfa5964635648357834 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 14 Aug 2025 12:20:36 -0400 Subject: [PATCH 054/123] Reapply "fix RolesRepository integration test" This reverts commit 997a4fb0e3386c8297db36d018a2bf8331fc4479. --- test/testHelpers/roles/roleHelper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index cf48cc3c..3e641cda 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -42,7 +42,9 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataverse', + 'LinkDataverse', 'PublishDataset', + 'LinkDataset', 'DeleteDataverse', 'DeleteDatasetDraft' ], @@ -99,10 +101,11 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataset', + 'LinkDataset', 'DeleteDatasetDraft' ], description: - 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.', + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.', id: 7 }, { From 628e4e657805c04b248ed6c55a02de0d3b4d6047 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 14 Aug 2025 12:32:23 -0400 Subject: [PATCH 055/123] fix: change order --- test/testHelpers/roles/roleHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index 3e641cda..d792b377 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -42,8 +42,8 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataverse', - 'LinkDataverse', 'PublishDataset', + 'LinkDataverse', 'LinkDataset', 'DeleteDataverse', 'DeleteDatasetDraft' From ed5feeb29bcf4f19536445a647dec817b04976ef Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 15 Aug 2025 09:39:35 -0400 Subject: [PATCH 056/123] fix: add a , --- src/datasets/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datasets/index.ts b/src/datasets/index.ts index aa13cf5c..36b8c6b3 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -85,7 +85,7 @@ export { linkDataset, unlinkDataset, getDatasetLinkedCollections, - getDatasetAvailableCategories + getDatasetAvailableCategories, getDatasetCitationInOtherFormats } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' From 5ec425ac0795ad5685252dfac4da620dd4d0f7bd Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 15 Aug 2025 09:47:10 -0400 Subject: [PATCH 057/123] fix: tests roleHelper --- test/testHelpers/roles/roleHelper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index 04ac0fdb..58769ed7 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -101,10 +101,11 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataset', + 'LinkDataset', 'DeleteDatasetDraft' ], description: - 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets.', + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets and link datasets.', id: 7 }, { From ace8b9de85814b2d9bb2b1768a381d6f5f62f02e Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 15 Aug 2025 09:47:57 -0400 Subject: [PATCH 058/123] fix: tests roleHelper --- test/testHelpers/roles/roleHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index 58769ed7..d792b377 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -105,7 +105,7 @@ export const createSuperAdminRoleArray = (): Role[] => { 'DeleteDatasetDraft' ], description: - 'For datasets, a person who can edit License + Terms, edit Permissions, and publish datasets and link datasets.', + 'For datasets, a person who can edit License + Terms, edit Permissions, and publish and link datasets.', id: 7 }, { From 1d78fbb7d3b392cf9f7ace737f6bf7a0b4a7c7c8 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 15 Aug 2025 10:58:41 -0400 Subject: [PATCH 059/123] remove .DS_Store --- .DS_Store | Bin 8196 -> 8196 bytes .gitignore | 3 +++ test/.DS_Store | Bin 6148 -> 0 bytes test/environment/.env | 4 ++-- test/integration/.DS_Store | Bin 6148 -> 0 bytes 5 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 test/.DS_Store delete mode 100644 test/integration/.DS_Store diff --git a/.DS_Store b/.DS_Store index f5fea97756ffd74e77ac6f33202d1aee9fb2b100..e9ecac09f4c5bb3b1b643dcf5b8336bf3ac7e742 100644 GIT binary patch delta 181 zcmZp1XmQw}DiF6^Xchwl0}F#5LpnnyLrHGFi%U{YeiBfO`;FT}P zFbq!4&n*DzVPFysnA|L|PN59QW(8W1$&kX3%8-a`MZM##nn#-h1$mj7bW{CF*#LGeshxGA10>QzR3w95^Qew aB*NFoO|BD>f-&cbNHeJzY~C&6$qfK0=`N1| diff --git a/.gitignore b/.gitignore index dbc1d777..e8782206 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ node_modules # unit tests coverage +# macOS +.DS_Store + # ignore npm lock package-json.lock .npmrc \ No newline at end of file diff --git a/test/.DS_Store b/test/.DS_Store deleted file mode 100644 index 0ecc76d5bde691dd63753fa1a15d6fd87890c7bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOG*Pl5UomPF_2A`F8d0(K^w*sZkqg@g=>Uf9E~_f%oR-Ad5=A zQ%Eon3VaYO!9Xw&416#k`$Iw#%#OuSw+?iA1pvx1S_QhEQGrPe z!0cEIVS%uP0xgvNioq5Rdvd?*SPU(kSf7k_{K@Z^7xoh}Cv_*zhS3KD!N4&CeH#v? z{$Jo%>b&G1hr}ot2nJ4!0j`=!Gsa7KZT<9mQfm|1C7Ou%6;UA2wG;!j6k{O!$eB*s bd=ee=*|8YPDq`1gU>pRLkm!PeKVaYu8(KBS diff --git a/test/environment/.env b/test/environment/.env index 92b485ab..e7b54bde 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=11724-extend-list-dataverse-collection-links +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/integration/.DS_Store b/test/integration/.DS_Store deleted file mode 100644 index ee0e96a4e69d35a729cb53a9ae7da53d297a89e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy-ve05Wa&Bk-Btb^uhyVXD}tOqrO1Z76DRPR|59D2LlhlgYZx+eD_1uh|mQI zA#@kn-`T#qTs|qbkBE5sw5o|FL{y;(vM2*0(}PP#Zaf0A#<-y+J<>fD+6)Bxi&K*O z7+Du;@$BdS(B9UwWm#`EZN7f}-f~@@fEUJt8`Q_^L?fj7SYG?hP&q_XMy}jGd zVvevO!9Xw&3gTfnJrR{4WkbRf`MRQ$AIh)2~99Nc80ojpwkinDA#Be=u%6F zPjbwTogr2rY_33aWiK(<+%ccrE<1LH=1%Oz2m6~pix<|nBYskI;%pdwFc1v%8927# zM9%+9{4#?@e%~cV!9XzZ&luoQGi#>!C_h`je4d=O32lxhB6gi95a_*200y#;oamyh bC(&V-9XmsbBJ=7FjEjI05?wIx3k-Y!0S7ai From 3a969cbaec1a0a95b93f2cd8974fd8f8bdf9f0d6 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 15 Aug 2025 11:04:18 -0400 Subject: [PATCH 060/123] Remove remaining .DS_Store file --- .DS_Store | Bin 8196 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index e9ecac09f4c5bb3b1b643dcf5b8336bf3ac7e742..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM&ubGw82zT%;-VE6K}gZcN-Lg})}t5G5)t%Zpm-1jO|qq}ByMP%UKCsqLGj>G zPy`W550&Df2QN~qsQ4d<#~uVhP!HZb_~u8tGub4R9z^jQn0cG|zHi^-WwN`oAtEu+ zjK+xaMC7AO4EA8?&}3ZpOzSfJMgzSfP1!V zewX{cj`GM3umk_41M>P1qDw3oOf;%X2L`nT0D72K4ab2z0EPw&1`~~lpb1?H)TP2~ zF@!G1d}#6t1{00CoP^nY2*WJQ4n-(-oIh0QBnldNWCz%RtOIiI?$L3Yrde8W_V3I4 z9@?>bTr5^-O%dj7HflAyTg_`k`l*BATMItL5<`Sgfe66pQ;qoBKYU1jBjMhQ89FC4JtA@wwCkj7w+{xc_%&Tke4dzmFMUh9>uA! zJ|g%s-?l#dv$yP+t>evzfAZ>@(y>)rk5m3mahLadi#LWR^NtVV#FF7vF`M`E?dx|} z4w-cs!AsQIy!XEhzQ`SP;|{M->vDjup;oSg%T!lKO{(FOqhojD(NE~;nrrJC{WY+B zX-M(*$WQ&*PQI;s@AFsEuN=OkIjTBtd8_Brh^^^Pn3P}Ul_pbXJC%*vd=vzd&!YwD zO53^^JQmq8oAdCu2b=4y>xQYdSv$oY-r)cb2CDJ>U;Dr#JFrs+a=I`t_y4E6zyIIq z>A8*_U Date: Fri, 15 Aug 2025 11:48:55 -0400 Subject: [PATCH 061/123] add GetCollectionLinks to index.ts --- src/collections/domain/useCases/GetCollectionLinks.ts | 2 +- src/collections/domain/useCases/LinkCollection.ts | 2 +- src/collections/index.ts | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/collections/domain/useCases/GetCollectionLinks.ts b/src/collections/domain/useCases/GetCollectionLinks.ts index 3028e779..fa0b6c92 100644 --- a/src/collections/domain/useCases/GetCollectionLinks.ts +++ b/src/collections/domain/useCases/GetCollectionLinks.ts @@ -2,7 +2,7 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { ICollectionsRepository } from '../repositories/ICollectionsRepository' import { CollectionLinks } from '../models/CollectionLinks' -export class GetCollectionItems implements UseCase { +export class GetCollectionLinks implements UseCase { private collectionsRepository: ICollectionsRepository constructor(collectionsRepository: ICollectionsRepository) { diff --git a/src/collections/domain/useCases/LinkCollection.ts b/src/collections/domain/useCases/LinkCollection.ts index 4e5c5ad3..4ea0b2b4 100644 --- a/src/collections/domain/useCases/LinkCollection.ts +++ b/src/collections/domain/useCases/LinkCollection.ts @@ -9,7 +9,7 @@ export class LinkCollection implements UseCase { } /** - * Deletes the Dataverse collection whose database ID or alias is given: + * Creates a link between two collections. The linked collection will be linked to the linking collection.: * * @param {number| string} [linkedCollectionIdOrAlias] - The collection to be linked. Can be either a string (collection alias), or a number (collection id) * @param { number | string} [linkingCollectionIdOrAlias] - The collection that will be linking to the linked collection. Can be either a string (collection alias), or a number (collection id) diff --git a/src/collections/index.ts b/src/collections/index.ts index 4589c5d3..05e49954 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -14,6 +14,7 @@ import { GetMyDataCollectionItems } from './domain/useCases/GetMyDataCollectionI import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollectionFeaturedItem' import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' +import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' const collectionsRepository = new CollectionsRepository() @@ -32,6 +33,7 @@ const deleteCollection = new DeleteCollection(collectionsRepository) const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collectionsRepository) const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) +const getCollectionLinks = new GetCollectionLinks(collectionsRepository) export { getCollection, @@ -48,7 +50,8 @@ export { deleteCollection, deleteCollectionFeaturedItem, linkCollection, - unlinkCollection + unlinkCollection, + getCollectionLinks } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' From 8d47d048444c10209081834cfb073f3bb4b057aa Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 15 Aug 2025 12:25:59 -0400 Subject: [PATCH 062/123] fix string for API url, update unit UnlinkCollection test --- .../repositories/CollectionsRepository.ts | 4 +- .../collections/UnlinkCollection.test.ts | 38 ++++++++----------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 135ffecc..18e3c474 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -453,7 +453,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections linkingCollectionIdOrAlias: number | string ): Promise { return this.doPut( - `/dataverses/${linkedCollectionIdOrAlias}` + `/link/${linkingCollectionIdOrAlias}`, + `/dataverses/${linkedCollectionIdOrAlias}/link/${linkingCollectionIdOrAlias}`, {} // No data is needed for this operation ) .then(() => undefined) @@ -466,7 +466,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections linkingCollectionIdOrAlias: number | string ): Promise { return this.doDelete( - `/dataverses/${linkedCollectionIdOrAlias}` + `/deleteLink/${linkingCollectionIdOrAlias}` + `/dataverses/${linkedCollectionIdOrAlias}/deleteLink/${linkingCollectionIdOrAlias}` ) .then(() => undefined) .catch((error) => { diff --git a/test/functional/collections/UnlinkCollection.test.ts b/test/functional/collections/UnlinkCollection.test.ts index e8d3a0a8..c1c1b4e3 100644 --- a/test/functional/collections/UnlinkCollection.test.ts +++ b/test/functional/collections/UnlinkCollection.test.ts @@ -2,7 +2,6 @@ import { ApiConfig, WriteError, createCollection, - getCollection, linkCollection, deleteCollection, getCollectionItems, @@ -16,43 +15,37 @@ describe('execute', () => { const firstCollectionAlias = 'unlinkCollection-functional-test-first' const secondCollectionAlias = 'unlinkCollection-functional-test-second' + let firstCollectionId: number + let secondCollectionId: number beforeEach(async () => { ApiConfig.init( TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, process.env.TEST_API_KEY ) - const firstCollection = createCollectionDTO(firstCollectionAlias) - const secondCollection = createCollectionDTO(secondCollectionAlias) - await createCollection.execute(firstCollection) - await createCollection.execute(secondCollection) - await linkCollection.execute(secondCollection.alias, firstCollection.alias) + const firstCollectionDTO = createCollectionDTO(firstCollectionAlias) + const secondCollectionDTO = createCollectionDTO(secondCollectionAlias) + firstCollectionId = await createCollection.execute(firstCollectionDTO) + secondCollectionId = await createCollection.execute(secondCollectionDTO) + await linkCollection.execute(secondCollectionAlias, firstCollectionAlias) + // Give enough time to Solr for indexing + await new Promise((resolve) => setTimeout(resolve, 5000)) }) afterEach(async () => { await Promise.all([ - getCollection - .execute(firstCollectionAlias) - .then((collection) => - collection && collection.id ? deleteCollection.execute(collection.id) : null - ), - getCollection - .execute(secondCollectionAlias) - .then((collection) => - collection && collection.id ? deleteCollection.execute(collection.id) : null - ) + deleteCollection.execute(firstCollectionId), + deleteCollection.execute(secondCollectionId) ]) }) test('should successfully unlink two collections', async () => { - const firstCollection = await getCollection.execute(firstCollectionAlias) - const secondCollection = await getCollection.execute(secondCollectionAlias) - // Give enough time to Solr for indexing - await new Promise((resolve) => setTimeout(resolve, 5000)) + // Verify that the collections are linked const collectionItemSubset = await getCollectionItems.execute(firstCollectionAlias) expect(collectionItemSubset.items.length).toBe(1) - await unlinkCollection.execute(secondCollection.alias, firstCollection.alias) + await unlinkCollection.execute(secondCollectionAlias, firstCollectionAlias) + // Wait for the unlinking to be processed by Solr await new Promise((resolve) => setTimeout(resolve, 5000)) const collectionItemSubset2 = await getCollectionItems.execute(firstCollectionAlias) expect(collectionItemSubset2.items.length).toBe(0) @@ -60,12 +53,11 @@ describe('execute', () => { test('should throw an error when linking a non-existent collection', async () => { const invalidCollectionId = 99999 - const firstCollection = await getCollection.execute(firstCollectionAlias) expect.assertions(2) let writeError: WriteError | undefined = undefined try { - await unlinkCollection.execute(invalidCollectionId, firstCollection.id) + await unlinkCollection.execute(invalidCollectionId, firstCollectionId) throw new Error('Use case should throw an error') } catch (error) { writeError = error as WriteError From b9d0422d28f318dde5cc28ce9430b74467a264ce Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 15 Aug 2025 12:33:06 -0400 Subject: [PATCH 063/123] Update LinkCollection.test.ts --- .../collections/LinkCollection.test.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/test/functional/collections/LinkCollection.test.ts b/test/functional/collections/LinkCollection.test.ts index dd89d84d..eff7550d 100644 --- a/test/functional/collections/LinkCollection.test.ts +++ b/test/functional/collections/LinkCollection.test.ts @@ -14,7 +14,8 @@ import { createCollectionDTO } from '../../testHelpers/collections/collectionHel describe('execute', () => { const firstCollectionAlias = 'linkCollection-functional-test-first' const secondCollectionAlias = 'linkCollection-functional-test-second' - + let firstCollectionId: number + let secondCollectionId: number beforeEach(async () => { ApiConfig.init( TestConstants.TEST_API_URL, @@ -23,34 +24,25 @@ describe('execute', () => { ) const firstCollection = createCollectionDTO(firstCollectionAlias) const secondCollection = createCollectionDTO(secondCollectionAlias) - await createCollection.execute(firstCollection) - await createCollection.execute(secondCollection) + firstCollectionId = await createCollection.execute(firstCollection) + secondCollectionId = await createCollection.execute(secondCollection) }) afterEach(async () => { await Promise.all([ - getCollection - .execute(firstCollectionAlias) - .then((collection) => - collection && collection.id ? deleteCollection.execute(collection.id) : null - ), - getCollection - .execute(secondCollectionAlias) - .then((collection) => - collection && collection.id ? deleteCollection.execute(collection.id) : null - ) + deleteCollection.execute(firstCollectionId), + deleteCollection.execute(secondCollectionId) ]) }) test('should successfully link two collections', async () => { - const firstCollection = await getCollection.execute(firstCollectionAlias) - const secondCollection = await getCollection.execute(secondCollectionAlias) expect.assertions(1) try { - await linkCollection.execute(secondCollection.alias, firstCollection.alias) + await linkCollection.execute(secondCollectionAlias, firstCollectionAlias) } catch (error) { throw new Error('Collections should be linked successfully') } finally { + // Wait for the linking to be processed by Solr await new Promise((resolve) => setTimeout(resolve, 5000)) const collectionItemSubset = await getCollectionItems.execute(firstCollectionAlias) From 14e796898acec6fcf3298dd58b9579000d46ef6b Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 15 Aug 2025 12:55:27 -0400 Subject: [PATCH 064/123] fix test description --- test/functional/collections/UnlinkCollection.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/collections/UnlinkCollection.test.ts b/test/functional/collections/UnlinkCollection.test.ts index c1c1b4e3..0b20b455 100644 --- a/test/functional/collections/UnlinkCollection.test.ts +++ b/test/functional/collections/UnlinkCollection.test.ts @@ -51,7 +51,7 @@ describe('execute', () => { expect(collectionItemSubset2.items.length).toBe(0) }) - test('should throw an error when linking a non-existent collection', async () => { + test('should throw an error when unlinking a non-existent collection', async () => { const invalidCollectionId = 99999 expect.assertions(2) From 396c348620d7083fad35ddd225e62b3e14edd225 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 15 Aug 2025 13:36:10 -0400 Subject: [PATCH 065/123] fix integrations --- test/testHelpers/roles/roleHelper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/testHelpers/roles/roleHelper.ts b/test/testHelpers/roles/roleHelper.ts index be63c0f1..d792b377 100644 --- a/test/testHelpers/roles/roleHelper.ts +++ b/test/testHelpers/roles/roleHelper.ts @@ -42,7 +42,6 @@ export const createSuperAdminRoleArray = (): Role[] => { 'ManageDatasetPermissions', 'ManageFilePermissions', 'PublishDataverse', - 'LinkDataverse', 'PublishDataset', 'LinkDataverse', 'LinkDataset', From e93174e59e50486f65c5d0e91bd95e0cf52e35bd Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 15 Aug 2025 15:13:28 -0400 Subject: [PATCH 066/123] feat: update get notification parameter --- .../domain/models/Notification.ts | 71 ++++++++++++++++++- .../repositories/INotificationsRepository.ts | 2 +- .../useCases/GetAllNotificationsByUser.ts | 7 +- src/notifications/index.ts | 2 +- .../repositories/NotificationsRepository.ts | 11 ++- test/environment/.env | 4 +- .../GetAllNotificationsByUser.test.ts | 16 +++++ .../NotificationsRepository.test.ts | 50 +++++++++++-- .../notifications/DeleteNotification.test.ts | 15 ++-- .../GetAllNotificationsByUser.test.ts | 15 ++-- 10 files changed, 167 insertions(+), 26 deletions(-) diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index f99a2170..26f41d0d 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -1,7 +1,72 @@ +export enum NotificationType { + ASSIGNROLE = 'ASSIGNROLE', + REVOKEROLE = 'REVOKEROLE', + CREATEDV = 'CREATEDV', + CREATEDS = 'CREATEDS', + CREATEACC = 'CREATEACC', + SUBMITTEDDS = 'SUBMITTEDDS', + RETURNEDDS = 'RETURNEDDS', + PUBLISHEDDS = 'PUBLISHEDDS', + REQUESTFILEACCESS = 'REQUESTFILEACCESS', + GRANTFILEACCESS = 'GRANTFILEACCESS', + REJECTFILEACCESS = 'REJECTFILEACCESS', + FILESYSTEMIMPORT = 'FILESYSTEMIMPORT', + CHECKSUMIMPORT = 'CHECKSUMIMPORT', + CHECKSUMFAIL = 'CHECKSUMFAIL', + CONFIRMEMAIL = 'CONFIRMEMAIL', + APIGENERATED = 'APIGENERATED', + INGESTCOMPLETED = 'INGESTCOMPLETED', + INGESTCOMPLETEDWITHERRORS = 'INGESTCOMPLETEDWITHERRORS', + PUBLISHFAILED_PIDREG = 'PUBLISHFAILED_PIDREG', + WORKFLOW_SUCCESS = 'WORKFLOW_SUCCESS', + WORKFLOW_FAILURE = 'WORKFLOW_FAILURE', + STATUSUPDATED = 'STATUSUPDATED', + DATASETCREATED = 'DATASETCREATED', + DATASETMENTIONED = 'DATASETMENTIONED', + GLOBUSUPLOADCOMPLETED = 'GLOBUSUPLOADCOMPLETED', + GLOBUSUPLOADCOMPLETEDWITHERRORS = 'GLOBUSUPLOADCOMPLETEDWITHERRORS', + GLOBUSDOWNLOADCOMPLETED = 'GLOBUSDOWNLOADCOMPLETED', + GLOBUSDOWNLOADCOMPLETEDWITHERRORS = 'GLOBUSDOWNLOADCOMPLETEDWITHERRORS', + REQUESTEDFILEACCESS = 'REQUESTEDFILEACCESS', + GLOBUSUPLOADREMOTEFAILURE = 'GLOBUSUPLOADREMOTEFAILURE', + GLOBUSUPLOADLOCALFAILURE = 'GLOBUSUPLOADLOCALFAILURE', + PIDRECONCILED = 'PIDRECONCILED' +} + +export interface RoleAssignment { + id: number + assignee: string + definitionPointId: number + roleId: number + roleName: string + _roleAlias: string +} + export interface Notification { id: number - type: string - subjectText: string - messageText: string + type: NotificationType + subjectText?: string + messageText?: string sentTimestamp: string + displayAsRead?: boolean + installationBrandName?: string + userGuidesBaseUrl?: string + userGuidesVersion?: string + userGuidesSectionPath?: string + roleAssignments?: RoleAssignment[] + dataverseAlias?: string + dataverseDisplayName?: string + datasetPersistentIdentifier?: string + datasetDisplayName?: string + ownerPersistentIdentifier?: string + ownerAlias?: string + ownerDisplayName?: string + requestorFirstName?: string + requestorLastName?: string + requestorEmail?: string + dataFileId?: number + dataFileDisplayName?: string + currentCurationStatus?: string + additionalInfo?: string + objectDeleted?: boolean } diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index dd40d071..c414f8b8 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -1,6 +1,6 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { - getAllNotificationsByUser(): Promise + getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise deleteNotification(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts index 1eb70ac8..43555ccc 100644 --- a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -8,9 +8,12 @@ export class GetAllNotificationsByUser implements UseCase { /** * Use case for retrieving all notifications for the current user. * + * @param inAppNotificationFormat - Optional parameter to retrieve fields needed for in-app notifications * @returns {Promise} - A promise that resolves to an array of Notification instances. */ - async execute(): Promise { - return (await this.notificationsRepository.getAllNotificationsByUser()) as Notification[] + async execute(inAppNotificationFormat?: boolean): Promise { + return (await this.notificationsRepository.getAllNotificationsByUser( + inAppNotificationFormat + )) as Notification[] } } diff --git a/src/notifications/index.ts b/src/notifications/index.ts index 72ae6e82..8e97bfcc 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -9,4 +9,4 @@ const deleteNotification = new DeleteNotification(notificationsRepository) export { getAllNotificationsByUser, deleteNotification } -export { Notification } from './domain/models/Notification' +export { Notification, NotificationType, RoleAssignment } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 7cd608b9..b298d040 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -5,8 +5,15 @@ import { Notification } from '../../domain/models/Notification' export class NotificationsRepository extends ApiRepository implements INotificationsRepository { private readonly notificationsResourceName: string = 'notifications' - public async getAllNotificationsByUser(): Promise { - return this.doGet(this.buildApiEndpoint(this.notificationsResourceName, 'all'), true) + public async getAllNotificationsByUser( + inAppNotificationFormat?: boolean + ): Promise { + const queryParams = inAppNotificationFormat ? { inAppNotificationFormat: 'true' } : undefined + return this.doGet( + this.buildApiEndpoint(this.notificationsResourceName, 'all'), + true, + queryParams + ) .then((response) => response.data.data.notifications as Notification[]) .catch((error) => { throw error diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..d7d1957e 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=11648-notifications-api-extension DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 59d950d9..11f9a33c 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -25,4 +25,20 @@ describe('execute', () => { expect(notifications[0]).toHaveProperty('type') expect(notifications[0]).toHaveProperty('sentTimestamp') }) + + test('should have correct in-app notification properties when inAppNotificationFormat is true', async () => { + const notifications = await getAllNotificationsByUser.execute(true) + + expect(notifications[0]).toHaveProperty('id') + expect(notifications[0]).toHaveProperty('type') + expect(notifications[0]).toHaveProperty('sentTimestamp') + expect(notifications[0]).toHaveProperty('displayAsRead') + expect(notifications[0]).toHaveProperty('roleAssignments') + expect(notifications[0].roleAssignments).toBeDefined() + expect(notifications[0].roleAssignments?.length).toBeGreaterThan(0) + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleId', 1) + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) + }) }) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index df619136..d29dbb49 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -4,13 +4,21 @@ import { } from '../../../src/core/infra/repositories/ApiConfig' import { TestConstants } from '../../testHelpers/TestConstants' import { NotificationsRepository } from '../../../src/notifications/infra/repositories/NotificationsRepository' -import { Notification } from '../../../src/notifications/domain/models/Notification' -import { createDataset } from '../../../src/datasets' -import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' +import { + Notification, + NotificationType +} from '../../../src/notifications/domain/models/Notification' +import { createDataset, CreatedDatasetIdentifiers } from '../../../src/datasets' +import { + deletePublishedDatasetViaApi, + publishDatasetViaApi, + waitForNoLocks +} from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' describe('NotificationsRepository', () => { const sut: NotificationsRepository = new NotificationsRepository() + let testDatasetIds: CreatedDatasetIdentifiers beforeEach(() => { ApiConfig.init( @@ -20,9 +28,13 @@ describe('NotificationsRepository', () => { ) }) + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + }) + test('should return notifications after creating and publishing a dataset', async () => { // Create a dataset and publish it so that a notification of Dataset published is created - const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) await publishDatasetViaApi(testDatasetIds.numericId) await waitForNoLocks(testDatasetIds.numericId, 10) @@ -32,7 +44,9 @@ describe('NotificationsRepository', () => { expect(Array.isArray(notifications)).toBe(true) expect(notifications.length).toBeGreaterThan(0) - const publishedNotification = notifications.find((n) => n.type === 'PUBLISHEDDS') + const publishedNotification = notifications.find( + (n) => n.type === NotificationType.PUBLISHEDDS + ) as Notification expect(publishedNotification).toBeDefined() @@ -73,4 +87,30 @@ describe('NotificationsRepository', () => { await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) + + test('should return notifications with basic properties when inAppNotificationFormat is true', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + + const notification = notifications[0] + expect(notification).toHaveProperty('id') + expect(notification).toHaveProperty('type') + expect(notification.type).toBe(NotificationType.ASSIGNROLE) + expect(notification).toHaveProperty('sentTimestamp') + expect(notification).toHaveProperty('displayAsRead') + expect(notification).toHaveProperty('dataverseDisplayName') + + expect(notification).toHaveProperty('roleAssignments') + expect(notification.roleAssignments).toBeDefined() + expect(notification.roleAssignments?.length).toBeGreaterThan(0) + expect(notification.roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') + expect(notification.roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') + expect(notification.roleAssignments?.[0]).toHaveProperty('roleId', 1) + expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) + }) + + test('should return array when inAppNotificationFormat is false', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(false) + + expect(Array.isArray(notifications)).toBe(true) + }) }) diff --git a/test/unit/notifications/DeleteNotification.test.ts b/test/unit/notifications/DeleteNotification.test.ts index 4ad62444..d568b975 100644 --- a/test/unit/notifications/DeleteNotification.test.ts +++ b/test/unit/notifications/DeleteNotification.test.ts @@ -1,21 +1,26 @@ import { DeleteNotification } from '../../../src/notifications/domain/useCases/DeleteNotification' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' -import { Notification } from '../../../src/notifications/domain/models/Notification' +import { + Notification, + NotificationType +} from '../../../src/notifications/domain/models/Notification' const mockNotifications: Notification[] = [ { id: 1, - type: 'PUBLISHEDDS', + type: NotificationType.PUBLISHEDDS, subjectText: 'Test notification', messageText: 'Test message', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false }, { id: 2, - type: 'ASSIGNROLE', + type: NotificationType.ASSIGNROLE, subjectText: 'Role assignment', messageText: 'Role assigned', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false } ] diff --git a/test/unit/notifications/GetAllNotificationsByUser.test.ts b/test/unit/notifications/GetAllNotificationsByUser.test.ts index 5779f303..7df67bca 100644 --- a/test/unit/notifications/GetAllNotificationsByUser.test.ts +++ b/test/unit/notifications/GetAllNotificationsByUser.test.ts @@ -1,21 +1,26 @@ import { GetAllNotificationsByUser } from '../../../src/notifications/domain/useCases/GetAllNotificationsByUser' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' -import { Notification } from '../../../src/notifications/domain/models/Notification' +import { + Notification, + NotificationType +} from '../../../src/notifications/domain/models/Notification' const mockNotifications: Notification[] = [ { id: 1, - type: 'PUBLISHEDDS', + type: NotificationType.PUBLISHEDDS, subjectText: 'Test notification', messageText: 'Test message', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false }, { id: 2, - type: 'ASSIGNROLE', + type: NotificationType.ASSIGNROLE, subjectText: 'Role assignment', messageText: 'Role assigned', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false } ] From 541e892199533ba870b38565c9064a393d19d226 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 18 Aug 2025 12:06:00 -0400 Subject: [PATCH 067/123] feat: display as read --- docs/useCases.md | 44 +++++++++++++++ .../domain/models/Notification.ts | 2 +- .../repositories/INotificationsRepository.ts | 2 + .../domain/useCases/GetUnreadCount.ts | 14 +++++ .../domain/useCases/MarkAsRead.ts | 14 +++++ src/notifications/index.ts | 6 +- .../repositories/NotificationsRepository.ts | 16 +++++- .../GetAllNotificationsByUser.test.ts | 7 --- .../notifications/GetUnreadCount.test.ts | 10 ++++ .../notifications/MarkAsRead.test.ts | 1 + .../NotificationsRepository.test.ts | 55 +++++++++++++------ .../unit/notifications/GetUnreadCount.test.ts | 25 +++++++++ test/unit/notifications/MarkAsRead.test.ts | 24 ++++++++ 13 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 src/notifications/domain/useCases/GetUnreadCount.ts create mode 100644 src/notifications/domain/useCases/MarkAsRead.ts create mode 100644 test/functional/notifications/GetUnreadCount.test.ts create mode 100644 test/functional/notifications/MarkAsRead.test.ts create mode 100644 test/unit/notifications/GetUnreadCount.test.ts create mode 100644 test/unit/notifications/MarkAsRead.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index fec2a009..faaadb16 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -91,6 +91,8 @@ The different use cases currently available in the package are classified below, - [Notifications](#Notifications) - [Get All Notifications by User](#get-all-notifications-by-user) - [Delete Notification](#delete-notification) + - [Get Unread Count](#get-unread-count) + - [Mark As Read](#mark-as-read) - [Search](#Search) - [Get Search Services](#get-search-services) @@ -2143,6 +2145,48 @@ deleteNotification.execute(notificationId: number).then(() => { _See [use case](../src/notifications/domain/useCases/DeleteNotification.ts) implementation_. +#### Get Unread Count + +Returns the number of unread notifications for the current authenticated user. + +##### Example call: + +```typescript +import { getUnreadCount } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getUnreadCount.execute().then((count: number) => { + console.log(`You have ${count} unread notifications`) +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/GetUnreadCount.ts) implementation_. + +#### Mark As Read + +Marks a specific notification as read for the current authenticated user. This operation is idempotent - marking an already-read notification as read will not cause an error. + +##### Example call: + +```typescript +import { markAsRead } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const notificationId = 123 + +markAsRead.execute(notificationId).then(() => { + console.log('Notification marked as read') +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/MarkAsRead.ts) implementation_. + ## Search #### Get Search Services diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index 26f41d0d..04243636 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -48,7 +48,7 @@ export interface Notification { subjectText?: string messageText?: string sentTimestamp: string - displayAsRead?: boolean + displayAsRead: boolean installationBrandName?: string userGuidesBaseUrl?: string userGuidesVersion?: string diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index c414f8b8..d2e51467 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -3,4 +3,6 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise deleteNotification(notificationId: number): Promise + getUnreadCount(): Promise + markAsRead(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/GetUnreadCount.ts b/src/notifications/domain/useCases/GetUnreadCount.ts new file mode 100644 index 00000000..a7940ebb --- /dev/null +++ b/src/notifications/domain/useCases/GetUnreadCount.ts @@ -0,0 +1,14 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +export class GetUnreadCount implements UseCase { + private notificationsRepository: INotificationsRepository + + constructor(notificationsRepository: INotificationsRepository) { + this.notificationsRepository = notificationsRepository + } + + async execute(): Promise { + return await this.notificationsRepository.getUnreadCount() + } +} diff --git a/src/notifications/domain/useCases/MarkAsRead.ts b/src/notifications/domain/useCases/MarkAsRead.ts new file mode 100644 index 00000000..bb7f849d --- /dev/null +++ b/src/notifications/domain/useCases/MarkAsRead.ts @@ -0,0 +1,14 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +export class MarkAsRead implements UseCase { + private notificationsRepository: INotificationsRepository + + constructor(notificationsRepository: INotificationsRepository) { + this.notificationsRepository = notificationsRepository + } + + async execute(notificationId: number): Promise { + return await this.notificationsRepository.markAsRead(notificationId) + } +} diff --git a/src/notifications/index.ts b/src/notifications/index.ts index 8e97bfcc..1c819dbc 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -1,12 +1,16 @@ import { NotificationsRepository } from './infra/repositories/NotificationsRepository' import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' import { DeleteNotification } from './domain/useCases/DeleteNotification' +import { GetUnreadCount } from './domain/useCases/GetUnreadCount' +import { MarkAsRead } from './domain/useCases/MarkAsRead' const notificationsRepository = new NotificationsRepository() const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) const deleteNotification = new DeleteNotification(notificationsRepository) +const getUnreadCount = new GetUnreadCount(notificationsRepository) +const markAsRead = new MarkAsRead(notificationsRepository) -export { getAllNotificationsByUser, deleteNotification } +export { getAllNotificationsByUser, deleteNotification, getUnreadCount, markAsRead } export { Notification, NotificationType, RoleAssignment } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index b298d040..8d677721 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -24,9 +24,23 @@ export class NotificationsRepository extends ApiRepository implements INotificat return this.doDelete( this.buildApiEndpoint(this.notificationsResourceName, notificationId.toString()) ) - .then(() => {}) + .then(() => undefined) .catch((error) => { throw error }) } + + public async getUnreadCount(): Promise { + return this.doGet( + this.buildApiEndpoint(this.notificationsResourceName, 'unreadCount'), + true + ).then((response) => response.data.data.unreadCount as number) + } + + public async markAsRead(notificationId: number): Promise { + return this.doPut( + this.buildApiEndpoint(this.notificationsResourceName, 'markAsRead', notificationId), + {} + ).then(() => undefined) + } } diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 11f9a33c..7ccd7ec1 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -33,12 +33,5 @@ describe('execute', () => { expect(notifications[0]).toHaveProperty('type') expect(notifications[0]).toHaveProperty('sentTimestamp') expect(notifications[0]).toHaveProperty('displayAsRead') - expect(notifications[0]).toHaveProperty('roleAssignments') - expect(notifications[0].roleAssignments).toBeDefined() - expect(notifications[0].roleAssignments?.length).toBeGreaterThan(0) - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleId', 1) - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) }) }) diff --git a/test/functional/notifications/GetUnreadCount.test.ts b/test/functional/notifications/GetUnreadCount.test.ts new file mode 100644 index 00000000..cecb97c2 --- /dev/null +++ b/test/functional/notifications/GetUnreadCount.test.ts @@ -0,0 +1,10 @@ +import { getUnreadCount } from '../../../src/notifications' + +describe('GetUnreadCount', () => { + test('should return unread count', async () => { + const unreadCount = await getUnreadCount.execute() + + expect(typeof unreadCount).toBe('number') + expect(unreadCount).toBeGreaterThanOrEqual(0) + }) +}) diff --git a/test/functional/notifications/MarkAsRead.test.ts b/test/functional/notifications/MarkAsRead.test.ts new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/test/functional/notifications/MarkAsRead.test.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index d29dbb49..809d201f 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -9,11 +9,7 @@ import { NotificationType } from '../../../src/notifications/domain/models/Notification' import { createDataset, CreatedDatasetIdentifiers } from '../../../src/datasets' -import { - deletePublishedDatasetViaApi, - publishDatasetViaApi, - waitForNoLocks -} from '../../testHelpers/datasets/datasetHelper' +import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' describe('NotificationsRepository', () => { @@ -28,10 +24,6 @@ describe('NotificationsRepository', () => { ) }) - afterAll(async () => { - await deletePublishedDatasetViaApi(testDatasetIds.persistentId) - }) - test('should return notifications after creating and publishing a dataset', async () => { // Create a dataset and publish it so that a notification of Dataset published is created testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) @@ -81,9 +73,7 @@ describe('NotificationsRepository', () => { test('should throw error when trying to delete notification with wrong ID', async () => { const nonExistentNotificationId = 99999 - const expectedError = new WriteError( - `[404] Notification ${nonExistentNotificationId} not found.` - ) + const expectedError = new WriteError() await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) @@ -102,10 +92,10 @@ describe('NotificationsRepository', () => { expect(notification).toHaveProperty('roleAssignments') expect(notification.roleAssignments).toBeDefined() expect(notification.roleAssignments?.length).toBeGreaterThan(0) - expect(notification.roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') - expect(notification.roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') - expect(notification.roleAssignments?.[0]).toHaveProperty('roleId', 1) - expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) + expect(notification.roleAssignments?.[0]).toHaveProperty('roleName') + expect(notification.roleAssignments?.[0]).toHaveProperty('assignee') + expect(notification.roleAssignments?.[0]).toHaveProperty('roleId') + expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) test('should return array when inAppNotificationFormat is false', async () => { @@ -113,4 +103,37 @@ describe('NotificationsRepository', () => { expect(Array.isArray(notifications)).toBe(true) }) + + test('should return unread count', async () => { + const unreadCount = await sut.getUnreadCount() + + console.log('unreadCount', unreadCount) + expect(typeof unreadCount).toBe('number') + expect(unreadCount).toBeGreaterThanOrEqual(0) + }) + + test('should mark notification as read successfully', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser() + + expect(notifications.length).toBeGreaterThan(0) + + const unreadNotification = notifications[0] + + await expect(sut.markAsRead(unreadNotification.id)).resolves.toBeUndefined() + + const updatedNotifications: Notification[] = await sut.getAllNotificationsByUser() + const updatedNotification = updatedNotifications.find((n) => n.id === unreadNotification.id) + + expect(updatedNotification?.displayAsRead).toBe(true) + }) + + test('should throw error when marking non-existent notification as read', async () => { + const nonExistentNotificationId = 99999 + + const expectedError = new WriteError( + `[404] Notification ${nonExistentNotificationId} not found.` + ) + + await expect(sut.markAsRead(nonExistentNotificationId)).rejects.toThrow(expectedError) + }) }) diff --git a/test/unit/notifications/GetUnreadCount.test.ts b/test/unit/notifications/GetUnreadCount.test.ts new file mode 100644 index 00000000..af049c56 --- /dev/null +++ b/test/unit/notifications/GetUnreadCount.test.ts @@ -0,0 +1,25 @@ +import { GetUnreadCount } from '../../../src/notifications/domain/useCases/GetUnreadCount' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { ReadError } from '../../../src' + +describe('GetUnreadCount', () => { + test('should return unread count from repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + + notificationsRepositoryStub.getUnreadCount = jest.fn().mockResolvedValue(5) + const sut = new GetUnreadCount(notificationsRepositoryStub) + + const result = await sut.execute() + + expect(notificationsRepositoryStub.getUnreadCount).toHaveBeenCalledWith() + expect(result).toBe(5) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getUnreadCount = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetUnreadCount(notificationsRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/notifications/MarkAsRead.test.ts b/test/unit/notifications/MarkAsRead.test.ts new file mode 100644 index 00000000..41616a11 --- /dev/null +++ b/test/unit/notifications/MarkAsRead.test.ts @@ -0,0 +1,24 @@ +import { MarkAsRead } from '../../../src/notifications/domain/useCases/MarkAsRead' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { ReadError } from '../../../src' + +describe('MarkAsRead', () => { + test('should mark notification as read in repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + + notificationsRepositoryStub.markAsRead = jest.fn().mockResolvedValue(undefined) + const sut = new MarkAsRead(notificationsRepositoryStub) + + await sut.execute(123) + + expect(notificationsRepositoryStub.markAsRead).toHaveBeenCalledWith(123) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.markAsRead = jest.fn().mockRejectedValue(new ReadError()) + const sut = new MarkAsRead(notificationsRepositoryStub) + + await expect(sut.execute(123)).rejects.toThrow(ReadError) + }) +}) From de95d37e63c4f0f20f51ebbe31df3620a8564b5d Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 18 Aug 2025 12:20:50 -0400 Subject: [PATCH 068/123] fix on tests --- .../domain/useCases/GetUnreadCount.ts | 5 +++ .../domain/useCases/MarkAsRead.ts | 6 ++++ .../notifications/GetUnreadCount.test.ts | 10 ------ .../notifications/MarkAsRead.test.ts | 1 - .../NotificationsRepository.test.ts | 35 ++++++++++++------- 5 files changed, 34 insertions(+), 23 deletions(-) delete mode 100644 test/functional/notifications/GetUnreadCount.test.ts delete mode 100644 test/functional/notifications/MarkAsRead.test.ts diff --git a/src/notifications/domain/useCases/GetUnreadCount.ts b/src/notifications/domain/useCases/GetUnreadCount.ts index a7940ebb..fff13899 100644 --- a/src/notifications/domain/useCases/GetUnreadCount.ts +++ b/src/notifications/domain/useCases/GetUnreadCount.ts @@ -8,6 +8,11 @@ export class GetUnreadCount implements UseCase { this.notificationsRepository = notificationsRepository } + /** + * Use case for retrieving the number of unread notifications for the current user. + * + * @returns {Promise} - A promise that resolves to the number of unread notifications. + */ async execute(): Promise { return await this.notificationsRepository.getUnreadCount() } diff --git a/src/notifications/domain/useCases/MarkAsRead.ts b/src/notifications/domain/useCases/MarkAsRead.ts index bb7f849d..2789713e 100644 --- a/src/notifications/domain/useCases/MarkAsRead.ts +++ b/src/notifications/domain/useCases/MarkAsRead.ts @@ -8,6 +8,12 @@ export class MarkAsRead implements UseCase { this.notificationsRepository = notificationsRepository } + /** + * Use case for marking a notification as read. + * + * @param notificationId - The ID of the notification to mark as read. + * @returns {Promise} - A promise that resolves when the notification is marked as read. + */ async execute(notificationId: number): Promise { return await this.notificationsRepository.markAsRead(notificationId) } diff --git a/test/functional/notifications/GetUnreadCount.test.ts b/test/functional/notifications/GetUnreadCount.test.ts deleted file mode 100644 index cecb97c2..00000000 --- a/test/functional/notifications/GetUnreadCount.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getUnreadCount } from '../../../src/notifications' - -describe('GetUnreadCount', () => { - test('should return unread count', async () => { - const unreadCount = await getUnreadCount.execute() - - expect(typeof unreadCount).toBe('number') - expect(unreadCount).toBeGreaterThanOrEqual(0) - }) -}) diff --git a/test/functional/notifications/MarkAsRead.test.ts b/test/functional/notifications/MarkAsRead.test.ts deleted file mode 100644 index 0519ecba..00000000 --- a/test/functional/notifications/MarkAsRead.test.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 809d201f..2b7e7e13 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -73,7 +73,9 @@ describe('NotificationsRepository', () => { test('should throw error when trying to delete notification with wrong ID', async () => { const nonExistentNotificationId = 99999 - const expectedError = new WriteError() + const expectedError = new WriteError( + `[404] Notification ${nonExistentNotificationId} not found.` + ) await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) @@ -84,18 +86,28 @@ describe('NotificationsRepository', () => { const notification = notifications[0] expect(notification).toHaveProperty('id') expect(notification).toHaveProperty('type') - expect(notification.type).toBe(NotificationType.ASSIGNROLE) expect(notification).toHaveProperty('sentTimestamp') expect(notification).toHaveProperty('displayAsRead') - expect(notification).toHaveProperty('dataverseDisplayName') - - expect(notification).toHaveProperty('roleAssignments') - expect(notification.roleAssignments).toBeDefined() - expect(notification.roleAssignments?.length).toBeGreaterThan(0) - expect(notification.roleAssignments?.[0]).toHaveProperty('roleName') - expect(notification.roleAssignments?.[0]).toHaveProperty('assignee') - expect(notification.roleAssignments?.[0]).toHaveProperty('roleId') - expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId') + }) + + test('should find notification with ASSIGNROLE type', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + + // Find a notification with ASSIGNROLE type + const assignRoleNotification = notifications.find((n) => n.type === NotificationType.ASSIGNROLE) + + expect(assignRoleNotification).toBeDefined() + expect(assignRoleNotification?.type).toBe(NotificationType.ASSIGNROLE) + expect(assignRoleNotification?.sentTimestamp).toBeDefined() + expect(assignRoleNotification?.displayAsRead).toBeDefined() + expect(assignRoleNotification?.dataverseDisplayName).toBeDefined() + + expect(assignRoleNotification?.roleAssignments).toBeDefined() + expect(assignRoleNotification?.roleAssignments?.length).toBeGreaterThan(0) + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('roleName') + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('assignee') + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('roleId') + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) test('should return array when inAppNotificationFormat is false', async () => { @@ -107,7 +119,6 @@ describe('NotificationsRepository', () => { test('should return unread count', async () => { const unreadCount = await sut.getUnreadCount() - console.log('unreadCount', unreadCount) expect(typeof unreadCount).toBe('number') expect(unreadCount).toBeGreaterThanOrEqual(0) }) From e087a57dffa803cf737f25c677cc005e8b8c21e4 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 18 Aug 2025 21:24:44 -0400 Subject: [PATCH 069/123] update env. variables --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index d7d1957e..e7b54bde 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=11648-notifications-api-extension +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 0c1de29e1f83d4026b7398a4b794ae6d3a996657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 21 Aug 2025 10:10:05 -0300 Subject: [PATCH 070/123] fix: avoid incorrect split causing value part to be truncated --- .../infra/repositories/CollectionsRepository.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 18e3c474..704367e2 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -357,10 +357,13 @@ export class CollectionsRepository extends ApiRepository implements ICollections if (collectionSearchCriteria?.filterQueries) { collectionSearchCriteria.filterQueries.forEach((filterQuery) => { - const [filterQueryKey, filterQueryValue] = filterQuery.split(':') + const idx = filterQuery.indexOf(':') + if (idx === -1) return // Invalid filter query, skip it - const filterQueryValueWithQuotes = `"${filterQueryValue}"` + const filterQueryKey = filterQuery.substring(0, idx).trim() + const filterQueryValue = filterQuery.substring(idx + 1).trim() + const filterQueryValueWithQuotes = `"${filterQueryValue}"` const filterQueryToSet = `${filterQueryKey}:${filterQueryValueWithQuotes}` queryParams.append(GetCollectionItemsQueryParams.FILTERQUERY, filterQueryToSet) From b61f8cf8b081f6a753f165ade93ca4b3e7e3107f Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 22 Aug 2025 09:52:03 -0400 Subject: [PATCH 071/123] fix: update naming and test --- docs/useCases.md | 12 ++--- .../domain/models/Notification.ts | 4 +- .../repositories/INotificationsRepository.ts | 4 +- ...ount.ts => GetUnreadNotificationsCount.ts} | 4 +- ...arkAsRead.ts => MarkNotificationAsRead.ts} | 4 +- src/notifications/index.ts | 15 ++++-- .../repositories/NotificationsRepository.ts | 17 +++++-- .../infra/transformers/NotificationPayload.ts | 30 +++++++++++ .../NotificationsRepository.test.ts | 50 ++++++++++++++++--- .../unit/notifications/GetUnreadCount.test.ts | 16 +++--- test/unit/notifications/MarkAsRead.test.ts | 16 +++--- 11 files changed, 129 insertions(+), 43 deletions(-) rename src/notifications/domain/useCases/{GetUnreadCount.ts => GetUnreadNotificationsCount.ts} (80%) rename src/notifications/domain/useCases/{MarkAsRead.ts => MarkNotificationAsRead.ts} (81%) create mode 100644 src/notifications/infra/transformers/NotificationPayload.ts diff --git a/docs/useCases.md b/docs/useCases.md index faaadb16..bb3a2951 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -2152,18 +2152,18 @@ Returns the number of unread notifications for the current authenticated user. ##### Example call: ```typescript -import { getUnreadCount } from '@iqss/dataverse-client-javascript' +import { getUnreadNotificationsCount } from '@iqss/dataverse-client-javascript' /* ... */ -getUnreadCount.execute().then((count: number) => { +getUnreadNotificationsCount.execute().then((count: number) => { console.log(`You have ${count} unread notifications`) }) /* ... */ ``` -_See [use case](../src/notifications/domain/useCases/GetUnreadCount.ts) implementation_. +_See [use case](../src/notifications/domain/useCases/GetUnreadNotificationsCount.ts) implementation_. #### Mark As Read @@ -2172,20 +2172,20 @@ Marks a specific notification as read for the current authenticated user. This o ##### Example call: ```typescript -import { markAsRead } from '@iqss/dataverse-client-javascript' +import { markNotificationAsRead } from '@iqss/dataverse-client-javascript' /* ... */ const notificationId = 123 -markAsRead.execute(notificationId).then(() => { +markNotificationAsRead.execute(notificationId).then(() => { console.log('Notification marked as read') }) /* ... */ ``` -_See [use case](../src/notifications/domain/useCases/MarkAsRead.ts) implementation_. +_See [use case](../src/notifications/domain/useCases/MarkNotificationAsRead.ts) implementation_. ## Search diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index 04243636..001d0933 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -54,8 +54,8 @@ export interface Notification { userGuidesVersion?: string userGuidesSectionPath?: string roleAssignments?: RoleAssignment[] - dataverseAlias?: string - dataverseDisplayName?: string + collectionAlias?: string + collectionDisplayName?: string datasetPersistentIdentifier?: string datasetDisplayName?: string ownerPersistentIdentifier?: string diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index d2e51467..9392c543 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -3,6 +3,6 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise deleteNotification(notificationId: number): Promise - getUnreadCount(): Promise - markAsRead(notificationId: number): Promise + getUnreadNotificationsCount(): Promise + markNotificationAsRead(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/GetUnreadCount.ts b/src/notifications/domain/useCases/GetUnreadNotificationsCount.ts similarity index 80% rename from src/notifications/domain/useCases/GetUnreadCount.ts rename to src/notifications/domain/useCases/GetUnreadNotificationsCount.ts index fff13899..2e59c55e 100644 --- a/src/notifications/domain/useCases/GetUnreadCount.ts +++ b/src/notifications/domain/useCases/GetUnreadNotificationsCount.ts @@ -1,7 +1,7 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { INotificationsRepository } from '../repositories/INotificationsRepository' -export class GetUnreadCount implements UseCase { +export class GetUnreadNotificationsCount implements UseCase { private notificationsRepository: INotificationsRepository constructor(notificationsRepository: INotificationsRepository) { @@ -14,6 +14,6 @@ export class GetUnreadCount implements UseCase { * @returns {Promise} - A promise that resolves to the number of unread notifications. */ async execute(): Promise { - return await this.notificationsRepository.getUnreadCount() + return await this.notificationsRepository.getUnreadNotificationsCount() } } diff --git a/src/notifications/domain/useCases/MarkAsRead.ts b/src/notifications/domain/useCases/MarkNotificationAsRead.ts similarity index 81% rename from src/notifications/domain/useCases/MarkAsRead.ts rename to src/notifications/domain/useCases/MarkNotificationAsRead.ts index 2789713e..017be28c 100644 --- a/src/notifications/domain/useCases/MarkAsRead.ts +++ b/src/notifications/domain/useCases/MarkNotificationAsRead.ts @@ -1,7 +1,7 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { INotificationsRepository } from '../repositories/INotificationsRepository' -export class MarkAsRead implements UseCase { +export class MarkNotificationAsRead implements UseCase { private notificationsRepository: INotificationsRepository constructor(notificationsRepository: INotificationsRepository) { @@ -15,6 +15,6 @@ export class MarkAsRead implements UseCase { * @returns {Promise} - A promise that resolves when the notification is marked as read. */ async execute(notificationId: number): Promise { - return await this.notificationsRepository.markAsRead(notificationId) + return await this.notificationsRepository.markNotificationAsRead(notificationId) } } diff --git a/src/notifications/index.ts b/src/notifications/index.ts index 1c819dbc..3075ee90 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -1,16 +1,21 @@ import { NotificationsRepository } from './infra/repositories/NotificationsRepository' import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' import { DeleteNotification } from './domain/useCases/DeleteNotification' -import { GetUnreadCount } from './domain/useCases/GetUnreadCount' -import { MarkAsRead } from './domain/useCases/MarkAsRead' +import { GetUnreadNotificationsCount } from './domain/useCases/GetUnreadNotificationsCount' +import { MarkNotificationAsRead } from './domain/useCases/MarkNotificationAsRead' const notificationsRepository = new NotificationsRepository() const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) const deleteNotification = new DeleteNotification(notificationsRepository) -const getUnreadCount = new GetUnreadCount(notificationsRepository) -const markAsRead = new MarkAsRead(notificationsRepository) +const getUnreadNotificationsCount = new GetUnreadNotificationsCount(notificationsRepository) +const markNotificationAsRead = new MarkNotificationAsRead(notificationsRepository) -export { getAllNotificationsByUser, deleteNotification, getUnreadCount, markAsRead } +export { + getAllNotificationsByUser, + deleteNotification, + getUnreadNotificationsCount, + markNotificationAsRead +} export { Notification, NotificationType, RoleAssignment } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 8d677721..f310c34a 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -1,6 +1,7 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { INotificationsRepository } from '../../domain/repositories/INotificationsRepository' import { Notification } from '../../domain/models/Notification' +import { NotificationPayload } from '../transformers/NotificationPayload' export class NotificationsRepository extends ApiRepository implements INotificationsRepository { private readonly notificationsResourceName: string = 'notifications' @@ -14,7 +15,17 @@ export class NotificationsRepository extends ApiRepository implements INotificat true, queryParams ) - .then((response) => response.data.data.notifications as Notification[]) + .then((response) => { + const notifications = response.data.data.notifications + return notifications.map((notification: NotificationPayload) => { + const { dataverseDisplayName, dataverseAlias, ...restNotification } = notification + return { + ...restNotification, + ...(dataverseDisplayName && { collectionDisplayName: dataverseDisplayName }), + ...(dataverseAlias && { collectionAlias: dataverseAlias }) + } + }) as Notification[] + }) .catch((error) => { throw error }) @@ -30,14 +41,14 @@ export class NotificationsRepository extends ApiRepository implements INotificat }) } - public async getUnreadCount(): Promise { + public async getUnreadNotificationsCount(): Promise { return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'unreadCount'), true ).then((response) => response.data.data.unreadCount as number) } - public async markAsRead(notificationId: number): Promise { + public async markNotificationAsRead(notificationId: number): Promise { return this.doPut( this.buildApiEndpoint(this.notificationsResourceName, 'markAsRead', notificationId), {} diff --git a/src/notifications/infra/transformers/NotificationPayload.ts b/src/notifications/infra/transformers/NotificationPayload.ts new file mode 100644 index 00000000..96d381ac --- /dev/null +++ b/src/notifications/infra/transformers/NotificationPayload.ts @@ -0,0 +1,30 @@ +import { RoleAssignment } from '../../domain/models/Notification' + +export interface NotificationPayload { + id: number + type: string + subjectText?: string + messageText?: string + sentTimestamp: string + displayAsRead: boolean + installationBrandName?: string + userGuidesBaseUrl?: string + userGuidesVersion?: string + userGuidesSectionPath?: string + roleAssignments?: RoleAssignment[] + dataverseAlias?: string + dataverseDisplayName?: string + datasetPersistentIdentifier?: string + datasetDisplayName?: string + ownerPersistentIdentifier?: string + ownerAlias?: string + ownerDisplayName?: string + requestorFirstName?: string + requestorLastName?: string + requestorEmail?: string + dataFileId?: number + dataFileDisplayName?: string + currentCurationStatus?: string + additionalInfo?: string + objectDeleted?: boolean +} diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 2b7e7e13..5333e48d 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -11,6 +11,11 @@ import { import { createDataset, CreatedDatasetIdentifiers } from '../../../src/datasets' import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' +import { createCollection } from '../../../src/collections' +import { + createCollectionDTO, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' describe('NotificationsRepository', () => { const sut: NotificationsRepository = new NotificationsRepository() @@ -90,17 +95,18 @@ describe('NotificationsRepository', () => { expect(notification).toHaveProperty('displayAsRead') }) - test('should find notification with ASSIGNROLE type', async () => { + test('should find notification with ASSIGNROLE type that has not been deleted', async () => { const notifications: Notification[] = await sut.getAllNotificationsByUser(true) - // Find a notification with ASSIGNROLE type - const assignRoleNotification = notifications.find((n) => n.type === NotificationType.ASSIGNROLE) + const assignRoleNotification = notifications.find( + (n) => n.type === NotificationType.ASSIGNROLE && !n.objectDeleted + ) expect(assignRoleNotification).toBeDefined() expect(assignRoleNotification?.type).toBe(NotificationType.ASSIGNROLE) expect(assignRoleNotification?.sentTimestamp).toBeDefined() expect(assignRoleNotification?.displayAsRead).toBeDefined() - expect(assignRoleNotification?.dataverseDisplayName).toBeDefined() + expect(assignRoleNotification?.collectionDisplayName).toBeDefined() expect(assignRoleNotification?.roleAssignments).toBeDefined() expect(assignRoleNotification?.roleAssignments?.length).toBeGreaterThan(0) @@ -110,6 +116,34 @@ describe('NotificationsRepository', () => { expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) + test('should create a collection and find the notification with CREATEDV type', async () => { + const testCollectionAlias = 'test-notification-collection' + const createdCollectionId = await createCollection.execute( + createCollectionDTO(testCollectionAlias) + ) + + expect(createdCollectionId).toBeDefined() + expect(createdCollectionId).toBeGreaterThan(0) + + const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + expect(Array.isArray(notifications)).toBe(true) + expect(notifications.length).toBeGreaterThan(0) + + const createdvNotification = notifications.find( + (n) => n.collectionAlias === testCollectionAlias + ) + + expect(createdvNotification).toBeDefined() + expect(createdvNotification?.type).toBe(NotificationType.CREATEDV) + expect(createdvNotification?.collectionAlias).toBe(testCollectionAlias) + expect(createdvNotification?.sentTimestamp).toBeDefined() + expect(createdvNotification?.displayAsRead).toBe(false) + expect(createdvNotification?.collectionDisplayName).toBe('Test Collection') + expect(createdvNotification?.collectionAlias).toBe(testCollectionAlias) + + await deleteCollectionViaApi(testCollectionAlias) + }) + test('should return array when inAppNotificationFormat is false', async () => { const notifications: Notification[] = await sut.getAllNotificationsByUser(false) @@ -117,7 +151,7 @@ describe('NotificationsRepository', () => { }) test('should return unread count', async () => { - const unreadCount = await sut.getUnreadCount() + const unreadCount = await sut.getUnreadNotificationsCount() expect(typeof unreadCount).toBe('number') expect(unreadCount).toBeGreaterThanOrEqual(0) @@ -130,7 +164,7 @@ describe('NotificationsRepository', () => { const unreadNotification = notifications[0] - await expect(sut.markAsRead(unreadNotification.id)).resolves.toBeUndefined() + await expect(sut.markNotificationAsRead(unreadNotification.id)).resolves.toBeUndefined() const updatedNotifications: Notification[] = await sut.getAllNotificationsByUser() const updatedNotification = updatedNotifications.find((n) => n.id === unreadNotification.id) @@ -145,6 +179,8 @@ describe('NotificationsRepository', () => { `[404] Notification ${nonExistentNotificationId} not found.` ) - await expect(sut.markAsRead(nonExistentNotificationId)).rejects.toThrow(expectedError) + await expect(sut.markNotificationAsRead(nonExistentNotificationId)).rejects.toThrow( + expectedError + ) }) }) diff --git a/test/unit/notifications/GetUnreadCount.test.ts b/test/unit/notifications/GetUnreadCount.test.ts index af049c56..7a54f9e9 100644 --- a/test/unit/notifications/GetUnreadCount.test.ts +++ b/test/unit/notifications/GetUnreadCount.test.ts @@ -1,24 +1,26 @@ -import { GetUnreadCount } from '../../../src/notifications/domain/useCases/GetUnreadCount' +import { GetUnreadNotificationsCount } from '../../../src/notifications/domain/useCases/GetUnreadNotificationsCount' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' import { ReadError } from '../../../src' -describe('GetUnreadCount', () => { +describe('GetUnreadNotificationsCount', () => { test('should return unread count from repository', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.getUnreadCount = jest.fn().mockResolvedValue(5) - const sut = new GetUnreadCount(notificationsRepositoryStub) + notificationsRepositoryStub.getUnreadNotificationsCount = jest.fn().mockResolvedValue(5) + const sut = new GetUnreadNotificationsCount(notificationsRepositoryStub) const result = await sut.execute() - expect(notificationsRepositoryStub.getUnreadCount).toHaveBeenCalledWith() + expect(notificationsRepositoryStub.getUnreadNotificationsCount).toHaveBeenCalledWith() expect(result).toBe(5) }) test('should throw error when repository throws error', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.getUnreadCount = jest.fn().mockRejectedValue(new ReadError()) - const sut = new GetUnreadCount(notificationsRepositoryStub) + notificationsRepositoryStub.getUnreadNotificationsCount = jest + .fn() + .mockRejectedValue(new ReadError()) + const sut = new GetUnreadNotificationsCount(notificationsRepositoryStub) await expect(sut.execute()).rejects.toThrow(ReadError) }) diff --git a/test/unit/notifications/MarkAsRead.test.ts b/test/unit/notifications/MarkAsRead.test.ts index 41616a11..a1f57a59 100644 --- a/test/unit/notifications/MarkAsRead.test.ts +++ b/test/unit/notifications/MarkAsRead.test.ts @@ -1,23 +1,25 @@ -import { MarkAsRead } from '../../../src/notifications/domain/useCases/MarkAsRead' +import { MarkNotificationAsRead } from '../../../src/notifications/domain/useCases/MarkNotificationAsRead' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' import { ReadError } from '../../../src' -describe('MarkAsRead', () => { +describe('MarkNotificationAsRead', () => { test('should mark notification as read in repository', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.markAsRead = jest.fn().mockResolvedValue(undefined) - const sut = new MarkAsRead(notificationsRepositoryStub) + notificationsRepositoryStub.markNotificationAsRead = jest.fn().mockResolvedValue(undefined) + const sut = new MarkNotificationAsRead(notificationsRepositoryStub) await sut.execute(123) - expect(notificationsRepositoryStub.markAsRead).toHaveBeenCalledWith(123) + expect(notificationsRepositoryStub.markNotificationAsRead).toHaveBeenCalledWith(123) }) test('should throw error when repository throws error', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.markAsRead = jest.fn().mockRejectedValue(new ReadError()) - const sut = new MarkAsRead(notificationsRepositoryStub) + notificationsRepositoryStub.markNotificationAsRead = jest + .fn() + .mockRejectedValue(new ReadError()) + const sut = new MarkNotificationAsRead(notificationsRepositoryStub) await expect(sut.execute(123)).rejects.toThrow(ReadError) }) From d576136d1125c3382f8511e06c932b5b1bf8150f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 22 Aug 2025 16:50:49 -0300 Subject: [PATCH 072/123] feat: external tool use cases --- docs/useCases.md | 83 +++++++++++++++++++ .../domain/dtos/GetExternalToolDTO.ts | 9 ++ .../domain/models/ExternalTool.ts | 33 ++++++++ .../repositories/IExternalToolsRepository.ts | 16 ++++ .../useCases/GetDatasetExternalToolUrl.ts | 34 ++++++++ .../domain/useCases/GetExternalTools.ts | 20 +++++ .../domain/useCases/GetFileExternalToolUrl.ts | 34 ++++++++ src/externalTools/index.ts | 25 ++++++ .../infra/ExternalToolsRepository.ts | 53 ++++++++++++ .../infra/transformers/ExternalToolPayload.ts | 19 +++++ .../datasetExternalToolTransformer.ts | 17 ++++ .../transformers/externalToolsTransformer.ts | 19 +++++ .../fileExternalToolTransformer.ts | 17 ++++ src/index.ts | 1 + 14 files changed, 380 insertions(+) create mode 100644 src/externalTools/domain/dtos/GetExternalToolDTO.ts create mode 100644 src/externalTools/domain/models/ExternalTool.ts create mode 100644 src/externalTools/domain/repositories/IExternalToolsRepository.ts create mode 100644 src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts create mode 100644 src/externalTools/domain/useCases/GetExternalTools.ts create mode 100644 src/externalTools/domain/useCases/GetFileExternalToolUrl.ts create mode 100644 src/externalTools/index.ts create mode 100644 src/externalTools/infra/ExternalToolsRepository.ts create mode 100644 src/externalTools/infra/transformers/ExternalToolPayload.ts create mode 100644 src/externalTools/infra/transformers/datasetExternalToolTransformer.ts create mode 100644 src/externalTools/infra/transformers/externalToolsTransformer.ts create mode 100644 src/externalTools/infra/transformers/fileExternalToolTransformer.ts diff --git a/docs/useCases.md b/docs/useCases.md index 773c9122..a465df8f 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -93,6 +93,11 @@ The different use cases currently available in the package are classified below, - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) - [Search](#Search) - [Get Search Services](#get-search-services) +- [External Tools](#external-tools) + - [External Tools read use cases](#external-tools-read-use-cases) + - [Get External Tools](#get-external-tools) + - [Get Dataset External Tool Url](#get-dataset-external-tool-url) + - [Get File External Tool Url](#get-file-external-tool-url) ## Collections @@ -2142,3 +2147,81 @@ getSearchServices.execute().then((searchServices: SearchService[]) => { ``` _See [use case](../src/search/domain/useCases/GetSearchServices.ts) implementation_. + +## External Tools + +### External Tools Read Use Cases + +#### Get External Tools + +Returns an array of [ExternalTool](../src/externalTools/domain/models/ExternalTool.ts) objects, which represent the external tools available in the installation. + +##### Example call: + +```typescript +import { getExternalTools } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getExternalTools.execute().then((externalTools: ExternalTool[]) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/externalTools/domain/useCases/GetExternalTools.ts) implementation_. + +#### Get Dataset External Tool Url + +Returns an instance of [DatasetExternalToolUrl](../src/externalTools/domain/models/ExternalTool.ts), which contains the resolved URL for accessing an external tool that operates at the dataset level. + +##### Example call: + +```typescript +import { getDatasetExternalToolUrl } from '@iqss/dataverse-client-javascript' + +/* ... */ +const toolId = 1 +const datasetId = 2 +const getExternalToolDTO: GetExternalToolDTO = { + preview: true, + locale: 'en' +} + +getDatasetExternalToolUrl + .execute(toolId, datasetId, getExternalToolDTO) + .then((datasetExternalToolUrl: DatasetExternalToolUrl) => { + /* ... */ + }) +/* ... */ +``` + +_See [use case](../src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts) implementation_. + +#### Get File External Tool Url + +Returns an instance of [FileExternalToolUrl](../src/externalTools/domain/models/ExternalTool.ts), which contains the resolved URL for accessing an external tool that operates at the file level. + +##### Example call: + +```typescript +import { getFileExternalToolUrl } from '@iqss/dataverse-client-javascript' + +/* ... */ +const toolId = 1 +const fileId = 2 +const getExternalToolDTO: GetExternalToolDTO = { + preview: true, + locale: 'en' +} + +getFileExternalToolUrl + .execute(toolId, fileId, getExternalToolDTO) + .then((fileExternalToolUrl: FileExternalToolUrl) => { + /* ... */ + }) +/* ... */ +``` + +_See [use case](../src/externalTools/domain/useCases/GetfileExternalToolUrl.ts) implementation_. diff --git a/src/externalTools/domain/dtos/GetExternalToolDTO.ts b/src/externalTools/domain/dtos/GetExternalToolDTO.ts new file mode 100644 index 00000000..7e415d25 --- /dev/null +++ b/src/externalTools/domain/dtos/GetExternalToolDTO.ts @@ -0,0 +1,9 @@ +/** + * @property {boolean} preview - boolean flag to indicate if the request is for previewing the tool or not. + * @property {string} locale - string specifying the locale for internationalization + */ + +export interface GetExternalToolDTO { + preview: boolean + locale: string +} diff --git a/src/externalTools/domain/models/ExternalTool.ts b/src/externalTools/domain/models/ExternalTool.ts new file mode 100644 index 00000000..61955337 --- /dev/null +++ b/src/externalTools/domain/models/ExternalTool.ts @@ -0,0 +1,33 @@ +export interface ExternalTool { + id: number + displayName: string + description: string + types: ToolType[] + scope: ToolScope +} + +export enum ToolType { + Explore = 'explore', + Configure = 'configure', + Preview = 'preview', + Query = 'query' +} + +export enum ToolScope { + Dataset = 'dataset', + File = 'file' +} + +export interface DatasetExternalToolUrl { + toolUrlResolved: string + displayName: string + datasetId: number + preview: boolean +} + +export interface FileExternalToolUrl { + toolUrlResolved: string + displayName: string + fileId: number + preview: boolean +} diff --git a/src/externalTools/domain/repositories/IExternalToolsRepository.ts b/src/externalTools/domain/repositories/IExternalToolsRepository.ts new file mode 100644 index 00000000..9e163901 --- /dev/null +++ b/src/externalTools/domain/repositories/IExternalToolsRepository.ts @@ -0,0 +1,16 @@ +import { GetExternalToolDTO } from '../dtos/GetExternalToolDTO' +import { DatasetExternalToolUrl, ExternalTool, FileExternalToolUrl } from '../models/ExternalTool' + +export interface IExternalToolsRepository { + getExternalTools(): Promise + getDatasetExternalToolUrl( + datasetId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise + getFileExternalToolUrl( + fileId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise +} diff --git a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts b/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts new file mode 100644 index 00000000..ef6bfa2f --- /dev/null +++ b/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts @@ -0,0 +1,34 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GetExternalToolDTO } from '../dtos/GetExternalToolDTO' +import { DatasetExternalToolUrl } from '../models/ExternalTool' +import { IExternalToolsRepository } from '../repositories/IExternalToolsRepository' + +export class GetDatasetExternalToolUrl implements UseCase { + private externalToolsRepository: IExternalToolsRepository + + constructor(externalToolsRepository: IExternalToolsRepository) { + this.externalToolsRepository = externalToolsRepository + } + + /** + * Returns a DatasetExternalToolUrl object containing the resolved URL for accessing an external tool that operates at the dataset level. + * The URL includes necessary authentication tokens and parameters based on the user's permissions and the tool's configuration. + * Authentication is required for draft or deaccessioned datasets and the user must have ViewUnpublishedDataset permission. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {number} toolId - The identifier of the external tool. + * @param {GetExternalToolDTO} getExternalToolDTO - The GetExternalToolDTO object containing additional parameters for the request. + * @returns {Promise} + */ + async execute( + datasetId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise { + return await this.externalToolsRepository.getDatasetExternalToolUrl( + datasetId, + toolId, + getExternalToolDTO + ) + } +} diff --git a/src/externalTools/domain/useCases/GetExternalTools.ts b/src/externalTools/domain/useCases/GetExternalTools.ts new file mode 100644 index 00000000..09dd83f2 --- /dev/null +++ b/src/externalTools/domain/useCases/GetExternalTools.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ExternalTool } from '../models/ExternalTool' +import { IExternalToolsRepository } from '../repositories/IExternalToolsRepository' + +export class GetExternalTools implements UseCase { + private externalToolsRepository: IExternalToolsRepository + + constructor(externalToolsRepository: IExternalToolsRepository) { + this.externalToolsRepository = externalToolsRepository + } + + /** + * Returns a list containing all the external tools available in the installation. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.externalToolsRepository.getExternalTools() + } +} diff --git a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts b/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts new file mode 100644 index 00000000..60fdc8d6 --- /dev/null +++ b/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts @@ -0,0 +1,34 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GetExternalToolDTO } from '../dtos/GetExternalToolDTO' +import { FileExternalToolUrl } from '../models/ExternalTool' +import { IExternalToolsRepository } from '../repositories/IExternalToolsRepository' + +export class GetFileExternalToolUrl implements UseCase { + private externalToolsRepository: IExternalToolsRepository + + constructor(externalToolsRepository: IExternalToolsRepository) { + this.externalToolsRepository = externalToolsRepository + } + + /** + * Returns a FileExternalToolUrl object containing the resolved URL for accessing an external tool that operates at the file level. + * The URL includes necessary authentication tokens and parameters based on the user's permissions and the tool's configuration. + * Authentication is required for draft, restricted, embargoed, or expired (retention period) files, the user must have appropriate permissions. + * + * @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {number} toolId - The identifier of the external tool. + * @param {GetExternalToolDTO} getExternalToolDTO - The GetExternalToolDTO object containing additional parameters for the request. + * @returns {Promise} + */ + async execute( + fileId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise { + return await this.externalToolsRepository.getFileExternalToolUrl( + fileId, + toolId, + getExternalToolDTO + ) + } +} diff --git a/src/externalTools/index.ts b/src/externalTools/index.ts new file mode 100644 index 00000000..a9471ec3 --- /dev/null +++ b/src/externalTools/index.ts @@ -0,0 +1,25 @@ +import { GetDatasetExternalToolUrl } from './domain/useCases/GetDatasetExternalToolUrl' +import { GetExternalTools } from './domain/useCases/GetExternalTools' +import { GetFileExternalToolUrl } from './domain/useCases/GetFileExternalToolUrl' +import { ExternalToolsRepository } from './infra/ExternalToolsRepository' + +const externalToolsRepository = new ExternalToolsRepository() + +const getExternalTools = new GetExternalTools(externalToolsRepository) +const getDatasetExternalToolUrl = new GetDatasetExternalToolUrl(externalToolsRepository) +const getFileExternalToolUrl = new GetFileExternalToolUrl(externalToolsRepository) + +export { + getExternalTools, + getDatasetExternalToolUrl, + getFileExternalToolUrl, + externalToolsRepository +} + +export { + ExternalTool, + ToolScope, + ToolType, + DatasetExternalToolUrl, + FileExternalToolUrl +} from './domain/models/ExternalTool' diff --git a/src/externalTools/infra/ExternalToolsRepository.ts b/src/externalTools/infra/ExternalToolsRepository.ts new file mode 100644 index 00000000..f90a1a40 --- /dev/null +++ b/src/externalTools/infra/ExternalToolsRepository.ts @@ -0,0 +1,53 @@ +import { IExternalToolsRepository } from '../domain/repositories/IExternalToolsRepository' +import { ApiRepository } from '../../core/infra/repositories/ApiRepository' +import { + DatasetExternalToolUrl, + ExternalTool, + FileExternalToolUrl +} from '../domain/models/ExternalTool' +import { GetExternalToolDTO } from '../domain/dtos/GetExternalToolDTO' +import { datasetExternalToolTransformer } from './transformers/datasetExternalToolTransformer' +import { fileExternalToolTransformer } from './transformers/fileExternalToolTransformer' +import { externalToolsTransformer } from './transformers/externalToolsTransformer' + +export class ExternalToolsRepository extends ApiRepository implements IExternalToolsRepository { + private readonly externalToolsResourceName: string = 'externalTools' + + public async getExternalTools(): Promise { + return this.doGet(this.buildApiEndpoint(this.externalToolsResourceName)) + .then((response) => externalToolsTransformer(response)) + .catch((error) => { + throw error + }) + } + + public async getDatasetExternalToolUrl( + datasetId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise { + return this.doPost( + this.buildApiEndpoint('datasets', `externalTool/${toolId}/toolUrl`, datasetId), + getExternalToolDTO + ) + .then((response) => datasetExternalToolTransformer(response)) + .catch((error) => { + throw error + }) + } + + public async getFileExternalToolUrl( + fileId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise { + return this.doPost( + this.buildApiEndpoint('files', `externalTool/${toolId}/toolUrl`, fileId), + getExternalToolDTO + ) + .then((response) => fileExternalToolTransformer(response)) + .catch((error) => { + throw error + }) + } +} diff --git a/src/externalTools/infra/transformers/ExternalToolPayload.ts b/src/externalTools/infra/transformers/ExternalToolPayload.ts new file mode 100644 index 00000000..b2ce8b33 --- /dev/null +++ b/src/externalTools/infra/transformers/ExternalToolPayload.ts @@ -0,0 +1,19 @@ +export interface ExternalToolPayload { + id: number + displayName: string + description: string + types: ToolTypePayload[] + scope: ToolScopePayload +} + +enum ToolTypePayload { + Explore = 'explore', + Configure = 'configure', + Preview = 'preview', + Query = 'query' +} + +enum ToolScopePayload { + Dataset = 'dataset', + File = 'file' +} diff --git a/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts b/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts new file mode 100644 index 00000000..23a9dfc7 --- /dev/null +++ b/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts @@ -0,0 +1,17 @@ +import { AxiosResponse } from 'axios' +import { DatasetExternalToolUrl } from '../../domain/models/ExternalTool' + +export const datasetExternalToolTransformer = ( + response: AxiosResponse<{ + data: { toolUrl: string; toolName: string; datasetId: number; preview: boolean } + }> +): DatasetExternalToolUrl => { + const datasetExtTool = response.data.data + + return { + toolUrlResolved: datasetExtTool.toolUrl, + displayName: datasetExtTool.toolName, // TODO:ME - Maybe the API changes to displayName, keep an eye on it + datasetId: datasetExtTool.datasetId, + preview: datasetExtTool.preview + } +} diff --git a/src/externalTools/infra/transformers/externalToolsTransformer.ts b/src/externalTools/infra/transformers/externalToolsTransformer.ts new file mode 100644 index 00000000..09898cd5 --- /dev/null +++ b/src/externalTools/infra/transformers/externalToolsTransformer.ts @@ -0,0 +1,19 @@ +import { AxiosResponse } from 'axios' +import { ExternalTool } from '../../domain/models/ExternalTool' +import { ExternalToolPayload } from './ExternalToolPayload' + +export const externalToolsTransformer = ( + response: AxiosResponse<{ + data: ExternalToolPayload[] + }> +): ExternalTool[] => { + const tools = response.data.data + + return tools.map((tool) => ({ + id: tool.id, + displayName: tool.displayName, + description: tool.description, + types: tool.types as unknown as ExternalTool['types'], + scope: tool.scope as unknown as ExternalTool['scope'] + })) +} diff --git a/src/externalTools/infra/transformers/fileExternalToolTransformer.ts b/src/externalTools/infra/transformers/fileExternalToolTransformer.ts new file mode 100644 index 00000000..c39d9569 --- /dev/null +++ b/src/externalTools/infra/transformers/fileExternalToolTransformer.ts @@ -0,0 +1,17 @@ +import { AxiosResponse } from 'axios' +import { FileExternalToolUrl } from '../../domain/models/ExternalTool' + +export const fileExternalToolTransformer = ( + response: AxiosResponse<{ + data: { toolUrl: string; toolName: string; fileId: number; preview: boolean } + }> +): FileExternalToolUrl => { + const fileExtTool = response.data.data + + return { + toolUrlResolved: fileExtTool.toolUrl, + displayName: fileExtTool.toolName, // TODO:ME - Maybe the API changes to displayName, keep an eye on it + fileId: fileExtTool.fileId, + preview: fileExtTool.preview + } +} diff --git a/src/index.ts b/src/index.ts index 2fb70d9e..b0bb8921 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,3 +9,4 @@ export * from './metadataBlocks' export * from './files' export * from './contactInfo' export * from './search' +export * from './externalTools' From 550afa5652247d3f392b9af9b08d3000a5b9a408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 22 Aug 2025 16:56:36 -0300 Subject: [PATCH 073/123] test: unit --- .../externalTools/externalToolsHelper.ts | 126 ++++++++++++++++++ .../GetDatasetExternalToolUrl.test.ts | 29 ++++ .../externalTools/GetExternalTools.test.ts | 25 ++++ .../GetFileExternalToolUrl.test.ts | 29 ++++ 4 files changed, 209 insertions(+) create mode 100644 test/testHelpers/externalTools/externalToolsHelper.ts create mode 100644 test/unit/externalTools/GetDatasetExternalToolUrl.test.ts create mode 100644 test/unit/externalTools/GetExternalTools.test.ts create mode 100644 test/unit/externalTools/GetFileExternalToolUrl.test.ts diff --git a/test/testHelpers/externalTools/externalToolsHelper.ts b/test/testHelpers/externalTools/externalToolsHelper.ts new file mode 100644 index 00000000..2445c464 --- /dev/null +++ b/test/testHelpers/externalTools/externalToolsHelper.ts @@ -0,0 +1,126 @@ +import axios, { AxiosResponse } from 'axios' +import { + DatasetExternalToolUrl, + ExternalTool, + FileExternalToolUrl, + ToolScope, + ToolType +} from '../../../src' +import { TestConstants } from '../TestConstants' +import { ExternalToolPayload } from '../../../src/externalTools/infra/transformers/ExternalToolPayload' + +const DATAVERSE_API_REQUEST_HEADERS = { + headers: { 'Content-Type': 'application/json', 'X-Dataverse-Key': process.env.TEST_API_KEY } +} + +const CREATE_FILE_EXTERNAL_TOOL_PAYLOAD: ISetExternalToolViaApi = { + id: 80, + displayName: 'Text File Tool', + toolName: 'textFileTool', + description: 'Text File Tool', + types: [ToolType.Preview], + scope: ToolScope.File, + toolUrl: 'http://example.org/text-tool', + toolParameters: { + queryParameters: [ + { fileid: '{fileId}' }, + { siteUrl: '{siteUrl}' }, + { datasetid: '{datasetId}' }, + { datasetversion: '{datasetVersion}' }, + { locale: '{localeCode}' } + ] + }, + contentType: 'text/plain', + allowedApiCalls: [ + { + name: 'retrieveFileContents', + httpMethod: 'GET', + urlTemplate: '/api/v1/access/datafile/{fileId}', + timeOut: 3600 + } + ] +} + +export const createExternalToolsModel = (): ExternalTool[] => { + return [ + { + id: 1, + displayName: 'Test External Tool 1', + description: 'Description for Test External Tool 1', + scope: ToolScope.Dataset, + types: [ToolType.Explore] + }, + { + id: 2, + displayName: 'Test External Tool 2', + description: 'Description for Test External Tool 2', + scope: ToolScope.File, + types: [ToolType.Preview] + } + ] +} + +export const createFileExternalToolUrlModel = (): FileExternalToolUrl => { + return { + toolUrlResolved: 'https://example.com/text-tool?fileId=123', + displayName: 'Test File External Tool', + fileId: 123, + preview: true + } +} + +export const createDatasetExternalToolUrlModel = (): DatasetExternalToolUrl => { + return { + toolUrlResolved: 'https://example.com/dataset-tool?datasetId=456', + displayName: 'Test Dataset External Tool', + datasetId: 456, + preview: false + } +} + +interface ISetExternalToolViaApi { + id: number + displayName: string + toolName: string + description: string + types: ToolType[] + scope: ToolScope + toolUrl: string + toolParameters: { + queryParameters: { [key: string]: string }[] + } + contentType: string + allowedApiCalls: { + name: string + httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' + urlTemplate: string + timeOut: number + }[] +} + +export async function setExternalToolViaApi( + externalTool: ISetExternalToolViaApi = CREATE_FILE_EXTERNAL_TOOL_PAYLOAD +): Promise> { + try { + return await axios.post( + `${TestConstants.TEST_API_URL}/admin/externalTools`, + externalTool, + DATAVERSE_API_REQUEST_HEADERS + ) + } catch (error) { + console.log(error) + throw new Error('Error while setting external tool via API.') + } +} + +export async function deleteExternalToolViaApi(toolId: number): Promise { + try { + await axios.delete( + `${TestConstants.TEST_API_URL}/externalTools/${toolId}`, + DATAVERSE_API_REQUEST_HEADERS + ) + } catch (error) { + console.log(error) + throw new Error('Error while deleting external tool via API.') + } +} diff --git a/test/unit/externalTools/GetDatasetExternalToolUrl.test.ts b/test/unit/externalTools/GetDatasetExternalToolUrl.test.ts new file mode 100644 index 00000000..2bfb087d --- /dev/null +++ b/test/unit/externalTools/GetDatasetExternalToolUrl.test.ts @@ -0,0 +1,29 @@ +import { WriteError } from '../../../src' +import { IExternalToolsRepository } from '../../../src/externalTools/domain/repositories/IExternalToolsRepository' +import { GetDatasetExternalToolUrl } from '../../../src/externalTools/domain/useCases/GetDatasetExternalToolUrl' +import { createFileExternalToolUrlModel } from '../../testHelpers/externalTools/externalToolsHelper' + +describe('execute', () => { + test('should return dataset external tool url on repository success', async () => { + const testFileExternalToolUrl = createFileExternalToolUrlModel() + const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository + externalToolsRepositoryStub.getDatasetExternalToolUrl = jest + .fn() + .mockResolvedValue(testFileExternalToolUrl) + const sut = new GetDatasetExternalToolUrl(externalToolsRepositoryStub) + + const actual = await sut.execute(123, 3, { preview: true, locale: 'en' }) + + expect(actual).toEqual(testFileExternalToolUrl) + }) + + test('should return error result on repository error', async () => { + const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository + externalToolsRepositoryStub.getDatasetExternalToolUrl = jest + .fn() + .mockRejectedValue(new WriteError()) + const sut = new GetDatasetExternalToolUrl(externalToolsRepositoryStub) + + await expect(sut.execute(123, 3, { preview: true, locale: 'en' })).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/externalTools/GetExternalTools.test.ts b/test/unit/externalTools/GetExternalTools.test.ts new file mode 100644 index 00000000..a0b46762 --- /dev/null +++ b/test/unit/externalTools/GetExternalTools.test.ts @@ -0,0 +1,25 @@ +import { GetExternalTools } from '../../../src/externalTools/domain/useCases/GetExternalTools' +import { IExternalToolsRepository } from '../../../src/externalTools/domain/repositories/IExternalToolsRepository' +import { createExternalToolsModel } from '../../testHelpers/externalTools/externalToolsHelper' +import { ReadError } from '../../../src' + +describe('execute', () => { + test('should return external tools list on repository success', async () => { + const testExternalTools = createExternalToolsModel() + const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository + externalToolsRepositoryStub.getExternalTools = jest.fn().mockResolvedValue(testExternalTools) + const sut = new GetExternalTools(externalToolsRepositoryStub) + + const actual = await sut.execute() + + expect(actual).toEqual(testExternalTools) + }) + + test('should return error result on repository error', async () => { + const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository + externalToolsRepositoryStub.getExternalTools = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetExternalTools(externalToolsRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/externalTools/GetFileExternalToolUrl.test.ts b/test/unit/externalTools/GetFileExternalToolUrl.test.ts new file mode 100644 index 00000000..cf1eae26 --- /dev/null +++ b/test/unit/externalTools/GetFileExternalToolUrl.test.ts @@ -0,0 +1,29 @@ +import { WriteError } from '../../../src' +import { IExternalToolsRepository } from '../../../src/externalTools/domain/repositories/IExternalToolsRepository' +import { GetFileExternalToolUrl } from '../../../src/externalTools/domain/useCases/GetFileExternalToolUrl' +import { createFileExternalToolUrlModel } from '../../testHelpers/externalTools/externalToolsHelper' + +describe('execute', () => { + test('should return file external tool url on repository success', async () => { + const testFileExternalToolUrl = createFileExternalToolUrlModel() + const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository + externalToolsRepositoryStub.getFileExternalToolUrl = jest + .fn() + .mockResolvedValue(testFileExternalToolUrl) + const sut = new GetFileExternalToolUrl(externalToolsRepositoryStub) + + const actual = await sut.execute(123, 3, { preview: true, locale: 'en' }) + + expect(actual).toEqual(testFileExternalToolUrl) + }) + + test('should return error result on repository error', async () => { + const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository + externalToolsRepositoryStub.getFileExternalToolUrl = jest + .fn() + .mockRejectedValue(new WriteError()) + const sut = new GetFileExternalToolUrl(externalToolsRepositoryStub) + + await expect(sut.execute(123, 3, { preview: true, locale: 'en' })).rejects.toThrow(WriteError) + }) +}) From db7e61381a38011e56e5ab5c6a46197a871462a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 10:10:15 -0300 Subject: [PATCH 074/123] test: integration cases --- .../ExternalToolsRepository.test.ts | 228 ++++++++++++++++++ .../externalTools/externalToolsHelper.ts | 26 +- 2 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 test/integration/externalTools/ExternalToolsRepository.test.ts diff --git a/test/integration/externalTools/ExternalToolsRepository.test.ts b/test/integration/externalTools/ExternalToolsRepository.test.ts new file mode 100644 index 00000000..fba87cb8 --- /dev/null +++ b/test/integration/externalTools/ExternalToolsRepository.test.ts @@ -0,0 +1,228 @@ +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { ExternalToolsRepository } from '../../../src/externalTools/infra/ExternalToolsRepository' +import { + deleteExternalToolViaApi, + createExternalToolViaApi, + CREATE_FILE_EXTERNAL_TOOL_PAYLOAD, + CREATE_DATASET_EXTERNAL_TOOL_PAYLOAD +} from '../../testHelpers/externalTools/externalToolsHelper' +import { createDataset, CreatedDatasetIdentifiers, getDatasetFiles, WriteError } from '../../../src' +import { + createCollectionViaApi, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' +import { uploadFileViaApi } from '../../testHelpers/files/filesHelper' +import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' + +describe('ExternalToolsRepository', () => { + const sut: ExternalToolsRepository = new ExternalToolsRepository() + + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + describe('getExternalTools', () => { + test('should return all external tools availables in the installation', async () => { + const createdToolResponse = await createExternalToolViaApi('file') + const actual = await sut.getExternalTools() + + expect(actual.length).toBe(1) + expect(actual[0].id).toBe(createdToolResponse.data.data.id) + + await deleteExternalToolViaApi(createdToolResponse.data.data.id) + }) + + test('should return empty array if no external tools are available', async () => { + const actual = await sut.getExternalTools() + + expect(actual.length).toBe(0) + expect(actual).toStrictEqual([]) + }) + }) + + describe('getFileExternalToolUrl', () => { + const testCollectionAlias = 'getFileExternalToolUrlFunctionalTestCollection' + let testDatasetIds: CreatedDatasetIdentifiers + const testTextFile1Name = 'test-file-1.txt' + let testFileId: number + let testDatasetExternalToolId: number + let testFileExternalToolId: number + + beforeAll(async () => { + try { + // Create a Collection + await createCollectionViaApi(testCollectionAlias) + // Create a Dataset + testDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testCollectionAlias + ) + // Upload a file to the Dataset + await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name) + // Save File Id + const datasetFiles = await getDatasetFiles.execute(testDatasetIds.numericId) + testFileId = datasetFiles.files[0].id + // Create a dataset-level External Tool + const createdExtToolResponse1 = await createExternalToolViaApi('dataset') + testDatasetExternalToolId = createdExtToolResponse1.data.data.id + // Create a file-level External Tool + const createdExtToolResponse2 = await createExternalToolViaApi('file') + testFileExternalToolId = createdExtToolResponse2.data.data.id + } catch (error) { + throw new Error('Tests beforeAll(): Error setting up test data.') + } + }) + + afterAll(async () => { + try { + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + await deleteCollectionViaApi(testCollectionAlias) + await deleteExternalToolViaApi(testDatasetExternalToolId) + await deleteExternalToolViaApi(testFileExternalToolId) + } catch (error) { + throw new Error('Tests afterAll(): Error cleaning up test data.') + } + }) + + test('should return file external tool url', async () => { + const fileExternalToolUrl = await sut.getFileExternalToolUrl( + testFileId, + testFileExternalToolId, + { + preview: true, + locale: 'en' + } + ) + expect(fileExternalToolUrl.fileId).toBe(testFileId) + expect(fileExternalToolUrl.displayName).toBe(CREATE_FILE_EXTERNAL_TOOL_PAYLOAD.displayName) + expect(fileExternalToolUrl.toolUrlResolved).toContain( + CREATE_FILE_EXTERNAL_TOOL_PAYLOAD.toolUrl + ) + expect(fileExternalToolUrl.toolUrlResolved).toContain(`preview=true`) + expect(fileExternalToolUrl.preview).toBe(true) + }) + + test('should return error if file external tool id does not exist', async () => { + await expect( + sut.getFileExternalToolUrl(testFileId, 999999, { + preview: true, + locale: 'en' + }) + ).rejects.toThrow(WriteError) // e.g. [400] External tool not found with id: 999999 + }) + + test('should return error if toolId is not for a file-level external tool', async () => { + await expect( + sut.getFileExternalToolUrl(testFileId, testDatasetExternalToolId, { + preview: true, + locale: 'en' + }) + ).rejects.toThrow(WriteError) // e.g. [400] External tool does not have file scope. + }) + + test('should return error if file id does not exist', async () => { + await expect( + sut.getFileExternalToolUrl(56565656, testFileExternalToolId, { + preview: true, + locale: 'en' + }) + ).rejects.toThrow(WriteError) // e.g. [404] File not found for given id: 56565656 + }) + }) + + describe('getDatasetExternalToolUrl', () => { + const testCollectionAlias = 'getDatasetExternalToolUrlFunctionalTestCollection' + let testDatasetIds: CreatedDatasetIdentifiers + const testTextFile1Name = 'test-file-1.txt' + let testDatasetExternalToolId: number + let testFileExternalToolId: number + + beforeAll(async () => { + try { + // Create a Collection + await createCollectionViaApi(testCollectionAlias) + // Create a Dataset + testDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testCollectionAlias + ) + // Upload a file to the Dataset + await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name) + // Create a dataset-level External Tool + const createdExtToolResponse1 = await createExternalToolViaApi('dataset') + testDatasetExternalToolId = createdExtToolResponse1.data.data.id + // Create a file-level External Tool + const createdExtToolResponse2 = await createExternalToolViaApi('file') + testFileExternalToolId = createdExtToolResponse2.data.data.id + } catch (error) { + throw new Error('Tests beforeAll(): Error setting up test data.') + } + }) + + afterAll(async () => { + try { + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + await deleteCollectionViaApi(testCollectionAlias) + await deleteExternalToolViaApi(testDatasetExternalToolId) + await deleteExternalToolViaApi(testFileExternalToolId) + } catch (error) { + throw new Error('Tests afterAll(): Error cleaning up test data.') + } + }) + + test('should return dataset external tool url', async () => { + const datasetfileExternalToolUrl = await sut.getDatasetExternalToolUrl( + testDatasetIds.numericId, + testDatasetExternalToolId, + { + preview: true, + locale: 'en' + } + ) + expect(datasetfileExternalToolUrl.datasetId).toBe(testDatasetIds.numericId) + expect(datasetfileExternalToolUrl.displayName).toBe( + CREATE_DATASET_EXTERNAL_TOOL_PAYLOAD.displayName + ) + expect(datasetfileExternalToolUrl.toolUrlResolved).toContain( + CREATE_DATASET_EXTERNAL_TOOL_PAYLOAD.toolUrl + ) + expect(datasetfileExternalToolUrl.toolUrlResolved).toContain(`preview=true`) + expect(datasetfileExternalToolUrl.preview).toBe(true) + }) + + test('should return error if dataset external tool id does not exist', async () => { + await expect( + sut.getDatasetExternalToolUrl(testDatasetIds.numericId, 999999, { + preview: true, + locale: 'en' + }) + ).rejects.toThrow(WriteError) // e.g. [400] External tool not found with id: 999999 + }) + + test('should return error if toolId is not for a dataset-level external tool', async () => { + await expect( + sut.getDatasetExternalToolUrl(testDatasetIds.numericId, testFileExternalToolId, { + preview: true, + locale: 'en' + }) + ).rejects.toThrow(WriteError) // e.g. [400] External tool does not have dataset scope. + }) + + test('should return error if dataset id does not exist', async () => { + await expect( + sut.getDatasetExternalToolUrl(56565656, testDatasetExternalToolId, { + preview: true, + locale: 'en' + }) + ).rejects.toThrow(WriteError) // e.g. [404] Dataset not found for given id: 56565656 + }) + }) +}) diff --git a/test/testHelpers/externalTools/externalToolsHelper.ts b/test/testHelpers/externalTools/externalToolsHelper.ts index 2445c464..f9965f49 100644 --- a/test/testHelpers/externalTools/externalToolsHelper.ts +++ b/test/testHelpers/externalTools/externalToolsHelper.ts @@ -13,8 +13,7 @@ const DATAVERSE_API_REQUEST_HEADERS = { headers: { 'Content-Type': 'application/json', 'X-Dataverse-Key': process.env.TEST_API_KEY } } -const CREATE_FILE_EXTERNAL_TOOL_PAYLOAD: ISetExternalToolViaApi = { - id: 80, +export const CREATE_FILE_EXTERNAL_TOOL_PAYLOAD: ISetExternalToolViaApi = { displayName: 'Text File Tool', toolName: 'textFileTool', description: 'Text File Tool', @@ -41,6 +40,18 @@ const CREATE_FILE_EXTERNAL_TOOL_PAYLOAD: ISetExternalToolViaApi = { ] } +export const CREATE_DATASET_EXTERNAL_TOOL_PAYLOAD: ISetExternalToolViaApi = { + displayName: 'Dataset Tool', + toolName: 'datasetFileTool', + description: 'Dataset Explore Tool', + types: [ToolType.Explore], + scope: ToolScope.Dataset, + toolUrl: 'http://example.org/dataset-tool', + toolParameters: { + queryParameters: [{ datasetPid: '{datasetPid}' }] + } +} + export const createExternalToolsModel = (): ExternalTool[] => { return [ { @@ -79,7 +90,6 @@ export const createDatasetExternalToolUrlModel = (): DatasetExternalToolUrl => { } interface ISetExternalToolViaApi { - id: number displayName: string toolName: string description: string @@ -89,8 +99,8 @@ interface ISetExternalToolViaApi { toolParameters: { queryParameters: { [key: string]: string }[] } - contentType: string - allowedApiCalls: { + contentType?: string + allowedApiCalls?: { name: string httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' urlTemplate: string @@ -98,13 +108,13 @@ interface ISetExternalToolViaApi { }[] } -export async function setExternalToolViaApi( - externalTool: ISetExternalToolViaApi = CREATE_FILE_EXTERNAL_TOOL_PAYLOAD +export async function createExternalToolViaApi( + type: 'dataset' | 'file' ): Promise> { try { return await axios.post( `${TestConstants.TEST_API_URL}/admin/externalTools`, - externalTool, + type === 'dataset' ? CREATE_DATASET_EXTERNAL_TOOL_PAYLOAD : CREATE_FILE_EXTERNAL_TOOL_PAYLOAD, DATAVERSE_API_REQUEST_HEADERS ) } catch (error) { From aa4bb6535d788b55acf4b8aa09445b483a77bdb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 10:31:39 -0300 Subject: [PATCH 075/123] refactor: change mapping --- .../infra/transformers/datasetExternalToolTransformer.ts | 4 ++-- .../infra/transformers/fileExternalToolTransformer.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts b/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts index 23a9dfc7..8b840f38 100644 --- a/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts +++ b/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts @@ -3,14 +3,14 @@ import { DatasetExternalToolUrl } from '../../domain/models/ExternalTool' export const datasetExternalToolTransformer = ( response: AxiosResponse<{ - data: { toolUrl: string; toolName: string; datasetId: number; preview: boolean } + data: { toolUrl: string; displayName: string; datasetId: number; preview: boolean } }> ): DatasetExternalToolUrl => { const datasetExtTool = response.data.data return { toolUrlResolved: datasetExtTool.toolUrl, - displayName: datasetExtTool.toolName, // TODO:ME - Maybe the API changes to displayName, keep an eye on it + displayName: datasetExtTool.displayName, datasetId: datasetExtTool.datasetId, preview: datasetExtTool.preview } diff --git a/src/externalTools/infra/transformers/fileExternalToolTransformer.ts b/src/externalTools/infra/transformers/fileExternalToolTransformer.ts index c39d9569..017dfbc1 100644 --- a/src/externalTools/infra/transformers/fileExternalToolTransformer.ts +++ b/src/externalTools/infra/transformers/fileExternalToolTransformer.ts @@ -3,14 +3,14 @@ import { FileExternalToolUrl } from '../../domain/models/ExternalTool' export const fileExternalToolTransformer = ( response: AxiosResponse<{ - data: { toolUrl: string; toolName: string; fileId: number; preview: boolean } + data: { toolUrl: string; displayName: string; fileId: number; preview: boolean } }> ): FileExternalToolUrl => { const fileExtTool = response.data.data return { toolUrlResolved: fileExtTool.toolUrl, - displayName: fileExtTool.toolName, // TODO:ME - Maybe the API changes to displayName, keep an eye on it + displayName: fileExtTool.displayName, fileId: fileExtTool.fileId, preview: fileExtTool.preview } From fc750c940ad8692a60fdb03e1c056371ccfe2d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 10:36:48 -0300 Subject: [PATCH 076/123] skip tests to get pr version generated --- .../externalTools/ExternalToolsRepository.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/integration/externalTools/ExternalToolsRepository.test.ts b/test/integration/externalTools/ExternalToolsRepository.test.ts index fba87cb8..17bb701e 100644 --- a/test/integration/externalTools/ExternalToolsRepository.test.ts +++ b/test/integration/externalTools/ExternalToolsRepository.test.ts @@ -48,7 +48,8 @@ describe('ExternalToolsRepository', () => { }) }) - describe('getFileExternalToolUrl', () => { + // TODO:ME - Skip for now until Backend PR is merged to develop. + describe.skip('getFileExternalToolUrl', () => { const testCollectionAlias = 'getFileExternalToolUrlFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' @@ -138,7 +139,8 @@ describe('ExternalToolsRepository', () => { }) }) - describe('getDatasetExternalToolUrl', () => { + // TODO:ME - Skip for now until Backend PR is merged to develop. + describe.skip('getDatasetExternalToolUrl', () => { const testCollectionAlias = 'getDatasetExternalToolUrlFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' From 5ed2e8a6cf9667fd40077f20154320c429a343eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 17:01:29 -0300 Subject: [PATCH 077/123] refactor: rename external tool URL methods and interfaces for clarity --- docs/useCases.md | 28 +++++------ .../domain/models/ExternalTool.ts | 4 +- .../repositories/IExternalToolsRepository.ts | 14 ++++-- ...l.ts => GetDatasetExternalToolResolved.ts} | 12 ++--- ...lUrl.ts => GetFileExternalToolResolved.ts} | 12 ++--- src/externalTools/index.ts | 16 +++--- .../infra/ExternalToolsRepository.ts | 12 ++--- .../datasetExternalToolTransformer.ts | 4 +- .../fileExternalToolTransformer.ts | 4 +- .../ExternalToolsRepository.test.ts | 50 ++++++++++--------- .../externalTools/externalToolsHelper.ts | 8 +-- .../GetDatasetExternalToolUrl.test.ts | 20 ++++---- .../GetFileExternalToolUrl.test.ts | 20 ++++---- 13 files changed, 105 insertions(+), 99 deletions(-) rename src/externalTools/domain/useCases/{GetDatasetExternalToolUrl.ts => GetDatasetExternalToolResolved.ts} (75%) rename src/externalTools/domain/useCases/{GetFileExternalToolUrl.ts => GetFileExternalToolResolved.ts} (76%) diff --git a/docs/useCases.md b/docs/useCases.md index a465df8f..c72b1a88 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -96,8 +96,8 @@ The different use cases currently available in the package are classified below, - [External Tools](#external-tools) - [External Tools read use cases](#external-tools-read-use-cases) - [Get External Tools](#get-external-tools) - - [Get Dataset External Tool Url](#get-dataset-external-tool-url) - - [Get File External Tool Url](#get-file-external-tool-url) + - [Get Dataset External Tool Resolved](#get-dataset-external-tool-resolved) + - [Get File External Tool Resolved](#get-file-external-tool-resolved) ## Collections @@ -2172,14 +2172,14 @@ getExternalTools.execute().then((externalTools: ExternalTool[]) => { _See [use case](../src/externalTools/domain/useCases/GetExternalTools.ts) implementation_. -#### Get Dataset External Tool Url +#### Get Dataset External Tool Resolved -Returns an instance of [DatasetExternalToolUrl](../src/externalTools/domain/models/ExternalTool.ts), which contains the resolved URL for accessing an external tool that operates at the dataset level. +Returns an instance of [DatasetExternalToolResolved](../src/externalTools/domain/models/ExternalTool.ts), which contains the resolved URL for accessing an external tool that operates at the dataset level. ##### Example call: ```typescript -import { getDatasetExternalToolUrl } from '@iqss/dataverse-client-javascript' +import { getDatasetExternalToolResolved } from '@iqss/dataverse-client-javascript' /* ... */ const toolId = 1 @@ -2189,24 +2189,24 @@ const getExternalToolDTO: GetExternalToolDTO = { locale: 'en' } -getDatasetExternalToolUrl +getDatasetExternalToolResolved .execute(toolId, datasetId, getExternalToolDTO) - .then((datasetExternalToolUrl: DatasetExternalToolUrl) => { + .then((datasetExternalToolResolved: DatasetExternalToolResolved) => { /* ... */ }) /* ... */ ``` -_See [use case](../src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts) implementation_. +_See [use case](../src/externalTools/domain/useCases/GetDatasetExternalToolResolved.ts) implementation_. -#### Get File External Tool Url +#### Get File External Tool Resolved -Returns an instance of [FileExternalToolUrl](../src/externalTools/domain/models/ExternalTool.ts), which contains the resolved URL for accessing an external tool that operates at the file level. +Returns an instance of [FileExternalToolResolved](../src/externalTools/domain/models/ExternalTool.ts), which contains the resolved URL for accessing an external tool that operates at the file level. ##### Example call: ```typescript -import { getFileExternalToolUrl } from '@iqss/dataverse-client-javascript' +import { getFileExternalToolResolved } from '@iqss/dataverse-client-javascript' /* ... */ const toolId = 1 @@ -2216,12 +2216,12 @@ const getExternalToolDTO: GetExternalToolDTO = { locale: 'en' } -getFileExternalToolUrl +getFileExternalToolResolved .execute(toolId, fileId, getExternalToolDTO) - .then((fileExternalToolUrl: FileExternalToolUrl) => { + .then((fileExternalToolResolved: FileExternalToolResolved) => { /* ... */ }) /* ... */ ``` -_See [use case](../src/externalTools/domain/useCases/GetfileExternalToolUrl.ts) implementation_. +_See [use case](../src/externalTools/domain/useCases/GetfileExternalToolResolved.ts) implementation_. diff --git a/src/externalTools/domain/models/ExternalTool.ts b/src/externalTools/domain/models/ExternalTool.ts index 61955337..ea783c13 100644 --- a/src/externalTools/domain/models/ExternalTool.ts +++ b/src/externalTools/domain/models/ExternalTool.ts @@ -18,14 +18,14 @@ export enum ToolScope { File = 'file' } -export interface DatasetExternalToolUrl { +export interface DatasetExternalToolResolved { toolUrlResolved: string displayName: string datasetId: number preview: boolean } -export interface FileExternalToolUrl { +export interface FileExternalToolResolved { toolUrlResolved: string displayName: string fileId: number diff --git a/src/externalTools/domain/repositories/IExternalToolsRepository.ts b/src/externalTools/domain/repositories/IExternalToolsRepository.ts index 9e163901..36db3870 100644 --- a/src/externalTools/domain/repositories/IExternalToolsRepository.ts +++ b/src/externalTools/domain/repositories/IExternalToolsRepository.ts @@ -1,16 +1,20 @@ import { GetExternalToolDTO } from '../dtos/GetExternalToolDTO' -import { DatasetExternalToolUrl, ExternalTool, FileExternalToolUrl } from '../models/ExternalTool' +import { + DatasetExternalToolResolved, + ExternalTool, + FileExternalToolResolved +} from '../models/ExternalTool' export interface IExternalToolsRepository { getExternalTools(): Promise - getDatasetExternalToolUrl( + getDatasetExternalToolResolved( datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise - getFileExternalToolUrl( + ): Promise + getFileExternalToolResolved( fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise + ): Promise } diff --git a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts b/src/externalTools/domain/useCases/GetDatasetExternalToolResolved.ts similarity index 75% rename from src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts rename to src/externalTools/domain/useCases/GetDatasetExternalToolResolved.ts index ef6bfa2f..c1668529 100644 --- a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts +++ b/src/externalTools/domain/useCases/GetDatasetExternalToolResolved.ts @@ -1,9 +1,9 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { GetExternalToolDTO } from '../dtos/GetExternalToolDTO' -import { DatasetExternalToolUrl } from '../models/ExternalTool' +import { DatasetExternalToolResolved } from '../models/ExternalTool' import { IExternalToolsRepository } from '../repositories/IExternalToolsRepository' -export class GetDatasetExternalToolUrl implements UseCase { +export class GetDatasetExternalToolResolved implements UseCase { private externalToolsRepository: IExternalToolsRepository constructor(externalToolsRepository: IExternalToolsRepository) { @@ -11,21 +11,21 @@ export class GetDatasetExternalToolUrl implements UseCase} + * @returns {Promise} */ async execute( datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise { - return await this.externalToolsRepository.getDatasetExternalToolUrl( + ): Promise { + return await this.externalToolsRepository.getDatasetExternalToolResolved( datasetId, toolId, getExternalToolDTO diff --git a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts b/src/externalTools/domain/useCases/GetFileExternalToolResolved.ts similarity index 76% rename from src/externalTools/domain/useCases/GetFileExternalToolUrl.ts rename to src/externalTools/domain/useCases/GetFileExternalToolResolved.ts index 60fdc8d6..47f6b090 100644 --- a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts +++ b/src/externalTools/domain/useCases/GetFileExternalToolResolved.ts @@ -1,9 +1,9 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { GetExternalToolDTO } from '../dtos/GetExternalToolDTO' -import { FileExternalToolUrl } from '../models/ExternalTool' +import { FileExternalToolResolved } from '../models/ExternalTool' import { IExternalToolsRepository } from '../repositories/IExternalToolsRepository' -export class GetFileExternalToolUrl implements UseCase { +export class GetFileExternalToolResolved implements UseCase { private externalToolsRepository: IExternalToolsRepository constructor(externalToolsRepository: IExternalToolsRepository) { @@ -11,21 +11,21 @@ export class GetFileExternalToolUrl implements UseCase { } /** - * Returns a FileExternalToolUrl object containing the resolved URL for accessing an external tool that operates at the file level. + * Returns a FileExternalToolResolved object containing the resolved URL for accessing an external tool that operates at the file level. * The URL includes necessary authentication tokens and parameters based on the user's permissions and the tool's configuration. * Authentication is required for draft, restricted, embargoed, or expired (retention period) files, the user must have appropriate permissions. * * @param {number | string} [fileId] - The File identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {number} toolId - The identifier of the external tool. * @param {GetExternalToolDTO} getExternalToolDTO - The GetExternalToolDTO object containing additional parameters for the request. - * @returns {Promise} + * @returns {Promise} */ async execute( fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise { - return await this.externalToolsRepository.getFileExternalToolUrl( + ): Promise { + return await this.externalToolsRepository.getFileExternalToolResolved( fileId, toolId, getExternalToolDTO diff --git a/src/externalTools/index.ts b/src/externalTools/index.ts index a9471ec3..96b2581f 100644 --- a/src/externalTools/index.ts +++ b/src/externalTools/index.ts @@ -1,18 +1,18 @@ -import { GetDatasetExternalToolUrl } from './domain/useCases/GetDatasetExternalToolUrl' +import { GetDatasetExternalToolResolved } from './domain/useCases/GetDatasetExternalToolResolved' import { GetExternalTools } from './domain/useCases/GetExternalTools' -import { GetFileExternalToolUrl } from './domain/useCases/GetFileExternalToolUrl' +import { GetFileExternalToolResolved } from './domain/useCases/GetFileExternalToolResolved' import { ExternalToolsRepository } from './infra/ExternalToolsRepository' const externalToolsRepository = new ExternalToolsRepository() const getExternalTools = new GetExternalTools(externalToolsRepository) -const getDatasetExternalToolUrl = new GetDatasetExternalToolUrl(externalToolsRepository) -const getFileExternalToolUrl = new GetFileExternalToolUrl(externalToolsRepository) +const getDatasetExternalToolResolved = new GetDatasetExternalToolResolved(externalToolsRepository) +const getFileExternalToolResolved = new GetFileExternalToolResolved(externalToolsRepository) export { getExternalTools, - getDatasetExternalToolUrl, - getFileExternalToolUrl, + getDatasetExternalToolResolved, + getFileExternalToolResolved, externalToolsRepository } @@ -20,6 +20,6 @@ export { ExternalTool, ToolScope, ToolType, - DatasetExternalToolUrl, - FileExternalToolUrl + DatasetExternalToolResolved, + FileExternalToolResolved } from './domain/models/ExternalTool' diff --git a/src/externalTools/infra/ExternalToolsRepository.ts b/src/externalTools/infra/ExternalToolsRepository.ts index f90a1a40..5e104593 100644 --- a/src/externalTools/infra/ExternalToolsRepository.ts +++ b/src/externalTools/infra/ExternalToolsRepository.ts @@ -1,9 +1,9 @@ import { IExternalToolsRepository } from '../domain/repositories/IExternalToolsRepository' import { ApiRepository } from '../../core/infra/repositories/ApiRepository' import { - DatasetExternalToolUrl, + DatasetExternalToolResolved, ExternalTool, - FileExternalToolUrl + FileExternalToolResolved } from '../domain/models/ExternalTool' import { GetExternalToolDTO } from '../domain/dtos/GetExternalToolDTO' import { datasetExternalToolTransformer } from './transformers/datasetExternalToolTransformer' @@ -21,11 +21,11 @@ export class ExternalToolsRepository extends ApiRepository implements IExternalT }) } - public async getDatasetExternalToolUrl( + public async getDatasetExternalToolResolved( datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise { + ): Promise { return this.doPost( this.buildApiEndpoint('datasets', `externalTool/${toolId}/toolUrl`, datasetId), getExternalToolDTO @@ -36,11 +36,11 @@ export class ExternalToolsRepository extends ApiRepository implements IExternalT }) } - public async getFileExternalToolUrl( + public async getFileExternalToolResolved( fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise { + ): Promise { return this.doPost( this.buildApiEndpoint('files', `externalTool/${toolId}/toolUrl`, fileId), getExternalToolDTO diff --git a/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts b/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts index 8b840f38..fef953ca 100644 --- a/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts +++ b/src/externalTools/infra/transformers/datasetExternalToolTransformer.ts @@ -1,11 +1,11 @@ import { AxiosResponse } from 'axios' -import { DatasetExternalToolUrl } from '../../domain/models/ExternalTool' +import { DatasetExternalToolResolved } from '../../domain/models/ExternalTool' export const datasetExternalToolTransformer = ( response: AxiosResponse<{ data: { toolUrl: string; displayName: string; datasetId: number; preview: boolean } }> -): DatasetExternalToolUrl => { +): DatasetExternalToolResolved => { const datasetExtTool = response.data.data return { diff --git a/src/externalTools/infra/transformers/fileExternalToolTransformer.ts b/src/externalTools/infra/transformers/fileExternalToolTransformer.ts index 017dfbc1..f6305fb0 100644 --- a/src/externalTools/infra/transformers/fileExternalToolTransformer.ts +++ b/src/externalTools/infra/transformers/fileExternalToolTransformer.ts @@ -1,11 +1,11 @@ import { AxiosResponse } from 'axios' -import { FileExternalToolUrl } from '../../domain/models/ExternalTool' +import { FileExternalToolResolved } from '../../domain/models/ExternalTool' export const fileExternalToolTransformer = ( response: AxiosResponse<{ data: { toolUrl: string; displayName: string; fileId: number; preview: boolean } }> -): FileExternalToolUrl => { +): FileExternalToolResolved => { const fileExtTool = response.data.data return { diff --git a/test/integration/externalTools/ExternalToolsRepository.test.ts b/test/integration/externalTools/ExternalToolsRepository.test.ts index 17bb701e..79b21811 100644 --- a/test/integration/externalTools/ExternalToolsRepository.test.ts +++ b/test/integration/externalTools/ExternalToolsRepository.test.ts @@ -49,8 +49,8 @@ describe('ExternalToolsRepository', () => { }) // TODO:ME - Skip for now until Backend PR is merged to develop. - describe.skip('getFileExternalToolUrl', () => { - const testCollectionAlias = 'getFileExternalToolUrlFunctionalTestCollection' + describe.skip('getFileExternalToolResolved', () => { + const testCollectionAlias = 'getFileExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' let testFileId: number @@ -93,8 +93,8 @@ describe('ExternalToolsRepository', () => { } }) - test('should return file external tool url', async () => { - const fileExternalToolUrl = await sut.getFileExternalToolUrl( + test('should return file external tool resolved', async () => { + const fileExternalToolResolved = await sut.getFileExternalToolResolved( testFileId, testFileExternalToolId, { @@ -102,18 +102,20 @@ describe('ExternalToolsRepository', () => { locale: 'en' } ) - expect(fileExternalToolUrl.fileId).toBe(testFileId) - expect(fileExternalToolUrl.displayName).toBe(CREATE_FILE_EXTERNAL_TOOL_PAYLOAD.displayName) - expect(fileExternalToolUrl.toolUrlResolved).toContain( + expect(fileExternalToolResolved.fileId).toBe(testFileId) + expect(fileExternalToolResolved.displayName).toBe( + CREATE_FILE_EXTERNAL_TOOL_PAYLOAD.displayName + ) + expect(fileExternalToolResolved.toolUrlResolved).toContain( CREATE_FILE_EXTERNAL_TOOL_PAYLOAD.toolUrl ) - expect(fileExternalToolUrl.toolUrlResolved).toContain(`preview=true`) - expect(fileExternalToolUrl.preview).toBe(true) + expect(fileExternalToolResolved.toolUrlResolved).toContain(`preview=true`) + expect(fileExternalToolResolved.preview).toBe(true) }) test('should return error if file external tool id does not exist', async () => { await expect( - sut.getFileExternalToolUrl(testFileId, 999999, { + sut.getFileExternalToolResolved(testFileId, 999999, { preview: true, locale: 'en' }) @@ -122,7 +124,7 @@ describe('ExternalToolsRepository', () => { test('should return error if toolId is not for a file-level external tool', async () => { await expect( - sut.getFileExternalToolUrl(testFileId, testDatasetExternalToolId, { + sut.getFileExternalToolResolved(testFileId, testDatasetExternalToolId, { preview: true, locale: 'en' }) @@ -131,7 +133,7 @@ describe('ExternalToolsRepository', () => { test('should return error if file id does not exist', async () => { await expect( - sut.getFileExternalToolUrl(56565656, testFileExternalToolId, { + sut.getFileExternalToolResolved(56565656, testFileExternalToolId, { preview: true, locale: 'en' }) @@ -140,8 +142,8 @@ describe('ExternalToolsRepository', () => { }) // TODO:ME - Skip for now until Backend PR is merged to develop. - describe.skip('getDatasetExternalToolUrl', () => { - const testCollectionAlias = 'getDatasetExternalToolUrlFunctionalTestCollection' + describe.skip('getDatasetExternalToolResolved', () => { + const testCollectionAlias = 'getDatasetExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' let testDatasetExternalToolId: number @@ -180,8 +182,8 @@ describe('ExternalToolsRepository', () => { } }) - test('should return dataset external tool url', async () => { - const datasetfileExternalToolUrl = await sut.getDatasetExternalToolUrl( + test('should return dataset external tool resolved', async () => { + const datasetfileExternalToolResolved = await sut.getDatasetExternalToolResolved( testDatasetIds.numericId, testDatasetExternalToolId, { @@ -189,20 +191,20 @@ describe('ExternalToolsRepository', () => { locale: 'en' } ) - expect(datasetfileExternalToolUrl.datasetId).toBe(testDatasetIds.numericId) - expect(datasetfileExternalToolUrl.displayName).toBe( + expect(datasetfileExternalToolResolved.datasetId).toBe(testDatasetIds.numericId) + expect(datasetfileExternalToolResolved.displayName).toBe( CREATE_DATASET_EXTERNAL_TOOL_PAYLOAD.displayName ) - expect(datasetfileExternalToolUrl.toolUrlResolved).toContain( + expect(datasetfileExternalToolResolved.toolUrlResolved).toContain( CREATE_DATASET_EXTERNAL_TOOL_PAYLOAD.toolUrl ) - expect(datasetfileExternalToolUrl.toolUrlResolved).toContain(`preview=true`) - expect(datasetfileExternalToolUrl.preview).toBe(true) + expect(datasetfileExternalToolResolved.toolUrlResolved).toContain(`preview=true`) + expect(datasetfileExternalToolResolved.preview).toBe(true) }) test('should return error if dataset external tool id does not exist', async () => { await expect( - sut.getDatasetExternalToolUrl(testDatasetIds.numericId, 999999, { + sut.getDatasetExternalToolResolved(testDatasetIds.numericId, 999999, { preview: true, locale: 'en' }) @@ -211,7 +213,7 @@ describe('ExternalToolsRepository', () => { test('should return error if toolId is not for a dataset-level external tool', async () => { await expect( - sut.getDatasetExternalToolUrl(testDatasetIds.numericId, testFileExternalToolId, { + sut.getDatasetExternalToolResolved(testDatasetIds.numericId, testFileExternalToolId, { preview: true, locale: 'en' }) @@ -220,7 +222,7 @@ describe('ExternalToolsRepository', () => { test('should return error if dataset id does not exist', async () => { await expect( - sut.getDatasetExternalToolUrl(56565656, testDatasetExternalToolId, { + sut.getDatasetExternalToolResolved(56565656, testDatasetExternalToolId, { preview: true, locale: 'en' }) diff --git a/test/testHelpers/externalTools/externalToolsHelper.ts b/test/testHelpers/externalTools/externalToolsHelper.ts index f9965f49..0e280d6f 100644 --- a/test/testHelpers/externalTools/externalToolsHelper.ts +++ b/test/testHelpers/externalTools/externalToolsHelper.ts @@ -1,8 +1,8 @@ import axios, { AxiosResponse } from 'axios' import { - DatasetExternalToolUrl, + DatasetExternalToolResolved, ExternalTool, - FileExternalToolUrl, + FileExternalToolResolved, ToolScope, ToolType } from '../../../src' @@ -71,7 +71,7 @@ export const createExternalToolsModel = (): ExternalTool[] => { ] } -export const createFileExternalToolUrlModel = (): FileExternalToolUrl => { +export const createFileExternalToolResolvedModel = (): FileExternalToolResolved => { return { toolUrlResolved: 'https://example.com/text-tool?fileId=123', displayName: 'Test File External Tool', @@ -80,7 +80,7 @@ export const createFileExternalToolUrlModel = (): FileExternalToolUrl => { } } -export const createDatasetExternalToolUrlModel = (): DatasetExternalToolUrl => { +export const createDatasetExternalToolResolvedModel = (): DatasetExternalToolResolved => { return { toolUrlResolved: 'https://example.com/dataset-tool?datasetId=456', displayName: 'Test Dataset External Tool', diff --git a/test/unit/externalTools/GetDatasetExternalToolUrl.test.ts b/test/unit/externalTools/GetDatasetExternalToolUrl.test.ts index 2bfb087d..587eda01 100644 --- a/test/unit/externalTools/GetDatasetExternalToolUrl.test.ts +++ b/test/unit/externalTools/GetDatasetExternalToolUrl.test.ts @@ -1,28 +1,28 @@ import { WriteError } from '../../../src' import { IExternalToolsRepository } from '../../../src/externalTools/domain/repositories/IExternalToolsRepository' -import { GetDatasetExternalToolUrl } from '../../../src/externalTools/domain/useCases/GetDatasetExternalToolUrl' -import { createFileExternalToolUrlModel } from '../../testHelpers/externalTools/externalToolsHelper' +import { GetDatasetExternalToolResolved } from '../../../src/externalTools/domain/useCases/GetDatasetExternalToolResolved' +import { createDatasetExternalToolResolvedModel } from '../../testHelpers/externalTools/externalToolsHelper' describe('execute', () => { - test('should return dataset external tool url on repository success', async () => { - const testFileExternalToolUrl = createFileExternalToolUrlModel() + test('should return dataset external tool resolved on repository success', async () => { + const testDatasetExternalToolResolved = createDatasetExternalToolResolvedModel() const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository - externalToolsRepositoryStub.getDatasetExternalToolUrl = jest + externalToolsRepositoryStub.getDatasetExternalToolResolved = jest .fn() - .mockResolvedValue(testFileExternalToolUrl) - const sut = new GetDatasetExternalToolUrl(externalToolsRepositoryStub) + .mockResolvedValue(testDatasetExternalToolResolved) + const sut = new GetDatasetExternalToolResolved(externalToolsRepositoryStub) const actual = await sut.execute(123, 3, { preview: true, locale: 'en' }) - expect(actual).toEqual(testFileExternalToolUrl) + expect(actual).toEqual(testDatasetExternalToolResolved) }) test('should return error result on repository error', async () => { const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository - externalToolsRepositoryStub.getDatasetExternalToolUrl = jest + externalToolsRepositoryStub.getDatasetExternalToolResolved = jest .fn() .mockRejectedValue(new WriteError()) - const sut = new GetDatasetExternalToolUrl(externalToolsRepositoryStub) + const sut = new GetDatasetExternalToolResolved(externalToolsRepositoryStub) await expect(sut.execute(123, 3, { preview: true, locale: 'en' })).rejects.toThrow(WriteError) }) diff --git a/test/unit/externalTools/GetFileExternalToolUrl.test.ts b/test/unit/externalTools/GetFileExternalToolUrl.test.ts index cf1eae26..a55cca29 100644 --- a/test/unit/externalTools/GetFileExternalToolUrl.test.ts +++ b/test/unit/externalTools/GetFileExternalToolUrl.test.ts @@ -1,28 +1,28 @@ import { WriteError } from '../../../src' import { IExternalToolsRepository } from '../../../src/externalTools/domain/repositories/IExternalToolsRepository' -import { GetFileExternalToolUrl } from '../../../src/externalTools/domain/useCases/GetFileExternalToolUrl' -import { createFileExternalToolUrlModel } from '../../testHelpers/externalTools/externalToolsHelper' +import { GetFileExternalToolResolved } from '../../../src/externalTools/domain/useCases/GetFileExternalToolResolved' +import { createFileExternalToolResolvedModel } from '../../testHelpers/externalTools/externalToolsHelper' describe('execute', () => { - test('should return file external tool url on repository success', async () => { - const testFileExternalToolUrl = createFileExternalToolUrlModel() + test('should return file external tool resolved on repository success', async () => { + const testFileExternalToolResolved = createFileExternalToolResolvedModel() const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository - externalToolsRepositoryStub.getFileExternalToolUrl = jest + externalToolsRepositoryStub.getFileExternalToolResolved = jest .fn() - .mockResolvedValue(testFileExternalToolUrl) - const sut = new GetFileExternalToolUrl(externalToolsRepositoryStub) + .mockResolvedValue(testFileExternalToolResolved) + const sut = new GetFileExternalToolResolved(externalToolsRepositoryStub) const actual = await sut.execute(123, 3, { preview: true, locale: 'en' }) - expect(actual).toEqual(testFileExternalToolUrl) + expect(actual).toEqual(testFileExternalToolResolved) }) test('should return error result on repository error', async () => { const externalToolsRepositoryStub: IExternalToolsRepository = {} as IExternalToolsRepository - externalToolsRepositoryStub.getFileExternalToolUrl = jest + externalToolsRepositoryStub.getFileExternalToolResolved = jest .fn() .mockRejectedValue(new WriteError()) - const sut = new GetFileExternalToolUrl(externalToolsRepositoryStub) + const sut = new GetFileExternalToolResolved(externalToolsRepositoryStub) await expect(sut.execute(123, 3, { preview: true, locale: 'en' })).rejects.toThrow(WriteError) }) From 2435a255867f1edf82a0a1298fe21f00fd283f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 00:34:47 -0300 Subject: [PATCH 078/123] feat: add contentType to ExternalTool and ExternalToolPayload interfaces --- src/externalTools/domain/models/ExternalTool.ts | 1 + src/externalTools/infra/transformers/ExternalToolPayload.ts | 1 + .../infra/transformers/externalToolsTransformer.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/externalTools/domain/models/ExternalTool.ts b/src/externalTools/domain/models/ExternalTool.ts index ea783c13..ce315573 100644 --- a/src/externalTools/domain/models/ExternalTool.ts +++ b/src/externalTools/domain/models/ExternalTool.ts @@ -4,6 +4,7 @@ export interface ExternalTool { description: string types: ToolType[] scope: ToolScope + contentType?: string // Only present when scope is 'file' } export enum ToolType { diff --git a/src/externalTools/infra/transformers/ExternalToolPayload.ts b/src/externalTools/infra/transformers/ExternalToolPayload.ts index b2ce8b33..3fc1401a 100644 --- a/src/externalTools/infra/transformers/ExternalToolPayload.ts +++ b/src/externalTools/infra/transformers/ExternalToolPayload.ts @@ -4,6 +4,7 @@ export interface ExternalToolPayload { description: string types: ToolTypePayload[] scope: ToolScopePayload + contentType?: string // Only present when scope is 'file' } enum ToolTypePayload { diff --git a/src/externalTools/infra/transformers/externalToolsTransformer.ts b/src/externalTools/infra/transformers/externalToolsTransformer.ts index 09898cd5..d0cbb3bd 100644 --- a/src/externalTools/infra/transformers/externalToolsTransformer.ts +++ b/src/externalTools/infra/transformers/externalToolsTransformer.ts @@ -14,6 +14,7 @@ export const externalToolsTransformer = ( displayName: tool.displayName, description: tool.description, types: tool.types as unknown as ExternalTool['types'], - scope: tool.scope as unknown as ExternalTool['scope'] + scope: tool.scope as unknown as ExternalTool['scope'], + contentType: tool.contentType })) } From 99286b18baae8369f2e5042d170416b89a71f5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 1 Sep 2025 08:17:56 -0300 Subject: [PATCH 079/123] test: remove skips --- .../externalTools/ExternalToolsRepository.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/integration/externalTools/ExternalToolsRepository.test.ts b/test/integration/externalTools/ExternalToolsRepository.test.ts index 79b21811..ef2c9248 100644 --- a/test/integration/externalTools/ExternalToolsRepository.test.ts +++ b/test/integration/externalTools/ExternalToolsRepository.test.ts @@ -48,8 +48,7 @@ describe('ExternalToolsRepository', () => { }) }) - // TODO:ME - Skip for now until Backend PR is merged to develop. - describe.skip('getFileExternalToolResolved', () => { + describe('getFileExternalToolResolved', () => { const testCollectionAlias = 'getFileExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' @@ -141,8 +140,7 @@ describe('ExternalToolsRepository', () => { }) }) - // TODO:ME - Skip for now until Backend PR is merged to develop. - describe.skip('getDatasetExternalToolResolved', () => { + describe('getDatasetExternalToolResolved', () => { const testCollectionAlias = 'getDatasetExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' From 6bfc7d6b45c8b7792173a1c4078ba554a8bdb615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 2 Sep 2025 12:01:34 -0300 Subject: [PATCH 080/123] feat: methods and use cases --- docs/useCases.md | 21 ++++ .../models/CollectionDatasetTemplate.ts | 28 ++++++ .../repositories/ICollectionsRepository.ts | 5 +- .../useCases/GetCollectionDatasetTemplates.ts | 25 +++++ src/collections/index.ts | 5 +- .../repositories/CollectionsRepository.ts | 12 ++- .../CollectionDatasetTemplatePayload.ts | 98 +++++++++---------- .../collectionDatasetTemplateTransformer.ts | 53 ++++++++++ 8 files changed, 190 insertions(+), 57 deletions(-) create mode 100644 src/collections/domain/useCases/GetCollectionDatasetTemplates.ts create mode 100644 src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts diff --git a/docs/useCases.md b/docs/useCases.md index f86c117c..1fcb8f0b 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -16,6 +16,7 @@ The different use cases currently available in the package are classified below, - [List All Collection Items](#list-all-collection-items) - [List My Data Collection Items](#list-my-data-collection-items) - [Get Collection Featured Items](#get-collection-featured-items) + - [Get Collection Dataset Templates](#get-collection-dataset-templates) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) @@ -321,6 +322,26 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe If no collection identifier is specified, the default collection identifier; `:root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. +#### Get Collection Dataset Templates + +Returns a [CollectionDatasetTemplate](../src/collections/domain/models/CollectionDatasetTemplate.ts) array containing the dataset templates of the requested collection, given the collection identifier or alias. + +##### Example call: + +```typescript +import { getCollectionDatasetTemplates } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 12345 + +getCollectionDatasetTemplates + .execute(collectionIdOrAlias) + .then((datasetTemplates: CollectionDatasetTemplate[]) => { + /* ... */ + }) +``` + +_See [use case](../src/collections/domain/useCases/GetCollectionDatasetTemplates.ts)_ definition. + ### Collections Write Use Cases #### Create a Collection diff --git a/src/collections/domain/models/CollectionDatasetTemplate.ts b/src/collections/domain/models/CollectionDatasetTemplate.ts index e69de29b..fbb2f105 100644 --- a/src/collections/domain/models/CollectionDatasetTemplate.ts +++ b/src/collections/domain/models/CollectionDatasetTemplate.ts @@ -0,0 +1,28 @@ +import { DatasetLicense, DatasetMetadataFieldValue, TermsOfUse } from '../../../datasets' + +export interface CollectionDatasetTemplate { + id: number + name: string + alias: string + isDefault: boolean + usageCount: number + createTime: string + createDate: string + // 👇 From Edit Template Metadata + datasetFields: DatasetFields + instructions: DatasetTemplateInstruction[] + // 👇 From Edit Template Terms + termsOfUse: TermsOfUse + license?: DatasetLicense // This license property is going to be present if not custom terms are added in the UI +} + +type DatasetFields = Record +interface DatasetFieldInfo { + displayName: string + name: string + fields: DatasetMetadataFieldValue[] +} +export interface DatasetTemplateInstruction { + instructionField: string + instructionText: string +} diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index f78a688c..f5fbf397 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -10,6 +10,7 @@ import { CollectionUserPermissions } from '../models/CollectionUserPermissions' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType' import { CollectionLinks } from '../models/CollectionLinks' +import { CollectionDatasetTemplate } from '../models/CollectionDatasetTemplate' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -60,5 +61,7 @@ export interface ICollectionsRepository { linkingCollectionIdOrAlias: number | string ): Promise getCollectionLinks(collectionIdOrAlias: number | string): Promise - getDatasetTemplates(collectionIdOrAlias: number | string): Promise + getCollectionDatasetTemplates( + collectionIdOrAlias: number | string + ): Promise } diff --git a/src/collections/domain/useCases/GetCollectionDatasetTemplates.ts b/src/collections/domain/useCases/GetCollectionDatasetTemplates.ts new file mode 100644 index 00000000..e7e9e7b1 --- /dev/null +++ b/src/collections/domain/useCases/GetCollectionDatasetTemplates.ts @@ -0,0 +1,25 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { ROOT_COLLECTION_ID } from '../models/Collection' +import { CollectionDatasetTemplate } from '../models/CollectionDatasetTemplate' + +export class GetCollectionDatasetTemplates implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Returns a CollectionDatasetTemplate array containing the dataset templates of the requested collection, given the collection identifier or alias. + * + * @param {number | string} [collectionIdOrAlias = ':root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) + * If this parameter is not set, the default value is: ':root' + * @returns {Promise} + */ + async execute( + collectionIdOrAlias: number | string = ROOT_COLLECTION_ID + ): Promise { + return await this.collectionsRepository.getCollectionDatasetTemplates(collectionIdOrAlias) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 05e49954..3cbbd9a4 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -15,6 +15,7 @@ import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollection import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' +import { GetCollectionDatasetTemplates } from './domain/useCases/GetCollectionDatasetTemplates' const collectionsRepository = new CollectionsRepository() @@ -34,6 +35,7 @@ const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collection const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) +const getCollectionDatasetTemplates = new GetCollectionDatasetTemplates(collectionsRepository) export { getCollection, @@ -51,7 +53,8 @@ export { deleteCollectionFeaturedItem, linkCollection, unlinkCollection, - getCollectionLinks + getCollectionLinks, + getCollectionDatasetTemplates } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 3baba425..436147a0 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -1,3 +1,4 @@ +import { AxiosResponse } from 'axios' import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRepository' import { @@ -38,6 +39,9 @@ import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { ReadError } from '../../../core/domain/repositories/ReadError' import { CollectionLinks } from '../../domain/models/CollectionLinks' +import { CollectionDatasetTemplatePayload } from './transformers/CollectionDatasetTemplatePayload' +import { transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplate } from './transformers/collectionDatasetTemplateTransformer' +import { CollectionDatasetTemplate } from '../../domain/models/CollectionDatasetTemplate' export interface NewCollectionRequestPayload { alias: string @@ -489,9 +493,13 @@ export class CollectionsRepository extends ApiRepository implements ICollections }) } - public async getDatasetTemplates(collectionIdOrAlias: number | string): Promise { + public async getCollectionDatasetTemplates( + collectionIdOrAlias: number | string + ): Promise { return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, true) - .then((response) => response.data.data) + .then((response: AxiosResponse<{ data: CollectionDatasetTemplatePayload[] }>) => + transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplate(response.data.data) + ) .catch((error) => { throw error }) diff --git a/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts b/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts index 1172ee5a..c562b1b3 100644 --- a/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts +++ b/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts @@ -1,72 +1,64 @@ -// TODO:ME - Adding custom terms makes the get dataset templates endpoint throw internal server error +import { MetadataFieldPayload } from '../../../../datasets/infra/repositories/transformers/DatasetPayload' export interface CollectionDatasetTemplatePayload { id: number name: string + dataverseAlias: string isDefault: boolean usageCount: number createTime: string createDate: string - termsOfUseAndAccess: TermsOfUseAndAccess - datasetFields: DatasetFields + // 👇 From Edit Template Metadata + datasetFields: DatasetFieldsPayload instructions: Instruction[] - dataverseAlias: string + // 👇 From Edit Template Terms + termsOfUseAndAccess: { + id: number + fileAccessRequest: boolean + // This license property is going to be present if not custom terms are added in the UI + license?: { + id: number + name: string + shortDescription: string + uri: string + iconUrl?: string + active: boolean + isDefault: boolean + sortOrder: number + rightsIdentifier: string + rightsIdentifierScheme?: string + schemeUri: string + languageCode: string + } + // Below fields are going to be present if are added in "Restricted Files + Terms of Access" + termsOfAccess?: string // This is terms of access for restricted files in the JSF UI + dataAccessPlace?: string + originalArchive?: string + availabilityStatus?: string + sizeOfCollection?: string + studyCompletion?: string + contactForAccess?: string + // Below fields are going to be present if custom terms are added in the UI, they will be mapped and grouped under customTerms + termsOfUse?: string + confidentialityDeclaration?: string + specialPermissions?: string + restrictions?: string + citationRequirements?: string + depositorRequirements?: string + conditions?: string + disclaimer?: string + } } -export interface TermsOfUseAndAccess { - id: number - license: License - // Below fields are going to be present if are added in "Restricted Files + Terms of Access" - termsOfAccess?: string // This is terms of access for restricted files in the JSF UI - dataAccessPlace?: string - originalArchive?: string - availabilityStatus?: string - sizeOfCollection?: string - studyCompletion?: string - // Below fields are going to be present if custom terms are added in the JSF UI - termsOfUse?: string - confidentialityDeclaration?: string - specialPermissions?: string - restrictions?: string - citationRequirements?: string - depositorRequirements?: string - conditions?: string - disclaimer?: string -} +type DatasetFieldsPayload = Record -export interface License { - id: number - name: string - shortDescription: string - uri: string - iconUrl: string - active: boolean - isDefault: boolean - sortOrder: number - rightsIdentifier: string - rightsIdentifierScheme: string - schemeUri: string - languageCode: string -} - -export interface DatasetFields { - citation: Citation -} - -export interface Citation { +interface DatasetFieldInfoPayload { displayName: string name: string - fields: Field[] -} - -export interface Field { - typeName: string - multiple: boolean - typeClass: string - value: string + fields: MetadataFieldPayload[] } -export interface Instruction { +interface Instruction { instructionField: string instructionText: string } diff --git a/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts b/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts new file mode 100644 index 00000000..281ac35e --- /dev/null +++ b/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts @@ -0,0 +1,53 @@ +import { CollectionDatasetTemplate } from '../../../domain/models/CollectionDatasetTemplate' +import { CollectionDatasetTemplatePayload } from './CollectionDatasetTemplatePayload' + +export const transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplate = ( + collectionDatasetTemplatePayload: CollectionDatasetTemplatePayload[] +): CollectionDatasetTemplate[] => { + return collectionDatasetTemplatePayload.map((payload) => { + const collectionDatasetTemplate: CollectionDatasetTemplate = { + id: payload.id, + name: payload.name, + alias: payload.dataverseAlias, + isDefault: payload.isDefault, + usageCount: payload.usageCount, + createTime: payload.createTime, + createDate: payload.createDate, + datasetFields: payload.datasetFields as unknown as CollectionDatasetTemplate['datasetFields'], + instructions: payload.instructions.map((instruction) => ({ + instructionField: instruction.instructionField, + instructionText: instruction.instructionText + })), + termsOfUse: { + termsOfAccess: { + fileAccessRequest: payload.termsOfUseAndAccess.fileAccessRequest, + termsOfAccessForRestrictedFiles: payload.termsOfUseAndAccess.termsOfAccess, + dataAccessPlace: payload.termsOfUseAndAccess.dataAccessPlace, + originalArchive: payload.termsOfUseAndAccess.originalArchive, + availabilityStatus: payload.termsOfUseAndAccess.availabilityStatus, + contactForAccess: payload.termsOfUseAndAccess.contactForAccess, + sizeOfCollection: payload.termsOfUseAndAccess.sizeOfCollection, + studyCompletion: payload.termsOfUseAndAccess.studyCompletion + } + } + } + + if (payload.termsOfUseAndAccess.license) { + collectionDatasetTemplate.license = payload.termsOfUseAndAccess.license + } else { + collectionDatasetTemplate.termsOfUse.customTerms = { + termsOfUse: payload.termsOfUseAndAccess.termsOfUse as string, + confidentialityDeclaration: payload.termsOfUseAndAccess + .confidentialityDeclaration as string, + specialPermissions: payload.termsOfUseAndAccess.specialPermissions as string, + restrictions: payload.termsOfUseAndAccess.restrictions as string, + citationRequirements: payload.termsOfUseAndAccess.citationRequirements as string, + depositorRequirements: payload.termsOfUseAndAccess.depositorRequirements as string, + conditions: payload.termsOfUseAndAccess.conditions as string, + disclaimer: payload.termsOfUseAndAccess.disclaimer as string + } + } + + return collectionDatasetTemplate + }) +} From aa34265ba4513a97fef7436998dbab8a01c8c700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 2 Sep 2025 13:49:33 -0300 Subject: [PATCH 081/123] feat: tansform iconUrl to iconUri --- .../transformers/collectionDatasetTemplateTransformer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts b/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts index 281ac35e..ab2c8d33 100644 --- a/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts +++ b/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts @@ -33,7 +33,11 @@ export const transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplat } if (payload.termsOfUseAndAccess.license) { - collectionDatasetTemplate.license = payload.termsOfUseAndAccess.license + collectionDatasetTemplate.license = { + name: payload.termsOfUseAndAccess.license.name, + uri: payload.termsOfUseAndAccess.license.uri, + iconUri: payload.termsOfUseAndAccess.license.iconUrl + } } else { collectionDatasetTemplate.termsOfUse.customTerms = { termsOfUse: payload.termsOfUseAndAccess.termsOfUse as string, From 6c9fb0e0635c5d6db9be695fa5e327e45b705f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 2 Sep 2025 14:30:52 -0300 Subject: [PATCH 082/123] refactor: move use case to datasets repository --- docs/useCases.md | 40 +++++++++---------- .../repositories/ICollectionsRepository.ts | 4 -- .../useCases/GetCollectionDatasetTemplates.ts | 25 ------------ src/collections/index.ts | 5 +-- .../repositories/CollectionsRepository.ts | 16 -------- .../domain/models/DatasetTemplate.ts} | 4 +- .../repositories/IDatasetsRepository.ts | 2 + .../domain/useCases/GetDatasetTemplates.ts | 25 ++++++++++++ src/datasets/index.ts | 5 ++- .../infra/repositories/DatasetsRepository.ts | 16 ++++++++ .../transformers/DatasetTemplatePayload.ts} | 4 +- .../datasetTemplateTransformers.ts} | 20 +++++----- 12 files changed, 81 insertions(+), 85 deletions(-) delete mode 100644 src/collections/domain/useCases/GetCollectionDatasetTemplates.ts rename src/{collections/domain/models/CollectionDatasetTemplate.ts => datasets/domain/models/DatasetTemplate.ts} (91%) create mode 100644 src/datasets/domain/useCases/GetDatasetTemplates.ts rename src/{collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts => datasets/infra/repositories/transformers/DatasetTemplatePayload.ts} (91%) rename src/{collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts => datasets/infra/repositories/transformers/datasetTemplateTransformers.ts} (75%) diff --git a/docs/useCases.md b/docs/useCases.md index 1fcb8f0b..b2d02ea8 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -16,7 +16,6 @@ The different use cases currently available in the package are classified below, - [List All Collection Items](#list-all-collection-items) - [List My Data Collection Items](#list-my-data-collection-items) - [Get Collection Featured Items](#get-collection-featured-items) - - [Get Collection Dataset Templates](#get-collection-dataset-templates) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) @@ -39,6 +38,7 @@ The different use cases currently available in the package are classified below, - [Get Dataset Versions Summaries](#get-dataset-versions-summaries) - [Get Dataset Linked Collections](#get-dataset-linked-collections) - [Get Dataset Available Categories](#get-dataset-available-categories) + - [Get Dataset Templates](#get-dataset-templates) - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) @@ -322,26 +322,6 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe If no collection identifier is specified, the default collection identifier; `:root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. -#### Get Collection Dataset Templates - -Returns a [CollectionDatasetTemplate](../src/collections/domain/models/CollectionDatasetTemplate.ts) array containing the dataset templates of the requested collection, given the collection identifier or alias. - -##### Example call: - -```typescript -import { getCollectionDatasetTemplates } from '@iqss/dataverse-client-javascript' - -const collectionIdOrAlias = 12345 - -getCollectionDatasetTemplates - .execute(collectionIdOrAlias) - .then((datasetTemplates: CollectionDatasetTemplate[]) => { - /* ... */ - }) -``` - -_See [use case](../src/collections/domain/useCases/GetCollectionDatasetTemplates.ts)_ definition. - ### Collections Write Use Cases #### Create a Collection @@ -1131,6 +1111,24 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetAvailableCategories.ts The `datasetId` parameter is a number for numeric identifiers or string for persistent identifiers. +#### Get Dataset Templates + +Returns a [DatasetTemplate](../src/datasets/domain/models/DatasetTemplate.ts) array containing the dataset templates of the requested collection, given the collection identifier or alias. + +##### Example call: + +```typescript +import { getDatasetTemplates } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 12345 + +getDatasetTemplates.execute(collectionIdOrAlias).then((datasetTemplates: DatasetTemplate[]) => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetTemplates.ts)_ definition. + ## Files ### Files read use cases diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index f5fbf397..820a1356 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -10,7 +10,6 @@ import { CollectionUserPermissions } from '../models/CollectionUserPermissions' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType' import { CollectionLinks } from '../models/CollectionLinks' -import { CollectionDatasetTemplate } from '../models/CollectionDatasetTemplate' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -61,7 +60,4 @@ export interface ICollectionsRepository { linkingCollectionIdOrAlias: number | string ): Promise getCollectionLinks(collectionIdOrAlias: number | string): Promise - getCollectionDatasetTemplates( - collectionIdOrAlias: number | string - ): Promise } diff --git a/src/collections/domain/useCases/GetCollectionDatasetTemplates.ts b/src/collections/domain/useCases/GetCollectionDatasetTemplates.ts deleted file mode 100644 index e7e9e7b1..00000000 --- a/src/collections/domain/useCases/GetCollectionDatasetTemplates.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { UseCase } from '../../../core/domain/useCases/UseCase' -import { ICollectionsRepository } from '../repositories/ICollectionsRepository' -import { ROOT_COLLECTION_ID } from '../models/Collection' -import { CollectionDatasetTemplate } from '../models/CollectionDatasetTemplate' - -export class GetCollectionDatasetTemplates implements UseCase { - private collectionsRepository: ICollectionsRepository - - constructor(collectionsRepository: ICollectionsRepository) { - this.collectionsRepository = collectionsRepository - } - - /** - * Returns a CollectionDatasetTemplate array containing the dataset templates of the requested collection, given the collection identifier or alias. - * - * @param {number | string} [collectionIdOrAlias = ':root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) - * If this parameter is not set, the default value is: ':root' - * @returns {Promise} - */ - async execute( - collectionIdOrAlias: number | string = ROOT_COLLECTION_ID - ): Promise { - return await this.collectionsRepository.getCollectionDatasetTemplates(collectionIdOrAlias) - } -} diff --git a/src/collections/index.ts b/src/collections/index.ts index 3cbbd9a4..05e49954 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -15,7 +15,6 @@ import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollection import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' -import { GetCollectionDatasetTemplates } from './domain/useCases/GetCollectionDatasetTemplates' const collectionsRepository = new CollectionsRepository() @@ -35,7 +34,6 @@ const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collection const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) -const getCollectionDatasetTemplates = new GetCollectionDatasetTemplates(collectionsRepository) export { getCollection, @@ -53,8 +51,7 @@ export { deleteCollectionFeaturedItem, linkCollection, unlinkCollection, - getCollectionLinks, - getCollectionDatasetTemplates + getCollectionLinks } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 436147a0..d16c2775 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -1,4 +1,3 @@ -import { AxiosResponse } from 'axios' import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRepository' import { @@ -39,9 +38,6 @@ import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { ReadError } from '../../../core/domain/repositories/ReadError' import { CollectionLinks } from '../../domain/models/CollectionLinks' -import { CollectionDatasetTemplatePayload } from './transformers/CollectionDatasetTemplatePayload' -import { transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplate } from './transformers/collectionDatasetTemplateTransformer' -import { CollectionDatasetTemplate } from '../../domain/models/CollectionDatasetTemplate' export interface NewCollectionRequestPayload { alias: string @@ -492,16 +488,4 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } - - public async getCollectionDatasetTemplates( - collectionIdOrAlias: number | string - ): Promise { - return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, true) - .then((response: AxiosResponse<{ data: CollectionDatasetTemplatePayload[] }>) => - transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplate(response.data.data) - ) - .catch((error) => { - throw error - }) - } } diff --git a/src/collections/domain/models/CollectionDatasetTemplate.ts b/src/datasets/domain/models/DatasetTemplate.ts similarity index 91% rename from src/collections/domain/models/CollectionDatasetTemplate.ts rename to src/datasets/domain/models/DatasetTemplate.ts index fbb2f105..9ddd8fad 100644 --- a/src/collections/domain/models/CollectionDatasetTemplate.ts +++ b/src/datasets/domain/models/DatasetTemplate.ts @@ -1,6 +1,6 @@ -import { DatasetLicense, DatasetMetadataFieldValue, TermsOfUse } from '../../../datasets' +import { DatasetLicense, DatasetMetadataFieldValue, TermsOfUse } from './Dataset' -export interface CollectionDatasetTemplate { +export interface DatasetTemplate { id: number name: string alias: string diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 1b33eb14..499dfed0 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -12,6 +12,7 @@ import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' +import { DatasetTemplate } from '../models/DatasetTemplate' export interface IDatasetsRepository { getDataset( @@ -74,4 +75,5 @@ export interface IDatasetsRepository { format: CitationFormat, includeDeaccessioned?: boolean ): Promise + getDatasetTemplates(collectionIdOrAlias: number | string): Promise } diff --git a/src/datasets/domain/useCases/GetDatasetTemplates.ts b/src/datasets/domain/useCases/GetDatasetTemplates.ts new file mode 100644 index 00000000..6878e625 --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetTemplates.ts @@ -0,0 +1,25 @@ +import { ROOT_COLLECTION_ID } from '../../../collections/domain/models/Collection' +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetTemplate } from '../models/DatasetTemplate' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class GetDatasetTemplates implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns a DatasetTemplate array containing the dataset templates of the requested collection, given the collection identifier or alias. + * + * @param {number | string} [collectionIdOrAlias = ':root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) + * If this parameter is not set, the default value is: ':root' + * @returns {Promise} + */ + async execute( + collectionIdOrAlias: number | string = ROOT_COLLECTION_ID + ): Promise { + return await this.datasetsRepository.getDatasetTemplates(collectionIdOrAlias) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 36b8c6b3..aadd5fa8 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -25,6 +25,7 @@ import { UnlinkDataset } from './domain/useCases/UnlinkDataset' import { GetDatasetLinkedCollections } from './domain/useCases/GetDatasetLinkedCollections' import { GetDatasetAvailableCategories } from './domain/useCases/GetDatasetAvailableCategories' import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' +import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' const datasetsRepository = new DatasetsRepository() @@ -64,6 +65,7 @@ const unlinkDataset = new UnlinkDataset(datasetsRepository) const getDatasetLinkedCollections = new GetDatasetLinkedCollections(datasetsRepository) const getDatasetAvailableCategories = new GetDatasetAvailableCategories(datasetsRepository) const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) +const getDatasetTemplates = new GetDatasetTemplates(datasetsRepository) export { getDataset, @@ -86,7 +88,8 @@ export { unlinkDataset, getDatasetLinkedCollections, getDatasetAvailableCategories, - getDatasetCitationInOtherFormats + getDatasetCitationInOtherFormats, + getDatasetTemplates } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 2040f763..9a38eaae 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -1,3 +1,4 @@ +import { AxiosResponse } from 'axios' import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { IDatasetsRepository } from '../../domain/repositories/IDatasetsRepository' import { Dataset, VersionUpdateType } from '../../domain/models/Dataset' @@ -24,6 +25,9 @@ import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollec import { CitationFormat } from '../../domain/models/CitationFormat' import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers' import { FormattedCitation } from '../../domain/models/FormattedCitation' +import { DatasetTemplate } from '../../domain/models/DatasetTemplate' +import { DatasetTemplatePayload } from './transformers/DatasetTemplatePayload' +import { transformDatasetTemplatePayloadToDatasetTemplate } from './transformers/datasetTemplateTransformers' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -357,4 +361,16 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async getDatasetTemplates( + collectionIdOrAlias: number | string + ): Promise { + return this.doGet(`/dataverses/${collectionIdOrAlias}/templates`, true) + .then((response: AxiosResponse<{ data: DatasetTemplatePayload[] }>) => + transformDatasetTemplatePayloadToDatasetTemplate(response.data.data) + ) + .catch((error) => { + throw error + }) + } } diff --git a/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts b/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts similarity index 91% rename from src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts rename to src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts index c562b1b3..167bc790 100644 --- a/src/collections/infra/repositories/transformers/CollectionDatasetTemplatePayload.ts +++ b/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts @@ -1,6 +1,6 @@ -import { MetadataFieldPayload } from '../../../../datasets/infra/repositories/transformers/DatasetPayload' +import { MetadataFieldPayload } from './DatasetPayload' -export interface CollectionDatasetTemplatePayload { +export interface DatasetTemplatePayload { id: number name: string dataverseAlias: string diff --git a/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts similarity index 75% rename from src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts rename to src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts index ab2c8d33..b4148c1c 100644 --- a/src/collections/infra/repositories/transformers/collectionDatasetTemplateTransformer.ts +++ b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts @@ -1,11 +1,11 @@ -import { CollectionDatasetTemplate } from '../../../domain/models/CollectionDatasetTemplate' -import { CollectionDatasetTemplatePayload } from './CollectionDatasetTemplatePayload' +import { DatasetTemplate } from '../../../domain/models/DatasetTemplate' +import { DatasetTemplatePayload } from './DatasetTemplatePayload' -export const transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplate = ( - collectionDatasetTemplatePayload: CollectionDatasetTemplatePayload[] -): CollectionDatasetTemplate[] => { +export const transformDatasetTemplatePayloadToDatasetTemplate = ( + collectionDatasetTemplatePayload: DatasetTemplatePayload[] +): DatasetTemplate[] => { return collectionDatasetTemplatePayload.map((payload) => { - const collectionDatasetTemplate: CollectionDatasetTemplate = { + const datasetTemplate: DatasetTemplate = { id: payload.id, name: payload.name, alias: payload.dataverseAlias, @@ -13,7 +13,7 @@ export const transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplat usageCount: payload.usageCount, createTime: payload.createTime, createDate: payload.createDate, - datasetFields: payload.datasetFields as unknown as CollectionDatasetTemplate['datasetFields'], + datasetFields: payload.datasetFields as unknown as DatasetTemplate['datasetFields'], instructions: payload.instructions.map((instruction) => ({ instructionField: instruction.instructionField, instructionText: instruction.instructionText @@ -33,13 +33,13 @@ export const transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplat } if (payload.termsOfUseAndAccess.license) { - collectionDatasetTemplate.license = { + datasetTemplate.license = { name: payload.termsOfUseAndAccess.license.name, uri: payload.termsOfUseAndAccess.license.uri, iconUri: payload.termsOfUseAndAccess.license.iconUrl } } else { - collectionDatasetTemplate.termsOfUse.customTerms = { + datasetTemplate.termsOfUse.customTerms = { termsOfUse: payload.termsOfUseAndAccess.termsOfUse as string, confidentialityDeclaration: payload.termsOfUseAndAccess .confidentialityDeclaration as string, @@ -52,6 +52,6 @@ export const transformCollectionDatasetTemplatePayloadToCollectionDatasetTemplat } } - return collectionDatasetTemplate + return datasetTemplate }) } From 9eb5ab8cfba6c46db901983b67ecf28de82ba4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 2 Sep 2025 15:32:34 -0300 Subject: [PATCH 083/123] feat: change property name to avoid confusion --- src/datasets/domain/models/DatasetTemplate.ts | 2 +- .../repositories/transformers/datasetTemplateTransformers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datasets/domain/models/DatasetTemplate.ts b/src/datasets/domain/models/DatasetTemplate.ts index 9ddd8fad..ba5ddfd0 100644 --- a/src/datasets/domain/models/DatasetTemplate.ts +++ b/src/datasets/domain/models/DatasetTemplate.ts @@ -3,7 +3,7 @@ import { DatasetLicense, DatasetMetadataFieldValue, TermsOfUse } from './Dataset export interface DatasetTemplate { id: number name: string - alias: string + collectionAlias: string isDefault: boolean usageCount: number createTime: string diff --git a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts index b4148c1c..6a4062d5 100644 --- a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts @@ -8,7 +8,7 @@ export const transformDatasetTemplatePayloadToDatasetTemplate = ( const datasetTemplate: DatasetTemplate = { id: payload.id, name: payload.name, - alias: payload.dataverseAlias, + collectionAlias: payload.dataverseAlias, isDefault: payload.isDefault, usageCount: payload.usageCount, createTime: payload.createTime, From 9b4688747e4c73675fa8d127085cbc37e26155e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 3 Sep 2025 08:00:42 -0300 Subject: [PATCH 084/123] refactor: use types from model instead of repeating --- .../infra/transformers/ExternalToolPayload.ts | 18 ++++-------------- .../transformers/externalToolsTransformer.ts | 4 ++-- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/externalTools/infra/transformers/ExternalToolPayload.ts b/src/externalTools/infra/transformers/ExternalToolPayload.ts index 3fc1401a..2168db97 100644 --- a/src/externalTools/infra/transformers/ExternalToolPayload.ts +++ b/src/externalTools/infra/transformers/ExternalToolPayload.ts @@ -1,20 +1,10 @@ +import { ToolScope, ToolType } from '../../domain/models/ExternalTool' + export interface ExternalToolPayload { id: number displayName: string description: string - types: ToolTypePayload[] - scope: ToolScopePayload + types: ToolType[] + scope: ToolScope contentType?: string // Only present when scope is 'file' } - -enum ToolTypePayload { - Explore = 'explore', - Configure = 'configure', - Preview = 'preview', - Query = 'query' -} - -enum ToolScopePayload { - Dataset = 'dataset', - File = 'file' -} diff --git a/src/externalTools/infra/transformers/externalToolsTransformer.ts b/src/externalTools/infra/transformers/externalToolsTransformer.ts index d0cbb3bd..6c52f2ca 100644 --- a/src/externalTools/infra/transformers/externalToolsTransformer.ts +++ b/src/externalTools/infra/transformers/externalToolsTransformer.ts @@ -13,8 +13,8 @@ export const externalToolsTransformer = ( id: tool.id, displayName: tool.displayName, description: tool.description, - types: tool.types as unknown as ExternalTool['types'], - scope: tool.scope as unknown as ExternalTool['scope'], + types: tool.types, + scope: tool.scope, contentType: tool.contentType })) } From 70276ca8bd89f508e90501e615a2ebcac4ac20b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 4 Sep 2025 10:34:11 -0300 Subject: [PATCH 085/123] feat: change model returned --- src/datasets/domain/models/DatasetTemplate.ts | 10 ++-------- .../transformers/datasetTemplateTransformers.ts | 3 ++- .../repositories/transformers/datasetTransformers.ts | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/datasets/domain/models/DatasetTemplate.ts b/src/datasets/domain/models/DatasetTemplate.ts index ba5ddfd0..c0afb505 100644 --- a/src/datasets/domain/models/DatasetTemplate.ts +++ b/src/datasets/domain/models/DatasetTemplate.ts @@ -1,4 +1,4 @@ -import { DatasetLicense, DatasetMetadataFieldValue, TermsOfUse } from './Dataset' +import { DatasetLicense, DatasetMetadataBlocks, TermsOfUse } from './Dataset' export interface DatasetTemplate { id: number @@ -9,19 +9,13 @@ export interface DatasetTemplate { createTime: string createDate: string // 👇 From Edit Template Metadata - datasetFields: DatasetFields + datasetMetadataBlocks: DatasetMetadataBlocks instructions: DatasetTemplateInstruction[] // 👇 From Edit Template Terms termsOfUse: TermsOfUse license?: DatasetLicense // This license property is going to be present if not custom terms are added in the UI } -type DatasetFields = Record -interface DatasetFieldInfo { - displayName: string - name: string - fields: DatasetMetadataFieldValue[] -} export interface DatasetTemplateInstruction { instructionField: string instructionText: string diff --git a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts index 6a4062d5..ff6591c7 100644 --- a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts @@ -1,5 +1,6 @@ import { DatasetTemplate } from '../../../domain/models/DatasetTemplate' import { DatasetTemplatePayload } from './DatasetTemplatePayload' +import { transformPayloadToDatasetMetadataBlocks } from './datasetTransformers' export const transformDatasetTemplatePayloadToDatasetTemplate = ( collectionDatasetTemplatePayload: DatasetTemplatePayload[] @@ -13,7 +14,7 @@ export const transformDatasetTemplatePayloadToDatasetTemplate = ( usageCount: payload.usageCount, createTime: payload.createTime, createDate: payload.createDate, - datasetFields: payload.datasetFields as unknown as DatasetTemplate['datasetFields'], + datasetMetadataBlocks: transformPayloadToDatasetMetadataBlocks(payload.datasetFields, false), instructions: payload.instructions.map((instruction) => ({ instructionField: instruction.instructionField, instructionText: instruction.instructionText diff --git a/src/datasets/infra/repositories/transformers/datasetTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTransformers.ts index e5e88ebd..9890d975 100644 --- a/src/datasets/infra/repositories/transformers/datasetTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTransformers.ts @@ -325,7 +325,7 @@ const transformPayloadText = ( return keepRawFields ? text : transformHtmlToMarkdown(text) } -const transformPayloadToDatasetMetadataBlocks = ( +export const transformPayloadToDatasetMetadataBlocks = ( metadataBlocksPayload: MetadataBlocksPayload, keepRawFields: boolean ): DatasetMetadataBlocks => { From 4ce6b26294410e20d278ddf0fca8d56362b7be70 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 4 Sep 2025 09:58:46 -0400 Subject: [PATCH 086/123] feat: new use case for licenses --- docs/useCases.md | 22 +++++++ src/index.ts | 1 + src/licenses/domain/models/License.ts | 8 +++ .../repositories/ILicensesRepository.ts | 5 ++ .../transformers/LicensePayload.ts | 14 +++++ .../transformers/licenseTransformers.ts | 15 +++++ .../useCases/GetAvailableStandardLicenses.ts | 20 +++++++ src/licenses/index.ts | 10 ++++ .../infra/repositories/LicensesRepository.ts | 15 +++++ .../GetAvailableStandardLicenses.test.ts | 51 +++++++++++++++++ .../licenses/LicensesRepository.test.ts | 57 +++++++++++++++++++ .../GetAvailableStandardLicenses.test.ts | 52 +++++++++++++++++ 12 files changed, 270 insertions(+) create mode 100644 src/licenses/domain/models/License.ts create mode 100644 src/licenses/domain/repositories/ILicensesRepository.ts create mode 100644 src/licenses/domain/repositories/transformers/LicensePayload.ts create mode 100644 src/licenses/domain/repositories/transformers/licenseTransformers.ts create mode 100644 src/licenses/domain/useCases/GetAvailableStandardLicenses.ts create mode 100644 src/licenses/index.ts create mode 100644 src/licenses/infra/repositories/LicensesRepository.ts create mode 100644 test/functional/licenses/GetAvailableStandardLicenses.test.ts create mode 100644 test/integration/licenses/LicensesRepository.test.ts create mode 100644 test/unit/licenses/GetAvailableStandardLicenses.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 773c9122..ff7a266a 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -89,6 +89,8 @@ The different use cases currently available in the package are classified below, - [Get Maximum Embargo Duration In Months](#get-maximum-embargo-duration-in-months) - [Get ZIP Download Limit](#get-zip-download-limit) - [Get Application Terms of Use](#get-application-terms-of-use) +- [Licenses](#Licenses) + - [Get Available Standard License Terms](#get-available-standard-license-terms) - [Contact](#Contact) - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) - [Search](#Search) @@ -2084,6 +2086,26 @@ getApplicationTermsOfUse.execute().then((termsOfUse: string) => { _See [use case](../src/info/domain/useCases/GetApplicationTermsOfUse.ts) implementation_. +## Licenses + +### Get Available Standard License Terms + +Returns a list of available standard licenses that can be selected for a dataset. + +##### Example call: + +```typescript +import { getAvailableStandardLicenses, License } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getAvailableStandardLicenses.execute().then((licenses: License[]) => { + /* ... */ +}) +``` + +_See [use case](../src/licenses/domain/useCases/GetAvailableStandardLicenses.ts) implementation_. + ## Contact #### Send Feedback to Object Contacts diff --git a/src/index.ts b/src/index.ts index 2fb70d9e..160e39e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,3 +9,4 @@ export * from './metadataBlocks' export * from './files' export * from './contactInfo' export * from './search' +export * from './licenses' diff --git a/src/licenses/domain/models/License.ts b/src/licenses/domain/models/License.ts new file mode 100644 index 00000000..d1012cb2 --- /dev/null +++ b/src/licenses/domain/models/License.ts @@ -0,0 +1,8 @@ +export interface License { + id: number + name: string + uri: string + iconUrl: string + active: boolean + isDefault: boolean +} diff --git a/src/licenses/domain/repositories/ILicensesRepository.ts b/src/licenses/domain/repositories/ILicensesRepository.ts new file mode 100644 index 00000000..45309a89 --- /dev/null +++ b/src/licenses/domain/repositories/ILicensesRepository.ts @@ -0,0 +1,5 @@ +import { License } from '../models/License' + +export interface ILicensesRepository { + getAvailableStandardLicenses(): Promise +} diff --git a/src/licenses/domain/repositories/transformers/LicensePayload.ts b/src/licenses/domain/repositories/transformers/LicensePayload.ts new file mode 100644 index 00000000..02f4bc58 --- /dev/null +++ b/src/licenses/domain/repositories/transformers/LicensePayload.ts @@ -0,0 +1,14 @@ +export interface LicensePayload { + id: number + name: string + shortDescription: string + uri: string + iconUrl: string + active: boolean + isDefault: boolean + sortOrder: number + rightsIdentifier: string + rightsIdentifierScheme: string + schemeUri: string + languageCode: string +} diff --git a/src/licenses/domain/repositories/transformers/licenseTransformers.ts b/src/licenses/domain/repositories/transformers/licenseTransformers.ts new file mode 100644 index 00000000..7484c23c --- /dev/null +++ b/src/licenses/domain/repositories/transformers/licenseTransformers.ts @@ -0,0 +1,15 @@ +import { AxiosResponse } from 'axios' +import { License } from '../../models/License' +import { LicensePayload } from './LicensePayload' + +export const transformLicensesResponseToLicenses = (response: AxiosResponse): License[] => { + const payload = response.data.data as LicensePayload[] + return payload.map((license: LicensePayload) => ({ + id: license.id, + name: license.name, + uri: license.uri, + iconUrl: license.iconUrl, + active: license.active, + isDefault: license.isDefault + })) +} diff --git a/src/licenses/domain/useCases/GetAvailableStandardLicenses.ts b/src/licenses/domain/useCases/GetAvailableStandardLicenses.ts new file mode 100644 index 00000000..00517770 --- /dev/null +++ b/src/licenses/domain/useCases/GetAvailableStandardLicenses.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { License } from '../models/License' +import { ILicensesRepository } from '../repositories/ILicensesRepository' + +export class GetAvailableStandardLicenses implements UseCase { + private licensesRepository: ILicensesRepository + + constructor(licensesRepository: ILicensesRepository) { + this.licensesRepository = licensesRepository + } + + /** + * Returns the list of available standard license terms that can be selected for a dataset. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.licensesRepository.getAvailableStandardLicenses() + } +} diff --git a/src/licenses/index.ts b/src/licenses/index.ts new file mode 100644 index 00000000..0d9158f3 --- /dev/null +++ b/src/licenses/index.ts @@ -0,0 +1,10 @@ +import { LicensesRepository } from './infra/repositories/LicensesRepository' +import { GetAvailableStandardLicenses } from './domain/useCases/GetAvailableStandardLicenses' + +const licensesRepository = new LicensesRepository() + +const getAvailableStandardLicenses = new GetAvailableStandardLicenses(licensesRepository) + +export { getAvailableStandardLicenses } + +export { License } from './domain/models/License' diff --git a/src/licenses/infra/repositories/LicensesRepository.ts b/src/licenses/infra/repositories/LicensesRepository.ts new file mode 100644 index 00000000..042fce56 --- /dev/null +++ b/src/licenses/infra/repositories/LicensesRepository.ts @@ -0,0 +1,15 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { ILicensesRepository } from '../../domain/repositories/ILicensesRepository' +import { License } from '../../domain/models/License' + +export class LicensesRepository extends ApiRepository implements ILicensesRepository { + private readonly licensesResourceName: string = 'licenses' + + public async getAvailableStandardLicenses(): Promise { + return this.doGet(this.buildApiEndpoint(this.licensesResourceName)) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } +} diff --git a/test/functional/licenses/GetAvailableStandardLicenses.test.ts b/test/functional/licenses/GetAvailableStandardLicenses.test.ts new file mode 100644 index 00000000..6e54d24c --- /dev/null +++ b/test/functional/licenses/GetAvailableStandardLicenses.test.ts @@ -0,0 +1,51 @@ +import { ApiConfig, getAvailableStandardLicenses, License } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('getAvailableStandardLicenses', () => { + describe('execute', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return available standard license terms', async () => { + const actualLicenses: License[] = await getAvailableStandardLicenses.execute() + const expectedLicenses = [ + { + id: 1, + name: 'CC0 1.0', + shortDescription: 'Creative Commons CC0 1.0 Universal Public Domain Dedication.', + uri: 'http://creativecommons.org/publicdomain/zero/1.0', + iconUrl: 'https://licensebuttons.net/p/zero/1.0/88x31.png', + active: true, + isDefault: true, + sortOrder: 0, + rightsIdentifier: 'CC0-1.0', + rightsIdentifierScheme: 'SPDX', + schemeUri: 'https://spdx.org/licenses/', + languageCode: 'en' + }, + { + id: 2, + name: 'CC BY 4.0', + shortDescription: 'Creative Commons Attribution 4.0 International License.', + uri: 'http://creativecommons.org/licenses/by/4.0', + iconUrl: 'https://licensebuttons.net/l/by/4.0/88x31.png', + active: true, + isDefault: false, + sortOrder: 2, + rightsIdentifier: 'CC-BY-4.0', + rightsIdentifierScheme: 'SPDX', + schemeUri: 'https://spdx.org/licenses/', + languageCode: 'en' + } + ] + + expect(actualLicenses).toEqual(expectedLicenses) + }) + }) +}) diff --git a/test/integration/licenses/LicensesRepository.test.ts b/test/integration/licenses/LicensesRepository.test.ts new file mode 100644 index 00000000..26f27228 --- /dev/null +++ b/test/integration/licenses/LicensesRepository.test.ts @@ -0,0 +1,57 @@ +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { LicensesRepository } from '../../../src/licenses/infra/repositories/LicensesRepository' + +describe('LicensesRepository', () => { + const sut: LicensesRepository = new LicensesRepository() + + describe('getAvailableStandardLicenses', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return list of available standard license terms', async () => { + const actual = await sut.getAvailableStandardLicenses() + + const licenses = [ + { + id: 1, + name: 'CC0 1.0', + shortDescription: 'Creative Commons CC0 1.0 Universal Public Domain Dedication.', + uri: 'http://creativecommons.org/publicdomain/zero/1.0', + iconUrl: 'https://licensebuttons.net/p/zero/1.0/88x31.png', + active: true, + isDefault: true, + sortOrder: 0, + rightsIdentifier: 'CC0-1.0', + rightsIdentifierScheme: 'SPDX', + schemeUri: 'https://spdx.org/licenses/', + languageCode: 'en' + }, + { + id: 2, + name: 'CC BY 4.0', + shortDescription: 'Creative Commons Attribution 4.0 International License.', + uri: 'http://creativecommons.org/licenses/by/4.0', + iconUrl: 'https://licensebuttons.net/l/by/4.0/88x31.png', + active: true, + isDefault: false, + sortOrder: 2, + rightsIdentifier: 'CC-BY-4.0', + rightsIdentifierScheme: 'SPDX', + schemeUri: 'https://spdx.org/licenses/', + languageCode: 'en' + } + ] + + expect(actual).toEqual(licenses) + }) + }) +}) diff --git a/test/unit/licenses/GetAvailableStandardLicenses.test.ts b/test/unit/licenses/GetAvailableStandardLicenses.test.ts new file mode 100644 index 00000000..ea0f7c39 --- /dev/null +++ b/test/unit/licenses/GetAvailableStandardLicenses.test.ts @@ -0,0 +1,52 @@ +import { ReadError } from '../../../src' +import { ILicensesRepository } from '../../../src/licenses/domain/repositories/ILicensesRepository' +import { GetAvailableStandardLicenses } from '../../../src/licenses/domain/useCases/GetAvailableStandardLicenses' + +describe('GetAvailableStandardLicenses', () => { + describe('execute', () => { + test('should return licenses array on repository success', async () => { + const licensesRepositoryStub: ILicensesRepository = {} as ILicensesRepository + + const testLicenses = [ + { + id: 1, + name: 'CC0 1.0', + uri: 'http://creativecommons.org/publicdomain/zero/1.0', + iconUrl: 'https://licensebuttons.net/p/zero/1.0/88x31.png', + active: true, + isDefault: true + }, + { + id: 2, + name: 'CC BY 4.0', + uri: 'http://creativecommons.org/licenses/by/4.0', + iconUrl: 'https://licensebuttons.net/l/by/4.0/88x31.png', + active: true, + isDefault: false + } + ] + + licensesRepositoryStub.getAvailableStandardLicenses = jest + .fn() + .mockResolvedValue(testLicenses) + const sut = new GetAvailableStandardLicenses(licensesRepositoryStub) + + const actual = await sut.execute() + + expect(actual).toEqual(testLicenses) + expect(licensesRepositoryStub.getAvailableStandardLicenses).toHaveBeenCalledTimes(1) + }) + + test('should return error result on repository error', async () => { + const licensesRepositoryStub: ILicensesRepository = {} as ILicensesRepository + const expectedError = new ReadError('Failed to fetch licenses') + licensesRepositoryStub.getAvailableStandardLicenses = jest + .fn() + .mockRejectedValue(expectedError) + const sut = new GetAvailableStandardLicenses(licensesRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + expect(licensesRepositoryStub.getAvailableStandardLicenses).toHaveBeenCalledTimes(1) + }) + }) +}) From 48785eefc95eb9885346cc391781013ff6f05b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 5 Sep 2025 08:02:39 -0300 Subject: [PATCH 087/123] chore: back to unstable image for tests --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index 406a9ae1..e7b54bde 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=11703-return-isDefault-property-get-dataset-templates +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 5cec1d2f97418006389583ebac9ea6b10233fab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 5 Sep 2025 09:07:06 -0300 Subject: [PATCH 088/123] feat: add toolParams, allowedApiCalls and requirements properties --- src/externalTools/domain/models/ExternalTool.ts | 3 +++ src/externalTools/infra/transformers/ExternalToolPayload.ts | 3 +++ .../infra/transformers/externalToolsTransformer.ts | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/externalTools/domain/models/ExternalTool.ts b/src/externalTools/domain/models/ExternalTool.ts index ce315573..0e3acdeb 100644 --- a/src/externalTools/domain/models/ExternalTool.ts +++ b/src/externalTools/domain/models/ExternalTool.ts @@ -5,6 +5,9 @@ export interface ExternalTool { types: ToolType[] scope: ToolScope contentType?: string // Only present when scope is 'file' + toolParameters?: { queryParameters?: Record[] } + allowedApiCalls?: { name: string; httpMethod: string; urlTemplate: string; timeOut: number }[] + requirements?: { auxFilesExist: { formatTag: string; formatVersion: string }[] } } export enum ToolType { diff --git a/src/externalTools/infra/transformers/ExternalToolPayload.ts b/src/externalTools/infra/transformers/ExternalToolPayload.ts index 2168db97..a6f97067 100644 --- a/src/externalTools/infra/transformers/ExternalToolPayload.ts +++ b/src/externalTools/infra/transformers/ExternalToolPayload.ts @@ -7,4 +7,7 @@ export interface ExternalToolPayload { types: ToolType[] scope: ToolScope contentType?: string // Only present when scope is 'file' + toolParameters?: { queryParameters?: Record[] } + allowedApiCalls?: { name: string; httpMethod: string; urlTemplate: string; timeOut: number }[] + requirements?: { auxFilesExist: { formatTag: string; formatVersion: string }[] } } diff --git a/src/externalTools/infra/transformers/externalToolsTransformer.ts b/src/externalTools/infra/transformers/externalToolsTransformer.ts index 6c52f2ca..69fbfdb2 100644 --- a/src/externalTools/infra/transformers/externalToolsTransformer.ts +++ b/src/externalTools/infra/transformers/externalToolsTransformer.ts @@ -15,6 +15,9 @@ export const externalToolsTransformer = ( description: tool.description, types: tool.types, scope: tool.scope, - contentType: tool.contentType + contentType: tool.contentType, + toolParameters: tool.toolParameters, + allowedApiCalls: tool.allowedApiCalls, + requirements: tool.requirements })) } From 5ab585240ae0d2685657aec216ce3ff09035a67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 5 Sep 2025 09:13:31 -0300 Subject: [PATCH 089/123] skip test for now --- .../externalTools/ExternalToolsRepository.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/integration/externalTools/ExternalToolsRepository.test.ts b/test/integration/externalTools/ExternalToolsRepository.test.ts index ef2c9248..de544e34 100644 --- a/test/integration/externalTools/ExternalToolsRepository.test.ts +++ b/test/integration/externalTools/ExternalToolsRepository.test.ts @@ -48,7 +48,8 @@ describe('ExternalToolsRepository', () => { }) }) - describe('getFileExternalToolResolved', () => { + // Skipping until related backed PR is merged + describe.skip('getFileExternalToolResolved', () => { const testCollectionAlias = 'getFileExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' @@ -140,7 +141,7 @@ describe('ExternalToolsRepository', () => { }) }) - describe('getDatasetExternalToolResolved', () => { + describe.skip('getDatasetExternalToolResolved', () => { const testCollectionAlias = 'getDatasetExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' From 8720bdc578df6564dfe80c64a27429eba66d870d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Sun, 7 Sep 2025 23:27:04 -0300 Subject: [PATCH 090/123] test: add integration cases --- .../repositories/CollectionsRepository.ts | 3 - src/datasets/domain/models/DatasetTemplate.ts | 4 +- .../datasets/DatasetsRepository.test.ts | 39 +++++++++++ .../datasets/datasetTemplatesHelper.ts | 67 +++++++++++++++++++ 4 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 test/testHelpers/datasets/datasetTemplatesHelper.ts diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index d16c2775..704367e2 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -451,7 +451,6 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } - public async linkCollection( linkedCollectionIdOrAlias: number | string, linkingCollectionIdOrAlias: number | string @@ -465,7 +464,6 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } - public async unlinkCollection( linkedCollectionIdOrAlias: number | string, linkingCollectionIdOrAlias: number | string @@ -478,7 +476,6 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } - public async getCollectionLinks(collectionIdOrAlias: number | string): Promise { return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/links`, true) .then((response) => { diff --git a/src/datasets/domain/models/DatasetTemplate.ts b/src/datasets/domain/models/DatasetTemplate.ts index c0afb505..e6d7a2e9 100644 --- a/src/datasets/domain/models/DatasetTemplate.ts +++ b/src/datasets/domain/models/DatasetTemplate.ts @@ -1,4 +1,4 @@ -import { DatasetLicense, DatasetMetadataBlocks, TermsOfUse } from './Dataset' +import { DatasetLicense, DatasetMetadataBlock, TermsOfUse } from './Dataset' export interface DatasetTemplate { id: number @@ -9,7 +9,7 @@ export interface DatasetTemplate { createTime: string createDate: string // 👇 From Edit Template Metadata - datasetMetadataBlocks: DatasetMetadataBlocks + datasetMetadataBlocks: DatasetMetadataBlock[] instructions: DatasetTemplateInstruction[] // 👇 From Edit Template Terms termsOfUse: TermsOfUse diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 7962b465..ba9c9d79 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -52,6 +52,10 @@ import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepo import { DirectUploadClient } from '../../../src/files/infra/clients/DirectUploadClient' import { createTestFileUploadDestination } from '../../testHelpers/files/fileUploadDestinationHelper' import { CitationFormat } from '../../../src/datasets/domain/models/CitationFormat' +import { + createDatasetTemplateViaApi, + deleteDatasetTemplateViaApi +} from '../../testHelpers/datasets/datasetTemplatesHelper' const TEST_DIFF_DATASET_DTO: DatasetDTO = { license: { @@ -1649,4 +1653,39 @@ describe('DatasetsRepository', () => { await expect(sut.getDatasetAvailableCategories(nonExistentTestDatasetId)).rejects.toThrow() }) }) + + describe('getDatasetTemplates', () => { + const testCollectionAlias = 'testGetDatasetTemplates' + + beforeAll(async () => { + await createCollectionViaApi(testCollectionAlias) + }) + + afterAll(async () => { + await deleteCollectionViaApi(testCollectionAlias) + }) + + test('should return empty dataset templates', async () => { + const actual = await sut.getDatasetTemplates(testCollectionAlias) + + expect(actual.length).toBe(0) + }) + + test('should return dataset templates for a collection', async () => { + const templateCreated = await createDatasetTemplateViaApi(testCollectionAlias) + + const actual = await sut.getDatasetTemplates(testCollectionAlias) + + expect(actual.length).toBe(1) + + expect(actual[0].name).toBe(templateCreated.name) + expect(actual[0].isDefault).toBe(templateCreated.isDefault) + expect(actual[0].datasetMetadataBlocks.length).toBe(1) + expect(actual[0].datasetMetadataBlocks[0].name).toBe('citation') + expect(actual[0].datasetMetadataBlocks[0].fields.author.length).toBe(1) + expect(actual[0].instructions.length).toBe(templateCreated.instructions.length) + + await deleteDatasetTemplateViaApi(actual[0].id) + }) + }) }) diff --git a/test/testHelpers/datasets/datasetTemplatesHelper.ts b/test/testHelpers/datasets/datasetTemplatesHelper.ts new file mode 100644 index 00000000..1cc87300 --- /dev/null +++ b/test/testHelpers/datasets/datasetTemplatesHelper.ts @@ -0,0 +1,67 @@ +import axios from 'axios' +import { TestConstants } from '../TestConstants' +import { DatasetTemplatePayload } from '../../../src/datasets/infra/repositories/transformers/DatasetTemplatePayload' + +const DATASET_TEMPLATE_DTO = { + name: 'Dataset Template', + isDefault: true, + fields: [ + { + typeName: 'author', + value: [ + { + authorName: { + typeName: 'authorName', + value: 'Belicheck, Bill' + }, + authorAffiliation: { + typeName: 'authorIdentifierScheme', + value: 'ORCID' + } + } + ] + } + ], + instructions: [ + { + instructionField: 'author', + instructionText: 'The author data' + } + ] +} + +const DATAVERSE_API_REQUEST_HEADERS = { + headers: { 'Content-Type': 'application/json', 'X-Dataverse-Key': process.env.TEST_API_KEY } +} + +export async function createDatasetTemplateViaApi( + collectionAlias: string +): Promise { + try { + if (collectionAlias == undefined) { + collectionAlias = ':root' + } + return await axios + .post( + `${TestConstants.TEST_API_URL}/dataverses/${collectionAlias}/templates`, + JSON.stringify(DATASET_TEMPLATE_DTO), + DATAVERSE_API_REQUEST_HEADERS + ) + .then((response) => response.data.data) + } catch (error) { + throw new Error(`Error while creating dataset template in collection ${collectionAlias}`) + } +} + +export async function deleteDatasetTemplateViaApi(templateId: number): Promise { + try { + return await axios + .delete( + `${TestConstants.TEST_API_URL}/admin/template/${templateId}`, + DATAVERSE_API_REQUEST_HEADERS + ) + .then((response) => response.data.data) + } catch (error) { + throw new Error(`Error while deleting dataset template with id ${templateId}`) + } +} From 47d25619da38e84d3b0161f40c0fa2becbcf0326 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 8 Sep 2025 13:26:40 -0400 Subject: [PATCH 091/123] fix: model and payload changes --- src/licenses/domain/models/License.ts | 8 ++++++- .../transformers/LicensePayload.ts | 12 +++++----- .../transformers/licenseTransformers.ts | 13 ++++++++--- .../GetAvailableStandardLicenses.test.ts | 22 ++++++++++++++----- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/licenses/domain/models/License.ts b/src/licenses/domain/models/License.ts index d1012cb2..7f16442e 100644 --- a/src/licenses/domain/models/License.ts +++ b/src/licenses/domain/models/License.ts @@ -1,8 +1,14 @@ export interface License { id: number name: string + shortDescription?: string uri: string - iconUrl: string + iconUri?: string active: boolean isDefault: boolean + sortOrder: number + rightsIdentifier?: string + rightsIdentifierScheme?: string + schemeUri?: string + languageCode?: string } diff --git a/src/licenses/domain/repositories/transformers/LicensePayload.ts b/src/licenses/domain/repositories/transformers/LicensePayload.ts index 02f4bc58..a67228da 100644 --- a/src/licenses/domain/repositories/transformers/LicensePayload.ts +++ b/src/licenses/domain/repositories/transformers/LicensePayload.ts @@ -1,14 +1,14 @@ export interface LicensePayload { id: number name: string - shortDescription: string + shortDescription?: string uri: string - iconUrl: string + iconUrl?: string active: boolean isDefault: boolean sortOrder: number - rightsIdentifier: string - rightsIdentifierScheme: string - schemeUri: string - languageCode: string + rightsIdentifier?: string + rightsIdentifierScheme?: string + schemeUri?: string + languageCode?: string } diff --git a/src/licenses/domain/repositories/transformers/licenseTransformers.ts b/src/licenses/domain/repositories/transformers/licenseTransformers.ts index 7484c23c..38883f3c 100644 --- a/src/licenses/domain/repositories/transformers/licenseTransformers.ts +++ b/src/licenses/domain/repositories/transformers/licenseTransformers.ts @@ -2,14 +2,21 @@ import { AxiosResponse } from 'axios' import { License } from '../../models/License' import { LicensePayload } from './LicensePayload' -export const transformLicensesResponseToLicenses = (response: AxiosResponse): License[] => { +export const transformPayloadToLicense = (response: AxiosResponse): License[] => { const payload = response.data.data as LicensePayload[] + return payload.map((license: LicensePayload) => ({ id: license.id, name: license.name, + shortDescription: license.shortDescription, uri: license.uri, - iconUrl: license.iconUrl, + iconUri: license.iconUrl, // in payload, it is called iconUrl, but iconUri is the name matching everywhere else active: license.active, - isDefault: license.isDefault + isDefault: license.isDefault, + sortOrder: license.sortOrder, + rightsIdentifier: license.rightsIdentifier, + rightsIdentifierScheme: license.rightsIdentifierScheme, + schemeUri: license.schemeUri, + languageCode: license.languageCode })) } diff --git a/test/unit/licenses/GetAvailableStandardLicenses.test.ts b/test/unit/licenses/GetAvailableStandardLicenses.test.ts index ea0f7c39..4c6857b4 100644 --- a/test/unit/licenses/GetAvailableStandardLicenses.test.ts +++ b/test/unit/licenses/GetAvailableStandardLicenses.test.ts @@ -1,4 +1,4 @@ -import { ReadError } from '../../../src' +import { License, ReadError } from '../../../src' import { ILicensesRepository } from '../../../src/licenses/domain/repositories/ILicensesRepository' import { GetAvailableStandardLicenses } from '../../../src/licenses/domain/useCases/GetAvailableStandardLicenses' @@ -7,22 +7,32 @@ describe('GetAvailableStandardLicenses', () => { test('should return licenses array on repository success', async () => { const licensesRepositoryStub: ILicensesRepository = {} as ILicensesRepository - const testLicenses = [ + const testLicenses: License[] = [ { id: 1, name: 'CC0 1.0', uri: 'http://creativecommons.org/publicdomain/zero/1.0', - iconUrl: 'https://licensebuttons.net/p/zero/1.0/88x31.png', + iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png', active: true, - isDefault: true + isDefault: true, + sortOrder: 0, + rightsIdentifier: 'CC0-1.0', + rightsIdentifierScheme: 'SPDX', + schemeUri: 'https://spdx.org/licenses/', + languageCode: 'en' }, { id: 2, name: 'CC BY 4.0', uri: 'http://creativecommons.org/licenses/by/4.0', - iconUrl: 'https://licensebuttons.net/l/by/4.0/88x31.png', + iconUri: 'https://licensebuttons.net/l/by/4.0/88x31.png', active: true, - isDefault: false + isDefault: false, + sortOrder: 2, + rightsIdentifier: 'CC-BY-4.0', + rightsIdentifierScheme: 'SPDX', + schemeUri: 'https://spdx.org/licenses/', + languageCode: 'en' } ] From 1331a81f32c38df461ec1b13a2964d6933fbb9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 8 Sep 2025 14:55:41 -0300 Subject: [PATCH 092/123] feat: use case and docs --- docs/useCases.md | 26 +++++++++++++++++++ .../models/DatasetMetadataExportFormats.ts | 16 ++++++++++++ .../repositories/IDataverseInfoRepository.ts | 2 ++ ...etAvailableDatasetMetadataExportFormats.ts | 22 ++++++++++++++++ src/info/index.ts | 9 ++++++- .../repositories/DataverseInfoRepository.ts | 11 ++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/info/domain/models/DatasetMetadataExportFormats.ts create mode 100644 src/info/domain/useCases/GetAvailableDatasetMetadataExportFormats.ts diff --git a/docs/useCases.md b/docs/useCases.md index f86c117c..4a555c26 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -89,6 +89,7 @@ The different use cases currently available in the package are classified below, - [Get Maximum Embargo Duration In Months](#get-maximum-embargo-duration-in-months) - [Get ZIP Download Limit](#get-zip-download-limit) - [Get Application Terms of Use](#get-application-terms-of-use) + - [Get Available Dataset Metadata Export Formats](#get-available-dataset-metadata-export-formats) - [Contact](#Contact) - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) - [Notifications](#Notifications) @@ -2089,6 +2090,31 @@ getApplicationTermsOfUse.execute().then((termsOfUse: string) => { _See [use case](../src/info/domain/useCases/GetApplicationTermsOfUse.ts) implementation_. +#### Get Available Dataset Metadata Export Formats + +Returns a [DatasetMetadataExportFormats](../src/info/domain/models/DatasetMetadataExportFormats.ts) object containing the available dataset metadata export formats. + +##### Example call: + +```typescript +import { + getAvailableDatasetMetadataExportFormats, + DatasetMetadataExportFormats +} from '@iqss/dataverse-client-javascript' + +/* ... */ + +getAvailableDatasetMetadataExportFormats + .execute() + .then((datasetMetadataExportFormats: DatasetMetadataExportFormats) => { + /* ... */ + }) + +/* ... */ +``` + +_See [use case](../src/info/domain/useCases/GetAvailableDatasetMetadataExportFormats.ts) implementation_. + ## Contact #### Send Feedback to Object Contacts diff --git a/src/info/domain/models/DatasetMetadataExportFormats.ts b/src/info/domain/models/DatasetMetadataExportFormats.ts new file mode 100644 index 00000000..a749b49d --- /dev/null +++ b/src/info/domain/models/DatasetMetadataExportFormats.ts @@ -0,0 +1,16 @@ +export type DatasetMetadataExportFormats = Record + +type DatasetMetadataExportFormat = DatasetMetadataExportFormatBase | XmlDatasetMetadataExportFormat + +interface DatasetMetadataExportFormatBase { + displayName: string + mediaType: string + isHarvestable: boolean + isVisibleInUserInterface: boolean +} + +interface XmlDatasetMetadataExportFormat extends DatasetMetadataExportFormatBase { + XMLNameSpace: string + XMLSchemaLocation: string + XMLSchemaVersion: string +} diff --git a/src/info/domain/repositories/IDataverseInfoRepository.ts b/src/info/domain/repositories/IDataverseInfoRepository.ts index 0ec6c747..e0e85644 100644 --- a/src/info/domain/repositories/IDataverseInfoRepository.ts +++ b/src/info/domain/repositories/IDataverseInfoRepository.ts @@ -1,3 +1,4 @@ +import { DatasetMetadataExportFormats } from '../models/DatasetMetadataExportFormats' import { DataverseVersion } from '../models/DataverseVersion' export interface IDataverseInfoRepository { @@ -5,4 +6,5 @@ export interface IDataverseInfoRepository { getZipDownloadLimit(): Promise getMaxEmbargoDurationInMonths(): Promise getApplicationTermsOfUse(lang?: string): Promise + getAvailableDatasetMetadataExportFormats(): Promise } diff --git a/src/info/domain/useCases/GetAvailableDatasetMetadataExportFormats.ts b/src/info/domain/useCases/GetAvailableDatasetMetadataExportFormats.ts new file mode 100644 index 00000000..6faa99dc --- /dev/null +++ b/src/info/domain/useCases/GetAvailableDatasetMetadataExportFormats.ts @@ -0,0 +1,22 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetMetadataExportFormats } from '../models/DatasetMetadataExportFormats' +import { IDataverseInfoRepository } from '../repositories/IDataverseInfoRepository' + +export class GetAvailableDatasetMetadataExportFormats + implements UseCase +{ + private dataverseInfoRepository: IDataverseInfoRepository + + constructor(dataverseInfoRepository: IDataverseInfoRepository) { + this.dataverseInfoRepository = dataverseInfoRepository + } + + /** + * Returns a DatasetMetadataExportFormats object containing the available dataset metadata export formats. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.dataverseInfoRepository.getAvailableDatasetMetadataExportFormats() + } +} diff --git a/src/info/index.ts b/src/info/index.ts index 049cc48d..3837e282 100644 --- a/src/info/index.ts +++ b/src/info/index.ts @@ -3,6 +3,7 @@ import { GetDataverseVersion } from './domain/useCases/GetDataverseVersion' import { GetZipDownloadLimit } from './domain/useCases/GetZipDownloadLimit' import { GetMaxEmbargoDurationInMonths } from './domain/useCases/GetMaxEmbargoDurationInMonths' import { GetApplicationTermsOfUse } from './domain/useCases/GetApplicationTermsOfUse' +import { GetAvailableDatasetMetadataExportFormats } from './domain/useCases/GetAvailableDatasetMetadataExportFormats' const dataverseInfoRepository = new DataverseInfoRepository() @@ -10,10 +11,16 @@ const getDataverseVersion = new GetDataverseVersion(dataverseInfoRepository) const getZipDownloadLimit = new GetZipDownloadLimit(dataverseInfoRepository) const getMaxEmbargoDurationInMonths = new GetMaxEmbargoDurationInMonths(dataverseInfoRepository) const getApplicationTermsOfUse = new GetApplicationTermsOfUse(dataverseInfoRepository) +const getAvailableDatasetMetadataExportFormats = new GetAvailableDatasetMetadataExportFormats( + dataverseInfoRepository +) export { getDataverseVersion, getZipDownloadLimit, getMaxEmbargoDurationInMonths, - getApplicationTermsOfUse + getApplicationTermsOfUse, + getAvailableDatasetMetadataExportFormats } + +export { DatasetMetadataExportFormats } from './domain/models/DatasetMetadataExportFormats' diff --git a/src/info/infra/repositories/DataverseInfoRepository.ts b/src/info/infra/repositories/DataverseInfoRepository.ts index c4de4a22..5e8aa5e0 100644 --- a/src/info/infra/repositories/DataverseInfoRepository.ts +++ b/src/info/infra/repositories/DataverseInfoRepository.ts @@ -2,6 +2,7 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { IDataverseInfoRepository } from '../../domain/repositories/IDataverseInfoRepository' import { DataverseVersion } from '../../domain/models/DataverseVersion' import { AxiosResponse } from 'axios' +import { DatasetMetadataExportFormats } from '../../domain/models/DatasetMetadataExportFormats' export class DataverseInfoRepository extends ApiRepository implements IDataverseInfoRepository { private readonly infoResourceName: string = 'info' @@ -55,4 +56,14 @@ export class DataverseInfoRepository extends ApiRepository implements IDataverse throw error }) } + + public async getAvailableDatasetMetadataExportFormats(): Promise { + return this.doGet(this.buildApiEndpoint(this.infoResourceName, `exportFormats`)) + .then((response: AxiosResponse<{ data: DatasetMetadataExportFormats }>) => { + return response.data.data + }) + .catch((error) => { + throw error + }) + } } From 4235db545f0b05577212945f5c59946547fd984f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 8 Sep 2025 15:03:20 -0300 Subject: [PATCH 093/123] test: add unit and integration tests --- .../info/DataverseInfoRepository.test.ts | 8 +++ .../unit/info/DataverseInfoRepository.test.ts | 54 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/test/integration/info/DataverseInfoRepository.test.ts b/test/integration/info/DataverseInfoRepository.test.ts index 4fec4669..41487312 100644 --- a/test/integration/info/DataverseInfoRepository.test.ts +++ b/test/integration/info/DataverseInfoRepository.test.ts @@ -73,4 +73,12 @@ describe('DataverseInfoRepository', () => { await deleteApplicationTermsOfUseViaApi() }) }) + + describe('getAvailableDatasetMetadataExportFormats', () => { + test('should return available dataset metadata export formats', async () => { + const actual = await sut.getAvailableDatasetMetadataExportFormats() + + expect(actual).toBeDefined() + }) + }) }) diff --git a/test/unit/info/DataverseInfoRepository.test.ts b/test/unit/info/DataverseInfoRepository.test.ts index fd6dd138..61010810 100644 --- a/test/unit/info/DataverseInfoRepository.test.ts +++ b/test/unit/info/DataverseInfoRepository.test.ts @@ -1,6 +1,6 @@ import axios from 'axios' import { DataverseInfoRepository } from '../../../src/info/infra/repositories/DataverseInfoRepository' -import { ApiConfig, ReadError } from '../../../src' +import { ApiConfig, DatasetMetadataExportFormats, ReadError } from '../../../src' import { TestConstants } from '../../testHelpers/TestConstants' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -188,4 +188,56 @@ describe('DataverseInfoRepository', () => { expect(error).toBeInstanceOf(Error) }) }) + + describe('getAvailableDatasetMetadataExportFormats', () => { + test('should return available dataset metadata export formats on successful response', async () => { + const formats: DatasetMetadataExportFormats = { + OAI_ORE: { + displayName: 'OAI_ORE', + mediaType: 'application/json', + isHarvestable: false, + isVisibleInUserInterface: true + }, + Datacite: { + displayName: 'DataCite', + mediaType: 'application/xml', + isHarvestable: true, + isVisibleInUserInterface: true, + XMLNameSpace: 'http://datacite.org/schema/kernel-4', + XMLSchemaLocation: + 'http://datacite.org/schema/kernel-4 http://schema.datacite.org/meta/kernel-4.5/metadata.xsd', + XMLSchemaVersion: '4.5' + } + } + + const testSuccessfulResponse = { + data: { + status: 'OK', + data: formats + } + } + jest.spyOn(axios, 'get').mockResolvedValue(testSuccessfulResponse) + + const actual = await sut.getAvailableDatasetMetadataExportFormats() + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/info/exportFormats`, + TestConstants.TEST_EXPECTED_UNAUTHENTICATED_REQUEST_CONFIG + ) + expect(actual).toEqual(formats) + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'get').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + let error: ReadError | undefined + await sut.getAvailableDatasetMetadataExportFormats().catch((e) => (error = e)) + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/info/exportFormats`, + TestConstants.TEST_EXPECTED_UNAUTHENTICATED_REQUEST_CONFIG + ) + expect(error).toBeInstanceOf(Error) + }) + }) }) From d06e2cb31a1e8ad89d17a64f56b07b6409834f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 9 Sep 2025 16:12:22 -0300 Subject: [PATCH 094/123] fix: use mapper and correct test --- src/licenses/infra/repositories/LicensesRepository.ts | 3 ++- .../licenses/GetAvailableStandardLicenses.test.ts | 6 +++--- test/integration/licenses/LicensesRepository.test.ts | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/licenses/infra/repositories/LicensesRepository.ts b/src/licenses/infra/repositories/LicensesRepository.ts index 042fce56..2461d8f5 100644 --- a/src/licenses/infra/repositories/LicensesRepository.ts +++ b/src/licenses/infra/repositories/LicensesRepository.ts @@ -1,13 +1,14 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { ILicensesRepository } from '../../domain/repositories/ILicensesRepository' import { License } from '../../domain/models/License' +import { transformPayloadToLicense } from '../../domain/repositories/transformers/licenseTransformers' export class LicensesRepository extends ApiRepository implements ILicensesRepository { private readonly licensesResourceName: string = 'licenses' public async getAvailableStandardLicenses(): Promise { return this.doGet(this.buildApiEndpoint(this.licensesResourceName)) - .then((response) => response.data.data) + .then((response) => transformPayloadToLicense(response)) .catch((error) => { throw error }) diff --git a/test/functional/licenses/GetAvailableStandardLicenses.test.ts b/test/functional/licenses/GetAvailableStandardLicenses.test.ts index 6e54d24c..995551e1 100644 --- a/test/functional/licenses/GetAvailableStandardLicenses.test.ts +++ b/test/functional/licenses/GetAvailableStandardLicenses.test.ts @@ -14,13 +14,13 @@ describe('getAvailableStandardLicenses', () => { test('should return available standard license terms', async () => { const actualLicenses: License[] = await getAvailableStandardLicenses.execute() - const expectedLicenses = [ + const expectedLicenses: License[] = [ { id: 1, name: 'CC0 1.0', shortDescription: 'Creative Commons CC0 1.0 Universal Public Domain Dedication.', uri: 'http://creativecommons.org/publicdomain/zero/1.0', - iconUrl: 'https://licensebuttons.net/p/zero/1.0/88x31.png', + iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png', active: true, isDefault: true, sortOrder: 0, @@ -34,7 +34,7 @@ describe('getAvailableStandardLicenses', () => { name: 'CC BY 4.0', shortDescription: 'Creative Commons Attribution 4.0 International License.', uri: 'http://creativecommons.org/licenses/by/4.0', - iconUrl: 'https://licensebuttons.net/l/by/4.0/88x31.png', + iconUri: 'https://licensebuttons.net/l/by/4.0/88x31.png', active: true, isDefault: false, sortOrder: 2, diff --git a/test/integration/licenses/LicensesRepository.test.ts b/test/integration/licenses/LicensesRepository.test.ts index 26f27228..2f357ccf 100644 --- a/test/integration/licenses/LicensesRepository.test.ts +++ b/test/integration/licenses/LicensesRepository.test.ts @@ -4,6 +4,7 @@ import { } from '../../../src/core/infra/repositories/ApiConfig' import { TestConstants } from '../../testHelpers/TestConstants' import { LicensesRepository } from '../../../src/licenses/infra/repositories/LicensesRepository' +import { License } from '../../../src/licenses/domain/models/License' describe('LicensesRepository', () => { const sut: LicensesRepository = new LicensesRepository() @@ -20,13 +21,13 @@ describe('LicensesRepository', () => { test('should return list of available standard license terms', async () => { const actual = await sut.getAvailableStandardLicenses() - const licenses = [ + const licenses: License[] = [ { id: 1, name: 'CC0 1.0', shortDescription: 'Creative Commons CC0 1.0 Universal Public Domain Dedication.', uri: 'http://creativecommons.org/publicdomain/zero/1.0', - iconUrl: 'https://licensebuttons.net/p/zero/1.0/88x31.png', + iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png', active: true, isDefault: true, sortOrder: 0, @@ -40,7 +41,7 @@ describe('LicensesRepository', () => { name: 'CC BY 4.0', shortDescription: 'Creative Commons Attribution 4.0 International License.', uri: 'http://creativecommons.org/licenses/by/4.0', - iconUrl: 'https://licensebuttons.net/l/by/4.0/88x31.png', + iconUri: 'https://licensebuttons.net/l/by/4.0/88x31.png', active: true, isDefault: false, sortOrder: 2, From 5cad35de1eef4697a18f38b11eb68164e3cc6b9d Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 9 Sep 2025 16:46:29 -0400 Subject: [PATCH 095/123] feat: new use case for getting dataset types #363 --- docs/useCases.md | 19 +++++++ src/datasets/domain/models/DatasetType.ts | 6 +++ .../repositories/IDatasetsRepository.ts | 2 + .../GetDatasetAvailableDatasetTypes.ts | 20 ++++++++ src/datasets/index.ts | 4 ++ .../infra/repositories/DatasetsRepository.ts | 9 ++++ .../GetDatasetAvailableDatasetTypes.test.ts | 29 +++++++++++ .../GetDatasetAvailableDatasetTypes.test.ts | 49 +++++++++++++++++++ 8 files changed, 138 insertions(+) create mode 100644 src/datasets/domain/models/DatasetType.ts create mode 100644 src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts create mode 100644 test/functional/datasets/GetDatasetAvailableDatasetTypes.test.ts create mode 100644 test/unit/datasets/GetDatasetAvailableDatasetTypes.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 7fc7b955..fd97b80a 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -38,6 +38,7 @@ The different use cases currently available in the package are classified below, - [Get Dataset Versions Summaries](#get-dataset-versions-summaries) - [Get Dataset Linked Collections](#get-dataset-linked-collections) - [Get Dataset Available Categories](#get-dataset-available-categories) + - [Get Dataset Available Dataset Types](#get-dataset-available-dataset-types) - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) @@ -1113,6 +1114,24 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetAvailableCategories.ts The `datasetId` parameter is a number for numeric identifiers or string for persistent identifiers. +#### Get Dataset Available Dataset Types + +Returns a list of available dataset types that can be used at dataset creation. By default, only the type "dataset" is returned. + +###### Example call: + +```typescript +import { getDatasetAvailableDatasetTypes } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getDatasetAvailableDatasetTypes.execute().then((categories: String[]) => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts) implementation_. + ## Files ### Files read use cases diff --git a/src/datasets/domain/models/DatasetType.ts b/src/datasets/domain/models/DatasetType.ts new file mode 100644 index 00000000..5475cdaf --- /dev/null +++ b/src/datasets/domain/models/DatasetType.ts @@ -0,0 +1,6 @@ +export interface DatasetType { + id: number + name: string + linkedMetadataBlocks?: string[] + availableLicenses?: string[] +} diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 1b33eb14..76ea2a4e 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -12,6 +12,7 @@ import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' +import { DatasetType } from '../models/DatasetType' export interface IDatasetsRepository { getDataset( @@ -74,4 +75,5 @@ export interface IDatasetsRepository { format: CitationFormat, includeDeaccessioned?: boolean ): Promise + getDatasetAvailableDatasetTypes(): Promise } diff --git a/src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts b/src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts new file mode 100644 index 00000000..c7dce4a5 --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetType } from '../models/DatasetType' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class GetDatasetAvailableDatasetTypes implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns the list of available dataset types that can be selected when creating a dataset. + * + * @returns {Promise} + */ + async execute(): Promise { + return await this.datasetsRepository.getDatasetAvailableDatasetTypes() + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 36b8c6b3..97abcea9 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -24,6 +24,7 @@ import { LinkDataset } from './domain/useCases/LinkDataset' import { UnlinkDataset } from './domain/useCases/UnlinkDataset' import { GetDatasetLinkedCollections } from './domain/useCases/GetDatasetLinkedCollections' import { GetDatasetAvailableCategories } from './domain/useCases/GetDatasetAvailableCategories' +import { GetDatasetAvailableDatasetTypes } from './domain/useCases/GetDatasetAvailableDatasetTypes' import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' const datasetsRepository = new DatasetsRepository() @@ -63,6 +64,7 @@ const linkDataset = new LinkDataset(datasetsRepository) const unlinkDataset = new UnlinkDataset(datasetsRepository) const getDatasetLinkedCollections = new GetDatasetLinkedCollections(datasetsRepository) const getDatasetAvailableCategories = new GetDatasetAvailableCategories(datasetsRepository) +const getDatasetAvailableDatasetTypes = new GetDatasetAvailableDatasetTypes(datasetsRepository) const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) export { @@ -86,6 +88,7 @@ export { unlinkDataset, getDatasetLinkedCollections, getDatasetAvailableCategories, + getDatasetAvailableDatasetTypes, getDatasetCitationInOtherFormats } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' @@ -121,3 +124,4 @@ export { DatasetVersionSummaryStringValues } from './domain/models/DatasetVersionSummaryInfo' export { DatasetLinkedCollection } from './domain/models/DatasetLinkedCollection' +export { DatasetType } from './domain/models/DatasetType' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 2040f763..645d6c51 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -24,6 +24,7 @@ import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollec import { CitationFormat } from '../../domain/models/CitationFormat' import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers' import { FormattedCitation } from '../../domain/models/FormattedCitation' +import { DatasetType } from '../../domain/models/DatasetType' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -357,4 +358,12 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async getDatasetAvailableDatasetTypes(): Promise { + return this.doGet(this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes')) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } } diff --git a/test/functional/datasets/GetDatasetAvailableDatasetTypes.test.ts b/test/functional/datasets/GetDatasetAvailableDatasetTypes.test.ts new file mode 100644 index 00000000..14a1a2fd --- /dev/null +++ b/test/functional/datasets/GetDatasetAvailableDatasetTypes.test.ts @@ -0,0 +1,29 @@ +import { ApiConfig, DatasetType, getDatasetAvailableDatasetTypes } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('getDatasetAvailableDatasetTypes', () => { + describe('execute', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return available dataset types', async () => { + const actualDatasetTypes: DatasetType[] = await getDatasetAvailableDatasetTypes.execute() + const expectedDatasetTypes = [ + { + id: 1, + name: 'dataset', + linkedMetadataBlocks: [], + availableLicenses: [] + } + ] + + expect(actualDatasetTypes).toEqual(expectedDatasetTypes) + }) + }) +}) diff --git a/test/unit/datasets/GetDatasetAvailableDatasetTypes.test.ts b/test/unit/datasets/GetDatasetAvailableDatasetTypes.test.ts new file mode 100644 index 00000000..b8768f92 --- /dev/null +++ b/test/unit/datasets/GetDatasetAvailableDatasetTypes.test.ts @@ -0,0 +1,49 @@ +import { ReadError } from '../../../src' +import { DatasetType } from '../../../src' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { GetDatasetAvailableDatasetTypes } from '../../../src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes' + +describe('GetDatasetAvailableDatasetTypes', () => { + describe('execute', () => { + test('should return datasetTypes array on repository success', async () => { + const datasetTypesRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + + const testDatasetTypes: DatasetType[] = [ + { + id: 1, + name: 'dataset', + linkedMetadataBlocks: [], + availableLicenses: [] + }, + { + id: 2, + name: 'software', + linkedMetadataBlocks: ['codeMeta20'], + availableLicenses: ['MIT', 'Apache-2.0'] + } + ] + + datasetTypesRepositoryStub.getDatasetAvailableDatasetTypes = jest + .fn() + .mockResolvedValue(testDatasetTypes) + const sut = new GetDatasetAvailableDatasetTypes(datasetTypesRepositoryStub) + + const actual = await sut.execute() + + expect(actual).toEqual(testDatasetTypes) + expect(datasetTypesRepositoryStub.getDatasetAvailableDatasetTypes).toHaveBeenCalledTimes(1) + }) + + test('should return error result on repository error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + const expectedError = new ReadError('Failed to fetch dataset types') + datasetsRepositoryStub.getDatasetAvailableDatasetTypes = jest + .fn() + .mockRejectedValue(expectedError) + const sut = new GetDatasetAvailableDatasetTypes(datasetsRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + expect(datasetsRepositoryStub.getDatasetAvailableDatasetTypes).toHaveBeenCalledTimes(1) + }) + }) +}) From 0ec433f954435b5f42e4df7bbb86f053ef1dd154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 10 Sep 2025 08:13:23 -0300 Subject: [PATCH 096/123] refactor: update license handling across dataset models and transformers --- src/datasets/domain/models/Dataset.ts | 7 ++-- src/datasets/domain/models/DatasetTemplate.ts | 5 +-- .../transformers/DatasetPayload.ts | 4 +-- .../transformers/DatasetTemplatePayload.ts | 16 ++-------- .../datasetTemplateTransformers.ts | 9 +++--- .../transformers/datasetTransformers.ts | 6 ++-- .../transformers/licenseTransformers.ts | 32 ++++++++++--------- .../infra/repositories/LicensesRepository.ts | 4 +-- 8 files changed, 35 insertions(+), 48 deletions(-) diff --git a/src/datasets/domain/models/Dataset.ts b/src/datasets/domain/models/Dataset.ts index 51f2c433..caecd3ae 100644 --- a/src/datasets/domain/models/Dataset.ts +++ b/src/datasets/domain/models/Dataset.ts @@ -1,4 +1,5 @@ import { DvObjectOwnerNode } from '../../../core/domain/models/DvObjectOwnerNode' +import { License } from '../../../licenses' export interface Dataset { id: number @@ -32,11 +33,7 @@ export enum DatasetVersionState { DEACCESSIONED = 'DEACCESSIONED' } -export interface DatasetLicense { - name: string - uri: string - iconUri?: string -} +export type DatasetLicense = Pick export interface CustomTerms { termsOfUse: string diff --git a/src/datasets/domain/models/DatasetTemplate.ts b/src/datasets/domain/models/DatasetTemplate.ts index e6d7a2e9..9be71f23 100644 --- a/src/datasets/domain/models/DatasetTemplate.ts +++ b/src/datasets/domain/models/DatasetTemplate.ts @@ -1,4 +1,5 @@ -import { DatasetLicense, DatasetMetadataBlock, TermsOfUse } from './Dataset' +import { DatasetMetadataBlock, TermsOfUse } from './Dataset' +import { License } from '../../../licenses/domain/models/License' export interface DatasetTemplate { id: number @@ -13,7 +14,7 @@ export interface DatasetTemplate { instructions: DatasetTemplateInstruction[] // 👇 From Edit Template Terms termsOfUse: TermsOfUse - license?: DatasetLicense // This license property is going to be present if not custom terms are added in the UI + license?: License // This license property is going to be present if not custom terms are added in the UI } export interface DatasetTemplateInstruction { diff --git a/src/datasets/infra/repositories/transformers/DatasetPayload.ts b/src/datasets/infra/repositories/transformers/DatasetPayload.ts index 6378ac45..bf6a70fb 100644 --- a/src/datasets/infra/repositories/transformers/DatasetPayload.ts +++ b/src/datasets/infra/repositories/transformers/DatasetPayload.ts @@ -13,7 +13,7 @@ export interface DatasetPayload { lastUpdateTime: string releaseTime: string metadataBlocks: MetadataBlocksPayload - license?: LicensePayload + license?: DatasetLicensePayload alternativePersistentId?: string publicationDate?: string citationDate?: string @@ -38,7 +38,7 @@ export interface DatasetPayload { deaccessionNote?: string } -export interface LicensePayload { +export interface DatasetLicensePayload { name: string uri: string iconUri?: string diff --git a/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts b/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts index 167bc790..e43e96eb 100644 --- a/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts +++ b/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts @@ -1,3 +1,4 @@ +import { LicensePayload } from '../../../../licenses/domain/repositories/transformers/LicensePayload' import { MetadataFieldPayload } from './DatasetPayload' export interface DatasetTemplatePayload { @@ -16,20 +17,7 @@ export interface DatasetTemplatePayload { id: number fileAccessRequest: boolean // This license property is going to be present if not custom terms are added in the UI - license?: { - id: number - name: string - shortDescription: string - uri: string - iconUrl?: string - active: boolean - isDefault: boolean - sortOrder: number - rightsIdentifier: string - rightsIdentifierScheme?: string - schemeUri: string - languageCode: string - } + license?: LicensePayload // Below fields are going to be present if are added in "Restricted Files + Terms of Access" termsOfAccess?: string // This is terms of access for restricted files in the JSF UI dataAccessPlace?: string diff --git a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts index ff6591c7..32486199 100644 --- a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts @@ -1,3 +1,4 @@ +import { transformPayloadLicenseToLicense } from '../../../../licenses/domain/repositories/transformers/licenseTransformers' import { DatasetTemplate } from '../../../domain/models/DatasetTemplate' import { DatasetTemplatePayload } from './DatasetTemplatePayload' import { transformPayloadToDatasetMetadataBlocks } from './datasetTransformers' @@ -34,11 +35,9 @@ export const transformDatasetTemplatePayloadToDatasetTemplate = ( } if (payload.termsOfUseAndAccess.license) { - datasetTemplate.license = { - name: payload.termsOfUseAndAccess.license.name, - uri: payload.termsOfUseAndAccess.license.uri, - iconUri: payload.termsOfUseAndAccess.license.iconUrl - } + datasetTemplate.license = transformPayloadLicenseToLicense( + payload.termsOfUseAndAccess.license + ) } else { datasetTemplate.termsOfUse.customTerms = { termsOfUse: payload.termsOfUseAndAccess.termsOfUse as string, diff --git a/src/datasets/infra/repositories/transformers/datasetTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTransformers.ts index 9890d975..7d771fe5 100644 --- a/src/datasets/infra/repositories/transformers/datasetTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTransformers.ts @@ -11,7 +11,7 @@ import { import { AxiosResponse } from 'axios' import { DatasetPayload, - LicensePayload, + DatasetLicensePayload, MetadataFieldPayload, MetadataBlocksPayload, MetadataFieldValuePayload, @@ -261,7 +261,7 @@ export const transformVersionPayloadToDataset = ( } if ('license' in versionPayload) { datasetModel.license = transformPayloadToDatasetLicense( - versionPayload.license as LicensePayload + versionPayload.license as DatasetLicensePayload ) } else { datasetModel.termsOfUse.customTerms = { @@ -297,7 +297,7 @@ export const transformVersionPayloadToDataset = ( } const transformPayloadToDatasetLicense = ( - licensePayload: LicensePayload + licensePayload: DatasetLicensePayload ): DatasetLicense | undefined => { if (!licensePayload) { return undefined diff --git a/src/licenses/domain/repositories/transformers/licenseTransformers.ts b/src/licenses/domain/repositories/transformers/licenseTransformers.ts index 38883f3c..e36347d0 100644 --- a/src/licenses/domain/repositories/transformers/licenseTransformers.ts +++ b/src/licenses/domain/repositories/transformers/licenseTransformers.ts @@ -2,21 +2,23 @@ import { AxiosResponse } from 'axios' import { License } from '../../models/License' import { LicensePayload } from './LicensePayload' -export const transformPayloadToLicense = (response: AxiosResponse): License[] => { +export const transformPayloadToLicenses = (response: AxiosResponse): License[] => { const payload = response.data.data as LicensePayload[] - return payload.map((license: LicensePayload) => ({ - id: license.id, - name: license.name, - shortDescription: license.shortDescription, - uri: license.uri, - iconUri: license.iconUrl, // in payload, it is called iconUrl, but iconUri is the name matching everywhere else - active: license.active, - isDefault: license.isDefault, - sortOrder: license.sortOrder, - rightsIdentifier: license.rightsIdentifier, - rightsIdentifierScheme: license.rightsIdentifierScheme, - schemeUri: license.schemeUri, - languageCode: license.languageCode - })) + return payload.map((license: LicensePayload) => transformPayloadLicenseToLicense(license)) } + +export const transformPayloadLicenseToLicense = (license: LicensePayload): License => ({ + id: license.id, + name: license.name, + shortDescription: license.shortDescription, + uri: license.uri, + iconUri: license.iconUrl, // in payload, it is called iconUrl, but iconUri is the name matching everywhere else + active: license.active, + isDefault: license.isDefault, + sortOrder: license.sortOrder, + rightsIdentifier: license.rightsIdentifier, + rightsIdentifierScheme: license.rightsIdentifierScheme, + schemeUri: license.schemeUri, + languageCode: license.languageCode +}) diff --git a/src/licenses/infra/repositories/LicensesRepository.ts b/src/licenses/infra/repositories/LicensesRepository.ts index 2461d8f5..087d833d 100644 --- a/src/licenses/infra/repositories/LicensesRepository.ts +++ b/src/licenses/infra/repositories/LicensesRepository.ts @@ -1,14 +1,14 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { ILicensesRepository } from '../../domain/repositories/ILicensesRepository' import { License } from '../../domain/models/License' -import { transformPayloadToLicense } from '../../domain/repositories/transformers/licenseTransformers' +import { transformPayloadToLicenses } from '../../domain/repositories/transformers/licenseTransformers' export class LicensesRepository extends ApiRepository implements ILicensesRepository { private readonly licensesResourceName: string = 'licenses' public async getAvailableStandardLicenses(): Promise { return this.doGet(this.buildApiEndpoint(this.licensesResourceName)) - .then((response) => transformPayloadToLicense(response)) + .then((response) => transformPayloadToLicenses(response)) .catch((error) => { throw error }) From 40ea333a3126b81fff2a0239ec123aa9b2ebd247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 10 Sep 2025 08:36:06 -0300 Subject: [PATCH 097/123] test: unskip tests now that related backend PR was merged --- .../externalTools/ExternalToolsRepository.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/integration/externalTools/ExternalToolsRepository.test.ts b/test/integration/externalTools/ExternalToolsRepository.test.ts index de544e34..ef2c9248 100644 --- a/test/integration/externalTools/ExternalToolsRepository.test.ts +++ b/test/integration/externalTools/ExternalToolsRepository.test.ts @@ -48,8 +48,7 @@ describe('ExternalToolsRepository', () => { }) }) - // Skipping until related backed PR is merged - describe.skip('getFileExternalToolResolved', () => { + describe('getFileExternalToolResolved', () => { const testCollectionAlias = 'getFileExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' @@ -141,7 +140,7 @@ describe('ExternalToolsRepository', () => { }) }) - describe.skip('getDatasetExternalToolResolved', () => { + describe('getDatasetExternalToolResolved', () => { const testCollectionAlias = 'getDatasetExternalToolResolvedFunctionalTestCollection' let testDatasetIds: CreatedDatasetIdentifiers const testTextFile1Name = 'test-file-1.txt' From e4f09f955d1a7a06545251d4ec128a13b45be28e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 10 Sep 2025 11:59:21 -0400 Subject: [PATCH 098/123] fix docs (copy/paste error) #363 --- docs/useCases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/useCases.md b/docs/useCases.md index fd97b80a..e15785cf 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -1125,7 +1125,7 @@ import { getDatasetAvailableDatasetTypes } from '@iqss/dataverse-client-javascri /* ... */ -getDatasetAvailableDatasetTypes.execute().then((categories: String[]) => { +getDatasetAvailableDatasetTypes.execute().then((datasetTypes: DatasetType[]) => { /* ... */ }) ``` From a49c004814003cf3e0dcd7b952712496e75c6359 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 10 Sep 2025 15:28:10 -0400 Subject: [PATCH 099/123] add integration test #363 --- .../datasets/DatasetsRepository.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 7962b465..84a85656 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -20,7 +20,9 @@ import { CreatedDatasetIdentifiers, DatasetDTO, DatasetDeaccessionDTO, - publishDataset + publishDataset, + DatasetType, + getDatasetAvailableDatasetTypes } from '../../../src/datasets' import { ApiConfig, WriteError } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -1649,4 +1651,20 @@ describe('DatasetsRepository', () => { await expect(sut.getDatasetAvailableCategories(nonExistentTestDatasetId)).rejects.toThrow() }) }) + + describe('getDatasetAvailableDatasetTypes', () => { + test('should return available dataset types', async () => { + const actualDatasetTypes: DatasetType[] = await getDatasetAvailableDatasetTypes.execute() + const expectedDatasetTypes = [ + { + id: 1, + name: 'dataset', + linkedMetadataBlocks: [], + availableLicenses: [] + } + ] + + expect(actualDatasetTypes).toEqual(expectedDatasetTypes) + }) + }) }) From 76fa6362215d8393686575a2d45260e9100b5261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 10 Sep 2025 16:48:09 -0300 Subject: [PATCH 100/123] feat: use case --- .../repositories/ICollectionsRepository.ts | 7 ++++ .../useCases/GetCollectionsForLinking.ts | 27 +++++++++++++ src/collections/index.ts | 6 ++- .../repositories/CollectionsRepository.ts | 40 ++++++++++++++++++- test/environment/.env | 4 +- 5 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 src/collections/domain/useCases/GetCollectionsForLinking.ts diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index 820a1356..b512e880 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -10,6 +10,8 @@ import { CollectionUserPermissions } from '../models/CollectionUserPermissions' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType' import { CollectionLinks } from '../models/CollectionLinks' +import { CollectionSummary } from '../models/CollectionSummary' +import { LinkingObjectType } from '../useCases/GetCollectionsForLinking' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -60,4 +62,9 @@ export interface ICollectionsRepository { linkingCollectionIdOrAlias: number | string ): Promise getCollectionLinks(collectionIdOrAlias: number | string): Promise + getCollectionsForLinking( + objectType: LinkingObjectType, + id: number | string, + searchTerm: string + ): Promise } diff --git a/src/collections/domain/useCases/GetCollectionsForLinking.ts b/src/collections/domain/useCases/GetCollectionsForLinking.ts new file mode 100644 index 00000000..80738295 --- /dev/null +++ b/src/collections/domain/useCases/GetCollectionsForLinking.ts @@ -0,0 +1,27 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { CollectionSummary } from '../models/CollectionSummary' + +export type LinkingObjectType = 'collection' | 'dataset' + +export class GetCollectionsForLinking implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Returns an array of CollectionSummary (id, alias, displayName) to which the given Dataverse collection or Dataset may be linked. + * @param objectType - 'dataverse' when providing a collection identifier/alias; 'dataset' when providing a dataset persistentId. + * @param id - For objectType 'dataverse', a numeric id or alias string. For 'dataset', the persistentId string (e.g., doi:...) + * @param searchTerm - Optional search term to filter by collection name. Defaults to empty string (no filtering). + */ + async execute( + objectType: LinkingObjectType, + id: number | string, + searchTerm = '' + ): Promise { + return await this.collectionsRepository.getCollectionsForLinking(objectType, id, searchTerm) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 05e49954..59e2e50b 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -15,6 +15,7 @@ import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollection import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' +import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking' const collectionsRepository = new CollectionsRepository() @@ -34,6 +35,7 @@ const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collection const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) +const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository) export { getCollection, @@ -51,7 +53,8 @@ export { deleteCollectionFeaturedItem, linkCollection, unlinkCollection, - getCollectionLinks + getCollectionLinks, + getCollectionsForLinking } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' @@ -62,3 +65,4 @@ export { CollectionItemType } from './domain/models/CollectionItemType' export { CollectionSearchCriteria } from './domain/models/CollectionSearchCriteria' export { FeaturedItem } from './domain/models/FeaturedItem' export { FeaturedItemsDTO } from './domain/dtos/FeaturedItemsDTO' +export { CollectionSummary } from './domain/models/CollectionSummary' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 704367e2..4a2f8e47 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -38,6 +38,8 @@ import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { ReadError } from '../../../core/domain/repositories/ReadError' import { CollectionLinks } from '../../domain/models/CollectionLinks' +import { CollectionSummary } from '../../domain/models/CollectionSummary' +import { LinkingObjectType } from '../../domain/useCases/GetCollectionsForLinking' export interface NewCollectionRequestPayload { alias: string @@ -93,7 +95,6 @@ export enum GetMyDataCollectionItemsQueryParams { export class CollectionsRepository extends ApiRepository implements ICollectionsRepository { private readonly collectionsResourceName: string = 'dataverses' - public async getCollection( collectionIdOrAlias: number | string = ROOT_COLLECTION_ID ): Promise { @@ -485,4 +486,41 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + + public async getCollectionsForLinking( + objectType: LinkingObjectType, + id: number | string, + searchTerm: string + ): Promise { + let path: string + const queryParams = new URLSearchParams() + if (objectType === 'collection') { + path = `/${this.collectionsResourceName}/${id}/dataverse/linkingDataverses` + } else { + path = `/${this.collectionsResourceName}/:persistentId/dataset/linkingDataverses` + queryParams.set('persistentId', String(id)) + } + + if (searchTerm) { + queryParams.set('searchTerm', searchTerm) + } + + return this.doGet(path, true, queryParams) + .then((response) => { + const payload = response.data.data as { + id: number + alias: string + name: string + }[] + + return payload.map((item) => ({ + id: item.id, + alias: item.alias, + displayName: item.name + })) + }) + .catch((error) => { + throw error + }) + } } diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..3a9a818d 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=11710-find-dataverses-for-linking DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 5695023a8f948900b304154ca9d21418d2daa129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 10 Sep 2025 16:48:23 -0300 Subject: [PATCH 101/123] test: add unit and integration test --- .../collections/CollectionsRepository.test.ts | 57 +++++++++++++++++++ .../collections/CollectionsRepository.test.ts | 49 ++++++++++++++++ .../GetCollectionsForLinking.test.ts | 28 +++++++++ 3 files changed, 134 insertions(+) create mode 100644 test/unit/collections/GetCollectionsForLinking.test.ts diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index d1afd76d..7205678a 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -804,6 +804,63 @@ describe('CollectionsRepository', () => { }) }) + describe('getCollectionsForLinking', () => { + const linkingTargetAlias = 'collectionsRepositoryLinkTarget' + + beforeAll(async () => { + await createCollectionViaApi(linkingTargetAlias) + }) + + afterAll(async () => { + await deleteCollectionViaApi(linkingTargetAlias) + }) + + test('should list collections for linking for a given collection alias', async () => { + const results = await sut.getCollectionsForLinking( + 'collection', + testCollectionAlias, + 'Scientific' + ) + + expect(Array.isArray(results)).toBe(true) + // Should contain the newly created linking target collection among candidates + const found = results.find((c) => c.alias === linkingTargetAlias) + expect(found).toBeDefined() + expect(found?.id).toBeGreaterThan(0) + expect(found?.displayName).toBe('Scientific Research') + }) + + test('should list collections for linking for a given dataset persistentId', async () => { + // Create a temporary dataset to query linking candidates + const { persistentId, numericId } = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testCollectionAlias + ) + + const results = await sut.getCollectionsForLinking('dataset', persistentId, 'Scientific') + + // Cleanup dataset (unpublished) + await deleteUnpublishedDatasetViaApi(numericId) + + expect(Array.isArray(results)).toBe(true) + const found = results.find((c) => c.alias === linkingTargetAlias) + expect(found).toBeDefined() + expect(found?.displayName).toBe('Scientific Research') + }) + + it('should return error when collection does not exist', async () => { + await expect( + sut.getCollectionsForLinking('collection', TestConstants.TEST_DUMMY_COLLECTION_ALIAS, '') + ).rejects.toThrow(ReadError) + }) + + it('should return error when dataset does not exist', async () => { + await expect( + sut.getCollectionsForLinking('dataset', TestConstants.TEST_DUMMY_PERSISTENT_ID, '') + ).rejects.toThrow(ReadError) + }) + }) + describe('getCollectionItems for published tabular file', () => { let testDatasetIds: CreatedDatasetIdentifiers const testTextFile4Name = 'test-file-4.tab' diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index 950a9cdf..a5501119 100644 --- a/test/unit/collections/CollectionsRepository.test.ts +++ b/test/unit/collections/CollectionsRepository.test.ts @@ -567,6 +567,55 @@ describe('CollectionsRepository', () => { }) }) + describe('getCollectionsForLinking', () => { + test('should call dataverse variant with numeric id and search term', async () => { + const payload = { + data: { + status: 'OK', + data: [ + { id: 1, alias: 'dv1', name: 'DV 1' }, + { id: 2, alias: 'dv2', name: 'DV 2' } + ] + } + } + jest.spyOn(axios, 'get').mockResolvedValue(payload) + + const actual = await sut.getCollectionsForLinking('collection', 99, 'abc') + const expectedEndpoint = `${TestConstants.TEST_API_URL}/dataverses/99/dataverse/linkingDataverses` + const expectedParams = new URLSearchParams({ searchTerm: 'abc' }) + const expectedConfig = { + params: expectedParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + expect(axios.get).toHaveBeenCalledWith(expectedEndpoint, expectedConfig) + expect(actual).toEqual([ + { id: 1, alias: 'dv1', displayName: 'DV 1' }, + { id: 2, alias: 'dv2', displayName: 'DV 2' } + ]) + }) + + test('should call dataset variant using persistentId and map results', async () => { + const payload = { + data: { + status: 'OK', + data: [{ id: 3, alias: 'dv3', name: 'DV 3' }] + } + } + jest.spyOn(axios, 'get').mockResolvedValue(payload) + + const pid = 'doi:10.5072/FK2/J8SJZB' + const actual = await sut.getCollectionsForLinking('dataset', pid, '') + const expectedEndpoint = `${TestConstants.TEST_API_URL}/dataverses/:persistentId/dataset/linkingDataverses` + const expectedParams = new URLSearchParams({ persistentId: pid }) + const expectedConfig = { + params: expectedParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + expect(axios.get).toHaveBeenCalledWith(expectedEndpoint, expectedConfig) + expect(actual).toEqual([{ id: 3, alias: 'dv3', displayName: 'DV 3' }]) + }) + }) + describe('deleteCollection', () => { const deleteTestCollectionAlias = 'deleteCollection-unit-test' const deleteTestCollectionId = 123 diff --git a/test/unit/collections/GetCollectionsForLinking.test.ts b/test/unit/collections/GetCollectionsForLinking.test.ts new file mode 100644 index 00000000..390fe147 --- /dev/null +++ b/test/unit/collections/GetCollectionsForLinking.test.ts @@ -0,0 +1,28 @@ +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { GetCollectionsForLinking } from '../../../src/collections/domain/useCases/GetCollectionsForLinking' +import { CollectionSummary, ReadError } from '../../../src' + +const sample: CollectionSummary[] = [ + { id: 1, alias: 'col1', displayName: 'Collection 1' }, + { id: 2, alias: 'col2', displayName: 'Collection 2' } +] + +describe('GetCollectionsForLinking', () => { + test('should return collections for linking on success', async () => { + const repo: ICollectionsRepository = {} as ICollectionsRepository + repo.getCollectionsForLinking = jest.fn().mockResolvedValue(sample) + + const uc = new GetCollectionsForLinking(repo) + await expect(uc.execute('collection', 123, 'foo')).resolves.toEqual(sample) + expect(repo.getCollectionsForLinking).toHaveBeenCalledWith('collection', 123, 'foo') + }) + + test('should return error result on repository error', async () => { + const repo: ICollectionsRepository = {} as ICollectionsRepository + repo.getCollectionsForLinking = jest.fn().mockRejectedValue(new ReadError('x')) + + const uc = new GetCollectionsForLinking(repo) + await expect(uc.execute('dataset', 'doi:10.123/ABC')).rejects.toThrow(ReadError) + expect(repo.getCollectionsForLinking).toHaveBeenCalledWith('dataset', 'doi:10.123/ABC', '') + }) +}) From 8387b4e9e815d039956671c6fa1d4236df3a993c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 10 Sep 2025 16:48:39 -0300 Subject: [PATCH 102/123] docs: add use case docs --- docs/useCases.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/useCases.md b/docs/useCases.md index 7fc7b955..0b022447 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -16,6 +16,7 @@ The different use cases currently available in the package are classified below, - [List All Collection Items](#list-all-collection-items) - [List My Data Collection Items](#list-my-data-collection-items) - [Get Collection Featured Items](#get-collection-featured-items) + - [Get Collections for Linking](#get-collections-for-linking) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) @@ -324,6 +325,56 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe If no collection identifier is specified, the default collection identifier; `:root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. +#### Get Collections for Linking + +Returns an array of [CollectionSummary](../src/collections/domain/models/CollectionSummary.ts) (id, alias, displayName) representing the Dataverse collections to which a given Dataverse collection or Dataset may be linked. + +This use case supports an optional `searchTerm` to filter by collection name. + +##### Example calls: + +```typescript +import { getCollectionsForLinking } from '@iqss/dataverse-client-javascript' + +/* ... */ + +// Case 1: For a given Dataverse collection (by numeric id or alias) +const collectionIdOrAlias: number | string = 'collectionAlias' // or 123 +const searchTerm = 'searchOn' + +getCollectionsForLinking + .execute('dataverse', collectionIdOrAlias, searchTerm) + .then((collections) => { + // collections: CollectionSummary[] + /* ... */ + }) + .catch((error: Error) => { + /* ... */ + }) + +/* ... */ + +// Case 2: For a given Dataset (by persistent identifier) +const persistentId = 'doi:10.5072/FK2/J8SJZB' + +getCollectionsForLinking + .execute('dataset', persistentId, searchTerm) + .then((collections) => { + // collections: CollectionSummary[] + /* ... */ + }) + .catch((error: Error) => { + /* ... */ + }) +``` + +_See [use case](../src/collections/domain/useCases/GetCollectionsForLinking.ts) implementation_. + +Notes: + +- When the first argument is `'collection'`, the second argument can be a numeric collection id or a collection alias. +- When the first argument is `'dataset'`, the second argument must be the dataset persistent identifier string (e.g., `doi:...`). + ### Collections Write Use Cases #### Create a Collection From aaf41cc689441681fcaabd4707d1060bd88baa05 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Wed, 10 Sep 2025 16:15:37 -0400 Subject: [PATCH 103/123] docs: move use case to correct section (read instead of write) #363 --- docs/useCases.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index 6af837b3..db6feccc 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -810,6 +810,24 @@ getDatasetLinkedCollections _See [use case](../src/datasets/domain/useCases/GetDatasetLinkedCollections.ts) implementation_. +#### Get Dataset Available Dataset Types + +Returns a list of available dataset types that can be used at dataset creation. By default, only the type "dataset" is returned. + +###### Example call: + +```typescript +import { getDatasetAvailableDatasetTypes } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getDatasetAvailableDatasetTypes.execute().then((datasetTypes: DatasetType[]) => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts) implementation_. + ### Datasets Write Use Cases #### Create a Dataset @@ -1133,24 +1151,6 @@ getDatasetTemplates.execute(collectionIdOrAlias).then((datasetTemplates: Dataset _See [use case](../src/datasets/domain/useCases/GetDatasetTemplates.ts)_ definition. -#### Get Dataset Available Dataset Types - -Returns a list of available dataset types that can be used at dataset creation. By default, only the type "dataset" is returned. - -###### Example call: - -```typescript -import { getDatasetAvailableDatasetTypes } from '@iqss/dataverse-client-javascript' - -/* ... */ - -getDatasetAvailableDatasetTypes.execute().then((datasetTypes: DatasetType[]) => { - /* ... */ -}) -``` - -_See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts) implementation_. - ## Files ### Files read use cases From e4c088080eec5e463bec4cf6da52d2c17c237f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 16 Sep 2025 09:28:40 -0300 Subject: [PATCH 104/123] feat: update linkDataset and unlinkDataset methods to accept string or number for dataset and collection identifiers --- .../repositories/IDatasetsRepository.ts | 4 +-- src/datasets/domain/useCases/LinkDataset.ts | 8 ++--- src/datasets/domain/useCases/UnlinkDataset.ts | 8 ++--- .../infra/repositories/DatasetsRepository.ts | 24 ++++++++++--- .../datasets/DatasetsRepository.test.ts | 36 +++++++++++++++++++ 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 499dfed0..c0ad2bf7 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -65,8 +65,8 @@ export interface IDatasetsRepository { ): Promise getDatasetVersionsSummaries(datasetId: number | string): Promise deleteDatasetDraft(datasetId: number | string): Promise - linkDataset(datasetId: number, collectionAlias: string): Promise - unlinkDataset(datasetId: number, collectionAlias: string): Promise + linkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise + unlinkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise getDatasetLinkedCollections(datasetId: number | string): Promise getDatasetAvailableCategories(datasetId: number | string): Promise getDatasetCitationInOtherFormats( diff --git a/src/datasets/domain/useCases/LinkDataset.ts b/src/datasets/domain/useCases/LinkDataset.ts index be7f732f..4e953b17 100644 --- a/src/datasets/domain/useCases/LinkDataset.ts +++ b/src/datasets/domain/useCases/LinkDataset.ts @@ -11,11 +11,11 @@ export class LinkDataset implements UseCase { /** * Creates a link between a Dataset and a Collection. * - * @param {number} [datasetId] - The dataset id. - * @param {string} [collectionAlias] - The collection alias. + * @param {number | string} [datasetId] - The dataset id (numeric) or persistent identifier string. + * @param {number | string} [collectionIdOrAlias] - The collection identifier (numeric id) or alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number, collectionAlias: string): Promise { - return await this.datasetsRepository.linkDataset(datasetId, collectionAlias) + async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { + return await this.datasetsRepository.linkDataset(datasetId, collectionIdOrAlias) } } diff --git a/src/datasets/domain/useCases/UnlinkDataset.ts b/src/datasets/domain/useCases/UnlinkDataset.ts index d2d8eff5..8b2142fb 100644 --- a/src/datasets/domain/useCases/UnlinkDataset.ts +++ b/src/datasets/domain/useCases/UnlinkDataset.ts @@ -11,11 +11,11 @@ export class UnlinkDataset implements UseCase { /** * Removes a link between a Dataset and a Collection. * - * @param {number} [datasetId] - The dataset id. - * @param {string} [collectionAlias] - The collection alias. + * @param {number | string} [datasetId] - The dataset id (numeric) or persistent identifier string. + * @param {number | string} [collectionIdOrAlias] - The collection identifier (numeric id) or alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number, collectionAlias: string): Promise { - return await this.datasetsRepository.unlinkDataset(datasetId, collectionAlias) + async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { + return await this.datasetsRepository.unlinkDataset(datasetId, collectionIdOrAlias) } } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 9a38eaae..c1c166f5 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -323,16 +323,32 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - public async linkDataset(datasetId: number, collectionAlias: string): Promise { - return this.doPut(`/${this.datasetsResourceName}/${datasetId}/link/${collectionAlias}`, {}) + public async linkDataset( + datasetId: number | string, + collectionIdOrAlias: number | string + ): Promise { + const endpoint = this.buildApiEndpoint( + this.datasetsResourceName, + `link/${collectionIdOrAlias}`, + datasetId + ) + return this.doPut(endpoint, {}) .then(() => undefined) .catch((error) => { throw error }) } - public async unlinkDataset(datasetId: number, collectionAlias: string): Promise { - return this.doDelete(`/${this.datasetsResourceName}/${datasetId}/deleteLink/${collectionAlias}`) + public async unlinkDataset( + datasetId: number | string, + collectionIdOrAlias: number | string + ): Promise { + const endpoint = this.buildApiEndpoint( + this.datasetsResourceName, + `deleteLink/${collectionIdOrAlias}`, + datasetId + ) + return this.doDelete(endpoint) .then(() => undefined) .catch((error) => { throw error diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index ba9c9d79..5acb38bc 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1533,6 +1533,21 @@ describe('DatasetsRepository', () => { sut.linkDataset(testDatasetIds.numericId, 'nonExistentCollectionAlias') ).rejects.toThrow() }) + + test('should link a dataset to another collection using persistent id', async () => { + const persistentCollectionAlias = 'testLinkDatasetCollectionPersistent' + await createCollectionViaApi(persistentCollectionAlias) + + const actual = await sut.linkDataset(testDatasetIds.persistentId, persistentCollectionAlias) + + expect(actual).toBeUndefined() + + const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) + const aliases = linkedCollections.map((c) => c.alias) + expect(aliases).toContain(persistentCollectionAlias) + + await deleteCollectionViaApi(persistentCollectionAlias) + }) }) describe('unlinkDataset', () => { @@ -1578,6 +1593,27 @@ describe('DatasetsRepository', () => { sut.unlinkDataset(testDatasetIds.numericId, testCollectionAlias) ).rejects.toThrow() }) + + test('should unlink a dataset from a collection using persistent id', async () => { + const persistentCollectionAlias = 'testUnlinkDatasetCollectionPersistent' + await createCollectionViaApi(persistentCollectionAlias) + + await sut.linkDataset(testDatasetIds.persistentId, persistentCollectionAlias) + const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) + const aliases = linkedCollections.map((c) => c.alias) + expect(aliases).toContain(persistentCollectionAlias) + + const actual = await sut.unlinkDataset(testDatasetIds.persistentId, persistentCollectionAlias) + + expect(actual).toBeUndefined() + const updatedLinkedCollections = await sut.getDatasetLinkedCollections( + testDatasetIds.numericId + ) + const updatedAliases = updatedLinkedCollections.map((c) => c.alias) + expect(updatedAliases).not.toContain(persistentCollectionAlias) + + await deleteCollectionViaApi(persistentCollectionAlias) + }) }) describe('getDatasetLinkedCollections', () => { From 569b1da1dc8296f4d3a1d9b843c0798fa1184d14 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 17 Sep 2025 13:55:45 -0400 Subject: [PATCH 105/123] feat: add changelog and update relevant files --- CHANGELOG.md | 21 ++++++++++++++++++ CONTRIBUTING.md | 14 ++++++++++++ README.md | 4 ++++ docs/making-releases.md | 49 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..32bb51b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to **Dataverse Client Javascript** are documented here. + +This changelog follows the principles of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and adheres to [Semantic Versioning](https://semver.org/). This document is intended for developers, contributors, and users who need to understand the technical details. + +## [Unreleased] + +### Added + +### Changed + +### Fixed + +### Removed + +--- + +## [v2.0.0] -- 2025-07-04 + +[Unreleased]: https://github.com/IQSS/dataverse-frontend/compare/v2.0.0...develop diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44a7f046..5740ae70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,20 @@ First of all thank you very much for your interest in contributing to this proje - Unit and integration tests pass - Unit and integration tests for new functionality/fix are added - Documentation is updated (Any new use case added or modified should be documented in the [Use Cases](./docs/useCases.md) section) +- Changelog is updated with your changes in the `[Unreleased]` section of [CHANGELOG.md](./CHANGELOG.md) + +## Maintaining the Changelog + +We follow the [Keep a Changelog](https://keepachangelog.com/) format for our changelog. When contributing: + +1. **Add your changes to the `[Unreleased]` section** at the top of `CHANGELOG.md` +2. **Categorize your changes** under the appropriate headings: + - **Added** for new features + - **Changed** for changes in existing functionality + - **Removed** for now removed features + - **Fixed** for any bug fixes +3. **Write clear, concise descriptions** that help users understand the impact of changes +4. **Include relevant issue numbers** when applicable ## Code of Conduct diff --git a/README.md b/README.md index 4421abdf..44b2266d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ For detailed information about available use cases see [Use Cases Docs](https:// For detailed information about usage see [Usage Docs](https://github.com/IQSS/dataverse-client-javascript/blob/main/docs/usage.md). +## Changelog + +See [CHANGELOG.md](https://github.com/IQSS/dataverse-client-javascript/blob/main/CHANGELOG.md) for a detailed history of changes to this project. + ## Contributing Want to add a new use case or improve an existing one? Please check the [Contributing](https://github.com/IQSS/dataverse-client-javascript/blob/main/CONTRIBUTING.md) section. diff --git a/docs/making-releases.md b/docs/making-releases.md index 13ed7d96..ce405014 100644 --- a/docs/making-releases.md +++ b/docs/making-releases.md @@ -4,6 +4,7 @@ - [Regular or Hotfix?](#regular-or-hotfix) - [Create Github Issue and Release Branch](#create-github-issue-and-release-branch) - [Update the version](#update-the-version) +- [Update the changelog](#update-the-changelog) - [Merge "release branch" into "main"](#merge-release-branch-into-main) - [Publish the Dataverse Client Javascript package](#publish-the-dataverse-client-javascript-package) - [Create a Draft Release on GitHub and Tag the Version](#create-a-draft-release-on-github-and-tag-the-version) @@ -42,6 +43,52 @@ This command will update the version in the `package.json` and `package-lock.jso If everything looks good, you can push the changes to the repository. +## Update the changelog + +**Note**: Contributors should have already added their changes to the `[Unreleased]` section as part of their pull requests (see [CONTRIBUTING.md](../.github/CONTRIBUTING.md#changelog-guidelines) for details). + +Before releasing, ensure the changelog is properly prepared: + +1. **Review the [Unreleased] section** in `CHANGELOG.md` and `packages/design-system/CHANGELOG.md` +2. **Move entries from [Unreleased] to the new version section**: + + ```markdown + ## [vX.X.X] -- YYYY-MM-DD + + ### Added + + - Feature descriptions from unreleased section + + ### Changed + + - Changes from unreleased section + + ### Fixed + + - Bug fixes from unreleased section + + ### Removed + + - Removals from unreleased section + ``` + +3. **Clear the [Unreleased] section** but keep the structure: + + ```markdown + ## [Unreleased] + + ### Added + + ### Changed + + ### Fixed + + ### Removed + ``` + +4. **Update the version links** at the bottom of the changelog files +5. **Commit the changelog updates** as part of the release preparation + ## Merge "release branch" into "main" Create a pull request to merge the `release` branch into the `main` branch. @@ -96,7 +143,7 @@ Go to https://github.com/IQSS/dataverse-client-javascript/releases/new to start - Under "Release title" use the same name as the tag such as v3.5.0. -- Add a description of the changes included in this release. You should include a link to the recently published npm version and summarize the key updates, fixes, or features. +- Add a description of the changes included in this release. You should include a link to the recently published npm version and summarize the key updates, fixes, or features. You can copy the content from the corresponding version section in `CHANGELOG.md` for consistency. - Click "Save draft" because we do not want to publish the release yet. From 20dc116b6978181f61a5648363ae31ffb4c8aa45 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 17 Sep 2025 14:02:58 -0400 Subject: [PATCH 106/123] feat: update PR template --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CONTRIBUTING.md | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4e8597e3..977c9a83 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,6 +12,6 @@ ## Suggestions on how to test this: -## Is there a release notes update needed for this change?: +## Is there a release notes or changelog update needed for this change?: ## Additional documentation: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5740ae70..7e53e952 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,14 +20,23 @@ First of all thank you very much for your interest in contributing to this proje ## Maintaining the Changelog -We follow the [Keep a Changelog](https://keepachangelog.com/) format for our changelog. When contributing: +When contributing to this project, it's important to document your changes in the changelog to help users and developers understand what has been added, changed, fixed, or removed between versions. The changelog helps maintain transparency about project evolution and assists users in understanding the impact of updates. We also have another changelog for design system, so for any design system changes, please include them in that changelog. + +### When to Add Changelog Entries + +**Every pull request should include a changelog entry** + +Add a changelog entry for changes, including: + +- **Added**: New features, components, or functionality +- **Changed**: Changes to existing functionality, API modifications, or package updates +- **Fixed**: Bug fixes and issue resolutions +- **Removed**: Deprecated features or removed functionality + +### How to Add Changelog Entries 1. **Add your changes to the `[Unreleased]` section** at the top of `CHANGELOG.md` -2. **Categorize your changes** under the appropriate headings: - - **Added** for new features - - **Changed** for changes in existing functionality - - **Removed** for now removed features - - **Fixed** for any bug fixes +2. **Categorize your changes** under the appropriate category(Added, Changed, Fixed, Removed) 3. **Write clear, concise descriptions** that help users understand the impact of changes 4. **Include relevant issue numbers** when applicable From e30ad1843614f71361e50593f4a9a013d036e8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 19 Sep 2025 08:43:00 -0300 Subject: [PATCH 107/123] feat: add alreadyLinked parameter to getCollectionsForLinking to get candidates for unlinking --- docs/useCases.md | 13 ++++++ .../repositories/ICollectionsRepository.ts | 3 +- .../useCases/GetCollectionsForLinking.ts | 14 +++++- .../repositories/CollectionsRepository.ts | 7 ++- .../collections/CollectionsRepository.test.ts | 43 +++++++++++++++++-- 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index 6d9fec4a..c87f08e8 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -367,6 +367,19 @@ getCollectionsForLinking .catch((error: Error) => { /* ... */ }) + +// Case 3: [alreadyLinked] Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. +const alreadyLinked = true + +getCollectionsForLinking + .execute('dataset', persistentId, searchTerm, alreadyLinked) + .then((collections) => { + // collections: CollectionSummary[] + /* ... */ + }) + .catch((error: Error) => { + /* ... */ + }) ``` _See [use case](../src/collections/domain/useCases/GetCollectionsForLinking.ts) implementation_. diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index b512e880..bc8960c8 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -65,6 +65,7 @@ export interface ICollectionsRepository { getCollectionsForLinking( objectType: LinkingObjectType, id: number | string, - searchTerm: string + searchTerm: string, + alreadyLinked: boolean ): Promise } diff --git a/src/collections/domain/useCases/GetCollectionsForLinking.ts b/src/collections/domain/useCases/GetCollectionsForLinking.ts index 80738295..42c66fc2 100644 --- a/src/collections/domain/useCases/GetCollectionsForLinking.ts +++ b/src/collections/domain/useCases/GetCollectionsForLinking.ts @@ -4,6 +4,9 @@ import { CollectionSummary } from '../models/CollectionSummary' export type LinkingObjectType = 'collection' | 'dataset' +// TODO:ME - Add to the interface and here the alreadyLinking param to get collections for unlinking +// @param alreadyLinking - Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. + export class GetCollectionsForLinking implements UseCase { private collectionsRepository: ICollectionsRepository @@ -16,12 +19,19 @@ export class GetCollectionsForLinking implements UseCase { * @param objectType - 'dataverse' when providing a collection identifier/alias; 'dataset' when providing a dataset persistentId. * @param id - For objectType 'dataverse', a numeric id or alias string. For 'dataset', the persistentId string (e.g., doi:...) * @param searchTerm - Optional search term to filter by collection name. Defaults to empty string (no filtering). + * @param alreadyLinked - Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. */ async execute( objectType: LinkingObjectType, id: number | string, - searchTerm = '' + searchTerm = '', + alreadyLinked = false ): Promise { - return await this.collectionsRepository.getCollectionsForLinking(objectType, id, searchTerm) + return await this.collectionsRepository.getCollectionsForLinking( + objectType, + id, + searchTerm, + alreadyLinked + ) } } diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 4a2f8e47..e0e459b0 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -490,7 +490,8 @@ export class CollectionsRepository extends ApiRepository implements ICollections public async getCollectionsForLinking( objectType: LinkingObjectType, id: number | string, - searchTerm: string + searchTerm: string, + alreadyLinked: boolean ): Promise { let path: string const queryParams = new URLSearchParams() @@ -505,6 +506,10 @@ export class CollectionsRepository extends ApiRepository implements ICollections queryParams.set('searchTerm', searchTerm) } + if (alreadyLinked) { + queryParams.set('alreadyLinking', 'true') + } + return this.doGet(path, true, queryParams) .then((response) => { const payload = response.data.data as { diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 7205678a..8a04097e 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -819,7 +819,8 @@ describe('CollectionsRepository', () => { const results = await sut.getCollectionsForLinking( 'collection', testCollectionAlias, - 'Scientific' + 'Scientific', + false ) expect(Array.isArray(results)).toBe(true) @@ -837,7 +838,12 @@ describe('CollectionsRepository', () => { testCollectionAlias ) - const results = await sut.getCollectionsForLinking('dataset', persistentId, 'Scientific') + const results = await sut.getCollectionsForLinking( + 'dataset', + persistentId, + 'Scientific', + false + ) // Cleanup dataset (unpublished) await deleteUnpublishedDatasetViaApi(numericId) @@ -848,15 +854,44 @@ describe('CollectionsRepository', () => { expect(found?.displayName).toBe('Scientific Research') }) + test('should return collections for unlking when sending alreadyLinked param to true', async () => { + // Link the test collection with the linking target collection + await sut.linkCollection(testCollectionAlias, linkingTargetAlias) + + const collectionsForLinking = await sut.getCollectionsForLinking( + 'collection', + testCollectionAlias, + '', + false + ) + + const collectionsForUnlinking = await sut.getCollectionsForLinking( + 'collection', + testCollectionAlias, + '', + true + ) + + expect(collectionsForLinking.length).toBe(0) + expect(collectionsForUnlinking.length).toBeGreaterThan(0) + expect(collectionsForUnlinking[0].alias).toBe(linkingTargetAlias) + expect(collectionsForUnlinking[0].displayName).toBe('Scientific Research') + }) + it('should return error when collection does not exist', async () => { await expect( - sut.getCollectionsForLinking('collection', TestConstants.TEST_DUMMY_COLLECTION_ALIAS, '') + sut.getCollectionsForLinking( + 'collection', + TestConstants.TEST_DUMMY_COLLECTION_ALIAS, + '', + false + ) ).rejects.toThrow(ReadError) }) it('should return error when dataset does not exist', async () => { await expect( - sut.getCollectionsForLinking('dataset', TestConstants.TEST_DUMMY_PERSISTENT_ID, '') + sut.getCollectionsForLinking('dataset', TestConstants.TEST_DUMMY_PERSISTENT_ID, '', false) ).rejects.toThrow(ReadError) }) }) From 4e1c1a01346f0e98378255ec12d8ebb117665cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 19 Sep 2025 08:47:46 -0300 Subject: [PATCH 108/123] fix: unit tests --- test/unit/collections/CollectionsRepository.test.ts | 4 ++-- test/unit/collections/GetCollectionsForLinking.test.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index a5501119..d099acb1 100644 --- a/test/unit/collections/CollectionsRepository.test.ts +++ b/test/unit/collections/CollectionsRepository.test.ts @@ -580,7 +580,7 @@ describe('CollectionsRepository', () => { } jest.spyOn(axios, 'get').mockResolvedValue(payload) - const actual = await sut.getCollectionsForLinking('collection', 99, 'abc') + const actual = await sut.getCollectionsForLinking('collection', 99, 'abc', false) const expectedEndpoint = `${TestConstants.TEST_API_URL}/dataverses/99/dataverse/linkingDataverses` const expectedParams = new URLSearchParams({ searchTerm: 'abc' }) const expectedConfig = { @@ -604,7 +604,7 @@ describe('CollectionsRepository', () => { jest.spyOn(axios, 'get').mockResolvedValue(payload) const pid = 'doi:10.5072/FK2/J8SJZB' - const actual = await sut.getCollectionsForLinking('dataset', pid, '') + const actual = await sut.getCollectionsForLinking('dataset', pid, '', false) const expectedEndpoint = `${TestConstants.TEST_API_URL}/dataverses/:persistentId/dataset/linkingDataverses` const expectedParams = new URLSearchParams({ persistentId: pid }) const expectedConfig = { diff --git a/test/unit/collections/GetCollectionsForLinking.test.ts b/test/unit/collections/GetCollectionsForLinking.test.ts index 390fe147..79511fe8 100644 --- a/test/unit/collections/GetCollectionsForLinking.test.ts +++ b/test/unit/collections/GetCollectionsForLinking.test.ts @@ -14,7 +14,7 @@ describe('GetCollectionsForLinking', () => { const uc = new GetCollectionsForLinking(repo) await expect(uc.execute('collection', 123, 'foo')).resolves.toEqual(sample) - expect(repo.getCollectionsForLinking).toHaveBeenCalledWith('collection', 123, 'foo') + expect(repo.getCollectionsForLinking).toHaveBeenCalledWith('collection', 123, 'foo', false) }) test('should return error result on repository error', async () => { @@ -23,6 +23,11 @@ describe('GetCollectionsForLinking', () => { const uc = new GetCollectionsForLinking(repo) await expect(uc.execute('dataset', 'doi:10.123/ABC')).rejects.toThrow(ReadError) - expect(repo.getCollectionsForLinking).toHaveBeenCalledWith('dataset', 'doi:10.123/ABC', '') + expect(repo.getCollectionsForLinking).toHaveBeenCalledWith( + 'dataset', + 'doi:10.123/ABC', + '', + false + ) }) }) From 3c6da0a7df4b5d69c0bd489e6691a24b1a53e521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 19 Sep 2025 08:52:06 -0300 Subject: [PATCH 109/123] fix: add wait after linking for test --- test/integration/collections/CollectionsRepository.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 8a04097e..3b27f7c5 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -858,6 +858,8 @@ describe('CollectionsRepository', () => { // Link the test collection with the linking target collection await sut.linkCollection(testCollectionAlias, linkingTargetAlias) + await new Promise((resolve) => setTimeout(resolve, 2000)) + const collectionsForLinking = await sut.getCollectionsForLinking( 'collection', testCollectionAlias, From 9d20668c92bcfcdfe7f01e69f87ed1b20643289e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 19 Sep 2025 09:26:33 -0300 Subject: [PATCH 110/123] fix: make test assertions better --- .../collections/CollectionsRepository.test.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 3b27f7c5..d525edfd 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -805,20 +805,23 @@ describe('CollectionsRepository', () => { }) describe('getCollectionsForLinking', () => { + const linkingParentCollection = 'collectionsRepositoryLinkingTestParentCollection' const linkingTargetAlias = 'collectionsRepositoryLinkTarget' beforeAll(async () => { - await createCollectionViaApi(linkingTargetAlias) + await createCollectionViaApi(linkingParentCollection) + await createCollectionViaApi(linkingTargetAlias, linkingParentCollection) }) afterAll(async () => { await deleteCollectionViaApi(linkingTargetAlias) + await deleteCollectionViaApi(linkingParentCollection) }) test('should list collections for linking for a given collection alias', async () => { const results = await sut.getCollectionsForLinking( 'collection', - testCollectionAlias, + linkingParentCollection, 'Scientific', false ) @@ -835,7 +838,7 @@ describe('CollectionsRepository', () => { // Create a temporary dataset to query linking candidates const { persistentId, numericId } = await createDataset.execute( TestConstants.TEST_NEW_DATASET_DTO, - testCollectionAlias + linkingParentCollection ) const results = await sut.getCollectionsForLinking( @@ -854,30 +857,28 @@ describe('CollectionsRepository', () => { expect(found?.displayName).toBe('Scientific Research') }) - test('should return collections for unlking when sending alreadyLinked param to true', async () => { - // Link the test collection with the linking target collection - await sut.linkCollection(testCollectionAlias, linkingTargetAlias) - - await new Promise((resolve) => setTimeout(resolve, 2000)) - - const collectionsForLinking = await sut.getCollectionsForLinking( + test('should return collections for unlinking when sending alreadyLinked param to true', async () => { + const collectionsForUnlinkingBefore = await sut.getCollectionsForLinking( 'collection', - testCollectionAlias, + linkingParentCollection, '', - false + true ) - const collectionsForUnlinking = await sut.getCollectionsForLinking( + // Link the test collection with the linking target collection + await sut.linkCollection(linkingParentCollection, linkingTargetAlias) + + const collectionsForUnlinkingAfter = await sut.getCollectionsForLinking( 'collection', - testCollectionAlias, + linkingParentCollection, '', true ) - expect(collectionsForLinking.length).toBe(0) - expect(collectionsForUnlinking.length).toBeGreaterThan(0) - expect(collectionsForUnlinking[0].alias).toBe(linkingTargetAlias) - expect(collectionsForUnlinking[0].displayName).toBe('Scientific Research') + expect(collectionsForUnlinkingBefore.length).toBe(0) + expect(collectionsForUnlinkingAfter.length).toBeGreaterThan(0) + expect(collectionsForUnlinkingAfter[0].alias).toBe(linkingTargetAlias) + expect(collectionsForUnlinkingAfter[0].displayName).toBe('Scientific Research') }) it('should return error when collection does not exist', async () => { From d37c5f8860fdc01ca20842f388a10dafd2ab3f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 19 Sep 2025 09:31:29 -0300 Subject: [PATCH 111/123] remove comment --- src/collections/domain/useCases/GetCollectionsForLinking.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/collections/domain/useCases/GetCollectionsForLinking.ts b/src/collections/domain/useCases/GetCollectionsForLinking.ts index 42c66fc2..c9486c9f 100644 --- a/src/collections/domain/useCases/GetCollectionsForLinking.ts +++ b/src/collections/domain/useCases/GetCollectionsForLinking.ts @@ -4,9 +4,6 @@ import { CollectionSummary } from '../models/CollectionSummary' export type LinkingObjectType = 'collection' | 'dataset' -// TODO:ME - Add to the interface and here the alreadyLinking param to get collections for unlinking -// @param alreadyLinking - Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. - export class GetCollectionsForLinking implements UseCase { private collectionsRepository: ICollectionsRepository From ff42b7f650aea27c3aab3c40dc7069b4e8e2efb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 22 Sep 2025 11:16:01 -0300 Subject: [PATCH 112/123] fix: map linked dataset correctly according to model --- .../repositories/transformers/collectionTransformers.ts | 8 +++++++- .../integration/collections/CollectionsRepository.test.ts | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/collections/infra/repositories/transformers/collectionTransformers.ts b/src/collections/infra/repositories/transformers/collectionTransformers.ts index a26c4718..fa23b8ed 100644 --- a/src/collections/infra/repositories/transformers/collectionTransformers.ts +++ b/src/collections/infra/repositories/transformers/collectionTransformers.ts @@ -159,7 +159,13 @@ export const transformCollectionLinksResponseToCollectionLinks = ( const responseDataPayload = response.data.data const linkedCollections = responseDataPayload.linkedDataverses const collectionsLinkingToThis = responseDataPayload.dataversesLinkingToThis - const linkedDatasets = responseDataPayload.linkedDatasets + const linkedDatasets = responseDataPayload.linkedDatasets.map( + (ld: { identifier: string; title: string }) => ({ + persistentId: ld.identifier, + title: ld.title + }) + ) + return { linkedCollections, collectionsLinkingToThis, diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index d525edfd..116457e1 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -2093,16 +2093,18 @@ describe('CollectionsRepository', () => { const thirdCollectionAlias = 'getCollectionLinksThird' const fourthCollectionAlias = 'getCollectionLinksFourth' let childDatasetNumericId: number + let childDatasetPersistentId: string beforeAll(async () => { await createCollectionViaApi(firstCollectionAlias) await createCollectionViaApi(secondCollectionAlias) await createCollectionViaApi(thirdCollectionAlias) await createCollectionViaApi(fourthCollectionAlias) - const { numericId: createdId } = await createDataset.execute( + const { numericId: createdId, persistentId: createdPid } = await createDataset.execute( TestConstants.TEST_NEW_DATASET_DTO, fourthCollectionAlias ) childDatasetNumericId = createdId + childDatasetPersistentId = createdPid await sut.linkCollection(secondCollectionAlias, firstCollectionAlias) await sut.linkCollection(firstCollectionAlias, thirdCollectionAlias) await sut.linkCollection(firstCollectionAlias, fourthCollectionAlias) @@ -2131,6 +2133,7 @@ describe('CollectionsRepository', () => { expect(collectionLinks.linkedDatasets[0].title).toBe( 'Dataset created using the createDataset use case' ) + expect(collectionLinks.linkedDatasets[0].persistentId).toBe(childDatasetPersistentId) }) test('should return error when collection does not exist', async () => { From 88889823a4504403d52dbfe0d95879fa01c92b5b Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Mon, 22 Sep 2025 16:14:47 -0400 Subject: [PATCH 113/123] add, edit, and delete dataset types #370 --- docs/useCases.md | 95 ++++++++++++++++++ src/datasets/domain/models/DatasetType.ts | 2 +- .../repositories/IDatasetsRepository.ts | 11 +++ .../domain/useCases/AddDatasetType.ts | 18 ++++ .../domain/useCases/DeleteDatasetType.ts | 17 ++++ .../GetDatasetAvailableDatasetType.ts | 18 ++++ .../LinkDatasetTypeWithMetadataBlocks.ts | 20 ++++ .../SetAvailableLicensesForDatasetType.ts | 17 ++++ src/datasets/index.ts | 19 +++- .../infra/repositories/DatasetsRepository.ts | 66 +++++++++++++ .../datasets/AddDatasetType.test.ts | 28 ++++++ .../datasets/DeleteDatasetType.test.ts | 28 ++++++ .../GetDatasetAvailableDatasetType.test.ts | 30 ++++++ .../LinkDatasetTypeWithMetadataBlocks.test.ts | 40 ++++++++ ...SetAvailableLicensesForDatasetType.test.ts | 40 ++++++++ .../datasets/DatasetsRepository.test.ts | 96 ++++++++++++++++++- test/unit/datasets/DeleteDatasetType.test.ts | 23 +++++ .../GetDatasetAvailableDatasetType.test.ts | 54 +++++++++++ .../LinkDatasetTypeWithMetadataBlocks.test.ts | 27 ++++++ ...SetAvailableLicensesForDatasetType.test.ts | 27 ++++++ 20 files changed, 673 insertions(+), 3 deletions(-) create mode 100644 src/datasets/domain/useCases/AddDatasetType.ts create mode 100644 src/datasets/domain/useCases/DeleteDatasetType.ts create mode 100644 src/datasets/domain/useCases/GetDatasetAvailableDatasetType.ts create mode 100644 src/datasets/domain/useCases/LinkDatasetTypeWithMetadataBlocks.ts create mode 100644 src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts create mode 100644 test/functional/datasets/AddDatasetType.test.ts create mode 100644 test/functional/datasets/DeleteDatasetType.test.ts create mode 100644 test/functional/datasets/GetDatasetAvailableDatasetType.test.ts create mode 100644 test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts create mode 100644 test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts create mode 100644 test/unit/datasets/DeleteDatasetType.test.ts create mode 100644 test/unit/datasets/GetDatasetAvailableDatasetType.test.ts create mode 100644 test/unit/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts create mode 100644 test/unit/datasets/SetAvailableLicensesForDatasetType.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 2c5e9b5a..ece5c722 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -40,6 +40,7 @@ The different use cases currently available in the package are classified below, - [Get Dataset Available Categories](#get-dataset-available-categories) - [Get Dataset Templates](#get-dataset-templates) - [Get Dataset Available Dataset Types](#get-dataset-available-dataset-types) + - [Get Dataset Available Dataset Type](#get-dataset-available-dataset-type) - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) @@ -48,6 +49,10 @@ The different use cases currently available in the package are classified below, - [Delete a Draft Dataset](#delete-a-draft-dataset) - [Link a Dataset](#link-a-dataset) - [Unlink a Dataset](#unlink-a-dataset) + - [Add a Dataset Type](#add-a-dataset-type) + - [Link Dataset Type with Metadata Blocks](#link-dataset-type-with-metadata-blocks) + - [Set Available Licenses For Dataset Type](#set-available-licenses-for-dataset-type) + - [Delete a Dataset Type](#delete-a-dataset-type) - [Files](#Files) - [Files read use cases](#files-read-use-cases) - [Get a File](#get-a-file) @@ -833,6 +838,24 @@ getDatasetAvailableDatasetTypes.execute().then((datasetTypes: DatasetType[]) => _See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetTypes.ts) implementation_. +#### Get Dataset Available Dataset Type + +Returns an available dataset types that can be used at dataset creation. + +###### Example call: + +```typescript +import { getDatasetAvailableDatasetType } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getDatasetAvailableDatasetType.execute().then((datasetType: DatasetType) => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetType.ts) implementation_. + ### Datasets Write Use Cases #### Create a Dataset @@ -1156,6 +1179,78 @@ getDatasetTemplates.execute(collectionIdOrAlias).then((datasetTemplates: Dataset _See [use case](../src/datasets/domain/useCases/GetDatasetTemplates.ts)_ definition. +#### Add a Dataset Type + +Adds a dataset types that can be used at dataset creation. + +###### Example call: + +```typescript +import { addDatasetType } from '@iqss/dataverse-client-javascript' + +/* ... */ + +addDatasetType.execute(datasetType).then((datasetType: DatasetType) => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/AddDatasetType.ts) implementation_. + +#### Link Dataset Type with Metadata Blocks + +Link a dataset type with metadata blocks. + +###### Example call: + +```typescript +import { linkDatasetTypeWithMetadataBlocks } from '@iqss/dataverse-client-javascript' + +/* ... */ + +linkDatasetTypeWithMetadataBlocks.execute(datasetTypeId, ["geospatial"]).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/LinkDatasetTypeWithMetadataBlocks.ts) implementation_. + +#### Set Available Licenses For Dataset Type + +Set available licenses for dataset type. + +###### Example call: + +```typescript +import { setAvailableLicensesForDatasetType } from '@iqss/dataverse-client-javascript' + +/* ... */ + +setAvailableLicensesForDatasetType.execute(datasetTypeId, ["CC BY 4.0"]).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts) implementation_. + +#### Delete a Dataset Type + +Delete a dataset type. + +###### Example call: + +```typescript +import { deleteDatasetType } from '@iqss/dataverse-client-javascript' + +/* ... */ + +deleteDatasetType.execute(datasetTypeId).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/datasets/domain/useCases/DeleteDatasetType.ts) implementation_. + ## Files ### Files read use cases diff --git a/src/datasets/domain/models/DatasetType.ts b/src/datasets/domain/models/DatasetType.ts index 5475cdaf..56a5ed43 100644 --- a/src/datasets/domain/models/DatasetType.ts +++ b/src/datasets/domain/models/DatasetType.ts @@ -1,5 +1,5 @@ export interface DatasetType { - id: number + id?: number name: string linkedMetadataBlocks?: string[] availableLicenses?: string[] diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index fb8b26d5..3fe1c7ab 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -78,4 +78,15 @@ export interface IDatasetsRepository { ): Promise getDatasetTemplates(collectionIdOrAlias: number | string): Promise getDatasetAvailableDatasetTypes(): Promise + getDatasetAvailableDatasetType(datasetTypeId: number | string): Promise + addDatasetType(datasetType: DatasetType): Promise + linkDatasetTypeWithMetadataBlocks( + datasetTypeId: number | string, + metadataBlocks: string[] + ): Promise + setAvailableLicensesForDatasetType( + datasetTypeId: number | string, + licenses: string[] + ): Promise + deleteDatasetType(datasetTypeId: number): Promise } diff --git a/src/datasets/domain/useCases/AddDatasetType.ts b/src/datasets/domain/useCases/AddDatasetType.ts new file mode 100644 index 00000000..7e2e11c9 --- /dev/null +++ b/src/datasets/domain/useCases/AddDatasetType.ts @@ -0,0 +1,18 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetType } from '../models/DatasetType' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class AddDatasetType implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Add a dataset type that can be selected when creating a dataset. + */ + async execute(datasetType: DatasetType): Promise { + return await this.datasetsRepository.addDatasetType(datasetType) + } +} diff --git a/src/datasets/domain/useCases/DeleteDatasetType.ts b/src/datasets/domain/useCases/DeleteDatasetType.ts new file mode 100644 index 00000000..b3c841aa --- /dev/null +++ b/src/datasets/domain/useCases/DeleteDatasetType.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class DeleteDatasetType implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Deletes a dataset type. + */ + async execute(datasetTypeId: number): Promise { + return await this.datasetsRepository.deleteDatasetType(datasetTypeId) + } +} diff --git a/src/datasets/domain/useCases/GetDatasetAvailableDatasetType.ts b/src/datasets/domain/useCases/GetDatasetAvailableDatasetType.ts new file mode 100644 index 00000000..b870216c --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetAvailableDatasetType.ts @@ -0,0 +1,18 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetType } from '../models/DatasetType' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class GetDatasetAvailableDatasetType implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns a single available dataset type that can be selected when creating a dataset. + */ + async execute(datasetTypeId: number | string): Promise { + return await this.datasetsRepository.getDatasetAvailableDatasetType(datasetTypeId) + } +} diff --git a/src/datasets/domain/useCases/LinkDatasetTypeWithMetadataBlocks.ts b/src/datasets/domain/useCases/LinkDatasetTypeWithMetadataBlocks.ts new file mode 100644 index 00000000..f9a6b447 --- /dev/null +++ b/src/datasets/domain/useCases/LinkDatasetTypeWithMetadataBlocks.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class LinkDatasetTypeWithMetadataBlocks implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Links a dataset type with one or more metadata blocks. These metadata blocks will be shown when creating a dataset of this type. + */ + async execute(datasetTypeId: number | string, metadataBlocks: string[]): Promise { + return await this.datasetsRepository.linkDatasetTypeWithMetadataBlocks( + datasetTypeId, + metadataBlocks + ) + } +} diff --git a/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts b/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts new file mode 100644 index 00000000..e4df9ab2 --- /dev/null +++ b/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' + +export class SetAvailableLicensesForDatasetType implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Sets the available licenses for a given dataset type. This limits the license options when creating a dataset of this type. + */ + async execute(datasetTypeId: number | string, licenses: string[]): Promise { + return await this.datasetsRepository.setAvailableLicensesForDatasetType(datasetTypeId, licenses) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index e6de1b8c..6b93a7cd 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -25,6 +25,11 @@ import { UnlinkDataset } from './domain/useCases/UnlinkDataset' import { GetDatasetLinkedCollections } from './domain/useCases/GetDatasetLinkedCollections' import { GetDatasetAvailableCategories } from './domain/useCases/GetDatasetAvailableCategories' import { GetDatasetAvailableDatasetTypes } from './domain/useCases/GetDatasetAvailableDatasetTypes' +import { GetDatasetAvailableDatasetType } from './domain/useCases/GetDatasetAvailableDatasetType' +import { AddDatasetType } from './domain/useCases/AddDatasetType' +import { LinkDatasetTypeWithMetadataBlocks } from './domain/useCases/LinkDatasetTypeWithMetadataBlocks' +import { SetAvailableLicensesForDatasetType } from './domain/useCases/SetAvailableLicensesForDatasetType' +import { DeleteDatasetType } from './domain/useCases/DeleteDatasetType' import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' @@ -66,6 +71,13 @@ const unlinkDataset = new UnlinkDataset(datasetsRepository) const getDatasetLinkedCollections = new GetDatasetLinkedCollections(datasetsRepository) const getDatasetAvailableCategories = new GetDatasetAvailableCategories(datasetsRepository) const getDatasetAvailableDatasetTypes = new GetDatasetAvailableDatasetTypes(datasetsRepository) +const getDatasetAvailableDatasetType = new GetDatasetAvailableDatasetType(datasetsRepository) +const addDatasetType = new AddDatasetType(datasetsRepository) +const linkDatasetTypeWithMetadataBlocks = new LinkDatasetTypeWithMetadataBlocks(datasetsRepository) +const setAvailableLicensesForDatasetType = new SetAvailableLicensesForDatasetType( + datasetsRepository +) +const deleteDatasetType = new DeleteDatasetType(datasetsRepository) const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) const getDatasetTemplates = new GetDatasetTemplates(datasetsRepository) @@ -92,7 +104,12 @@ export { getDatasetAvailableCategories, getDatasetCitationInOtherFormats, getDatasetTemplates, - getDatasetAvailableDatasetTypes + getDatasetAvailableDatasetTypes, + getDatasetAvailableDatasetType, + addDatasetType, + linkDatasetTypeWithMetadataBlocks, + setAvailableLicensesForDatasetType, + deleteDatasetType } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 594744b4..99b380df 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -382,4 +382,70 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async getDatasetAvailableDatasetType( + datasetTypeId: number | string + ): Promise { + const endpoint = this.buildApiEndpoint( + this.datasetsResourceName, + 'datasetTypes/' + datasetTypeId + ) + return this.doGet(endpoint) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } + + public async addDatasetType(datasetType: DatasetType): Promise { + return this.doPost( + this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes'), + datasetType + ) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } + + public async linkDatasetTypeWithMetadataBlocks( + datasetTypeId: number | string, + metadataBlocks: string[] + ): Promise { + return this.doPut( + this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes/' + datasetTypeId), + metadataBlocks + ) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } + + public async setAvailableLicensesForDatasetType( + datasetTypeId: number | string, + licenses: string[] + ): Promise { + return this.doPut( + this.buildApiEndpoint( + this.datasetsResourceName, + 'datasetTypes/' + datasetTypeId + '/licenses' + ), + licenses + ) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } + + public async deleteDatasetType(datasetTypeId: number): Promise { + return this.doDelete( + this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes/' + datasetTypeId) + ) + .then((response) => response.data.data) + .catch((error) => { + throw error + }) + } } diff --git a/test/functional/datasets/AddDatasetType.test.ts b/test/functional/datasets/AddDatasetType.test.ts new file mode 100644 index 00000000..409c18b9 --- /dev/null +++ b/test/functional/datasets/AddDatasetType.test.ts @@ -0,0 +1,28 @@ +import { ApiConfig, DatasetType, addDatasetType, deleteDatasetType } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('AddDatasetType', () => { + describe('execute', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should allow for adding and deleting a dataset type', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + expect(actual.name).toEqual(randomName) + + const deleted: void = await deleteDatasetType.execute(actual.id as number) + expect(deleted).toEqual({ message: 'deleted' }) + }) + }) +}) diff --git a/test/functional/datasets/DeleteDatasetType.test.ts b/test/functional/datasets/DeleteDatasetType.test.ts new file mode 100644 index 00000000..8f447822 --- /dev/null +++ b/test/functional/datasets/DeleteDatasetType.test.ts @@ -0,0 +1,28 @@ +import { ApiConfig, DatasetType, addDatasetType, deleteDatasetType } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('DeleteDatasetType', () => { + describe('execute', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should allow for adding and deleting a dataset type', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + expect(actual.name).toEqual(randomName) + + const deleted: void = await deleteDatasetType.execute(actual.id as number) + expect(deleted).toEqual({ message: 'deleted' }) + }) + }) +}) diff --git a/test/functional/datasets/GetDatasetAvailableDatasetType.test.ts b/test/functional/datasets/GetDatasetAvailableDatasetType.test.ts new file mode 100644 index 00000000..3d80f35a --- /dev/null +++ b/test/functional/datasets/GetDatasetAvailableDatasetType.test.ts @@ -0,0 +1,30 @@ +import { ApiConfig, DatasetType, getDatasetAvailableDatasetType } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('getDatasetAvailableDatasetType', () => { + describe('execute', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return the default available dataset type', async () => { + const defaultDatasetType = 'dataset' + const actualDatasetType: DatasetType = await getDatasetAvailableDatasetType.execute( + defaultDatasetType + ) + const expectedDatasetType = { + id: 1, + name: 'dataset', + linkedMetadataBlocks: [], + availableLicenses: [] + } + + expect(actualDatasetType).toEqual(expectedDatasetType) + }) + }) +}) diff --git a/test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts b/test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts new file mode 100644 index 00000000..ee10efc1 --- /dev/null +++ b/test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts @@ -0,0 +1,40 @@ +import { + ApiConfig, + DatasetType, + addDatasetType, + linkDatasetTypeWithMetadataBlocks +} from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('LinkDatasetTypeWithMetadataBlocks', () => { + describe('execute', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should allow for linking a dataset type to metadata blocks', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + expect(actual.name).toEqual(randomName) + + const linked: void = await linkDatasetTypeWithMetadataBlocks.execute(actual.id as number, [ + 'geospatial' + ]) + expect(linked).toEqual({ + linkedMetadataBlocks: { + before: [], + after: ['geospatial'] + } + }) + }) + }) +}) diff --git a/test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts b/test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts new file mode 100644 index 00000000..fdad0d2c --- /dev/null +++ b/test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts @@ -0,0 +1,40 @@ +import { + ApiConfig, + DatasetType, + addDatasetType, + setAvailableLicensesForDatasetType +} from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('SetAvailableLicensesForDatasetType', () => { + describe('execute', () => { + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should allow for setting available licenses for a dataset type', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + expect(actual.name).toEqual(randomName) + + const linked: void = await setAvailableLicensesForDatasetType.execute(actual.id as number, [ + 'CC BY 4.0' + ]) + expect(linked).toEqual({ + availableLicenses: { + before: [], + after: ['CC BY 4.0'] + } + }) + }) + }) +}) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index bb44f3d2..5374b5f8 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -22,7 +22,12 @@ import { DatasetDeaccessionDTO, publishDataset, DatasetType, - getDatasetAvailableDatasetTypes + getDatasetAvailableDatasetTypes, + getDatasetAvailableDatasetType, + addDatasetType, + deleteDatasetType, + linkDatasetTypeWithMetadataBlocks, + setAvailableLicensesForDatasetType } from '../../../src/datasets' import { ApiConfig, WriteError } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -1706,4 +1711,93 @@ describe('DatasetsRepository', () => { expect(actualDatasetTypes).toEqual(expectedDatasetTypes) }) }) + + describe('getDatasetAvailableDatasetType', () => { + test('should return available the default dataset type', async () => { + const defaultDatasetType = 'dataset' + const actualDatasetType: DatasetType = await getDatasetAvailableDatasetType.execute( + defaultDatasetType + ) + const expectedDatasetType = { + id: 1, + name: 'dataset', + linkedMetadataBlocks: [], + availableLicenses: [] + } + + expect(actualDatasetType).toEqual(expectedDatasetType) + }) + }) + + describe('addDatasetType', () => { + test('should add a dataset type', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + + expect(actual.name).toEqual(randomName) + }) + }) + + describe('deleteDatasetType', () => { + test('should delete a dataset type (after adding it)', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + expect(actual.name).toEqual(randomName) + + const deleted: void = await deleteDatasetType.execute(actual.id as number) + expect(deleted).toEqual({ message: 'deleted' }) + }) + }) + + describe('linkDatasetTypeWithMetadataBlocks', () => { + test('should allow for linking a dataset type to metadata blocks', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + expect(actual.name).toEqual(randomName) + + const linked: void = await linkDatasetTypeWithMetadataBlocks.execute(actual.id as number, [ + 'geospatial' + ]) + expect(linked).toEqual({ + linkedMetadataBlocks: { + before: [], + after: ['geospatial'] + } + }) + }) + }) + + describe('setAvailableLicensesForDatasetType', () => { + test('should allow for setting available licenses for a dataset type', async () => { + const randomName = `datasetType-${crypto.randomUUID().slice(0, 6)}` + const actual: DatasetType = await addDatasetType.execute({ + name: randomName, + linkedMetadataBlocks: [], + availableLicenses: [] + }) + expect(actual.name).toEqual(randomName) + + const linked: void = await setAvailableLicensesForDatasetType.execute(actual.id as number, [ + 'CC BY 4.0' + ]) + expect(linked).toEqual({ + availableLicenses: { + before: [], + after: ['CC BY 4.0'] + } + }) + }) + }) }) diff --git a/test/unit/datasets/DeleteDatasetType.test.ts b/test/unit/datasets/DeleteDatasetType.test.ts new file mode 100644 index 00000000..b8104a7a --- /dev/null +++ b/test/unit/datasets/DeleteDatasetType.test.ts @@ -0,0 +1,23 @@ +import { DeleteDatasetType } from '../../../src/datasets/domain/useCases/DeleteDatasetType' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { WriteError } from '../../../src' + +describe('execute', () => { + test('should return undefined on delete success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.deleteDatasetType = jest.fn().mockResolvedValue(undefined) + const sut = new DeleteDatasetType(datasetsRepositoryStub) + + const actual = await sut.execute(1) + expect(actual).toEqual(undefined) + }) + + test('should return error result on delete error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.deleteDatasetType = jest.fn().mockRejectedValue(new WriteError()) + const sut = new DeleteDatasetType(datasetsRepositoryStub) + + const nonExistentDatasetTypeId = 111 + await expect(sut.execute(nonExistentDatasetTypeId)).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/datasets/GetDatasetAvailableDatasetType.test.ts b/test/unit/datasets/GetDatasetAvailableDatasetType.test.ts new file mode 100644 index 00000000..d6b4d958 --- /dev/null +++ b/test/unit/datasets/GetDatasetAvailableDatasetType.test.ts @@ -0,0 +1,54 @@ +import { GetDatasetAvailableDatasetType } from '../../../src/datasets/domain/useCases/GetDatasetAvailableDatasetType' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { DatasetType } from '../../../src/datasets/domain/models/DatasetType' +import { ReadError } from '../../../src' + +describe('GetDatasetAvailableDatasetType', () => { + const datasetTypesRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + + const datasetTypeId = 1 + const datasetTypeName = 'dataset' + const expectedDatasetType: DatasetType = { + id: datasetTypeId, + name: datasetTypeName, + linkedMetadataBlocks: [], + availableLicenses: [] + } + + it('should get a dataset type by database id', async () => { + datasetTypesRepositoryStub.getDatasetAvailableDatasetType = jest + .fn() + .mockResolvedValue(expectedDatasetType) + const sut = new GetDatasetAvailableDatasetType(datasetTypesRepositoryStub) + + const actual = await sut.execute(datasetTypeId) + + expect(actual).toEqual(expectedDatasetType) + expect(datasetTypesRepositoryStub.getDatasetAvailableDatasetType).toHaveBeenCalledTimes(1) + }) + + it('should get a dataset type by name', async () => { + datasetTypesRepositoryStub.getDatasetAvailableDatasetType = jest + .fn() + .mockResolvedValue(expectedDatasetType) + const sut = new GetDatasetAvailableDatasetType(datasetTypesRepositoryStub) + + const actual = await sut.execute(datasetTypeName) + + expect(actual).toEqual(expectedDatasetType) + expect(datasetTypesRepositoryStub.getDatasetAvailableDatasetType).toHaveBeenCalledTimes(1) + }) + + test('should return error result on repository error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + const datasetTypeId = 1 + const expectedError = new ReadError('Failed to fetch dataset type') + datasetsRepositoryStub.getDatasetAvailableDatasetType = jest + .fn() + .mockRejectedValue(expectedError) + const sut = new GetDatasetAvailableDatasetType(datasetsRepositoryStub) + + await expect(sut.execute(datasetTypeId)).rejects.toThrow(ReadError) + expect(datasetsRepositoryStub.getDatasetAvailableDatasetType).toHaveBeenCalledTimes(1) + }) +}) diff --git a/test/unit/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts b/test/unit/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts new file mode 100644 index 00000000..c284e0c1 --- /dev/null +++ b/test/unit/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts @@ -0,0 +1,27 @@ +import { LinkDatasetTypeWithMetadataBlocks } from '../../../src/datasets/domain/useCases/LinkDatasetTypeWithMetadataBlocks' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { WriteError } from '../../../src' + +describe('execute', () => { + test('should return undefined on link dataset type with metadata block success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.linkDatasetTypeWithMetadataBlocks = jest + .fn() + .mockResolvedValue(undefined) + const sut = new LinkDatasetTypeWithMetadataBlocks(datasetsRepositoryStub) + + const actual = await sut.execute(1, ['geospatial']) + expect(actual).toEqual(undefined) + }) + + test('should return error result on link dataset type with metadata block error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.linkDatasetTypeWithMetadataBlocks = jest + .fn() + .mockRejectedValue(new WriteError()) + const sut = new LinkDatasetTypeWithMetadataBlocks(datasetsRepositoryStub) + + const nonExistentDatasetTypeId = 111 + await expect(sut.execute(nonExistentDatasetTypeId, ['geospatial'])).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/datasets/SetAvailableLicensesForDatasetType.test.ts b/test/unit/datasets/SetAvailableLicensesForDatasetType.test.ts new file mode 100644 index 00000000..965ebcef --- /dev/null +++ b/test/unit/datasets/SetAvailableLicensesForDatasetType.test.ts @@ -0,0 +1,27 @@ +import { SetAvailableLicensesForDatasetType } from '../../../src/datasets/domain/useCases/SetAvailableLicensesForDatasetType' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { WriteError } from '../../../src' + +describe('execute', () => { + test('should return undefined on set available licenses for dataset type success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.setAvailableLicensesForDatasetType = jest + .fn() + .mockResolvedValue(undefined) + const sut = new SetAvailableLicensesForDatasetType(datasetsRepositoryStub) + + const actual = await sut.execute(1, ['geospatial']) + expect(actual).toEqual(undefined) + }) + + test('should return error result on set available licenses for dataset type error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.setAvailableLicensesForDatasetType = jest + .fn() + .mockRejectedValue(new WriteError()) + const sut = new SetAvailableLicensesForDatasetType(datasetsRepositoryStub) + + const nonExistentDatasetTypeId = 111 + await expect(sut.execute(nonExistentDatasetTypeId, ['geospatial'])).rejects.toThrow(WriteError) + }) +}) From 6f5df7ba3071adfe5a5324e89675fd5fab77181c Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 23 Sep 2025 09:03:27 -0400 Subject: [PATCH 114/123] delete dataset types after testing is done #370 --- .../datasets/LinkDatasetTypeWithMetadataBlocks.test.ts | 4 ++++ .../datasets/SetAvailableLicensesForDatasetType.test.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts b/test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts index ee10efc1..c6f4c3f8 100644 --- a/test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts +++ b/test/functional/datasets/LinkDatasetTypeWithMetadataBlocks.test.ts @@ -2,6 +2,7 @@ import { ApiConfig, DatasetType, addDatasetType, + deleteDatasetType, linkDatasetTypeWithMetadataBlocks } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -35,6 +36,9 @@ describe('LinkDatasetTypeWithMetadataBlocks', () => { after: ['geospatial'] } }) + + const deleted: void = await deleteDatasetType.execute(actual.id as number) + expect(deleted).toEqual({ message: 'deleted' }) }) }) }) diff --git a/test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts b/test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts index fdad0d2c..0c4d6876 100644 --- a/test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts +++ b/test/functional/datasets/SetAvailableLicensesForDatasetType.test.ts @@ -2,6 +2,7 @@ import { ApiConfig, DatasetType, addDatasetType, + deleteDatasetType, setAvailableLicensesForDatasetType } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -35,6 +36,9 @@ describe('SetAvailableLicensesForDatasetType', () => { after: ['CC BY 4.0'] } }) + + const deleted: void = await deleteDatasetType.execute(actual.id as number) + expect(deleted).toEqual({ message: 'deleted' }) }) }) }) From fd1779c504cd266218af422ccad7bd2f54b30c7f Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 23 Sep 2025 09:16:37 -0400 Subject: [PATCH 115/123] make prettier happy #370 --- docs/useCases.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index ece5c722..ff537daa 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -1208,7 +1208,7 @@ import { linkDatasetTypeWithMetadataBlocks } from '@iqss/dataverse-client-javasc /* ... */ -linkDatasetTypeWithMetadataBlocks.execute(datasetTypeId, ["geospatial"]).then(() => { +linkDatasetTypeWithMetadataBlocks.execute(datasetTypeId, ['geospatial']).then(() => { /* ... */ }) ``` @@ -1226,7 +1226,7 @@ import { setAvailableLicensesForDatasetType } from '@iqss/dataverse-client-javas /* ... */ -setAvailableLicensesForDatasetType.execute(datasetTypeId, ["CC BY 4.0"]).then(() => { +setAvailableLicensesForDatasetType.execute(datasetTypeId, ['CC BY 4.0']).then(() => { /* ... */ }) ``` From ad3f1cfb791f29c2d5ff5d1c50ec8dc476a7919e Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 23 Sep 2025 15:06:44 -0400 Subject: [PATCH 116/123] allow dataset to be created with non-default dataset type #359 --- docs/useCases.md | 4 +- src/datasets/domain/models/Dataset.ts | 1 + .../repositories/IDatasetsRepository.ts | 3 +- src/datasets/domain/useCases/CreateDataset.ts | 11 +- .../infra/repositories/DatasetsRepository.ts | 9 +- .../transformers/DatasetPayload.ts | 1 + .../transformers/datasetTransformers.ts | 8 +- .../functional/datasets/CreateDataset.test.ts | 102 +++++++++++++++++- .../datasets/DatasetsRepository.test.ts | 59 ++++++++++ test/testHelpers/datasets/datasetHelper.ts | 4 +- test/unit/datasets/CreateDataset.test.ts | 47 +++++++- .../unit/datasets/datasetTransformers.test.ts | 24 +++++ 12 files changed, 261 insertions(+), 12 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index ff537daa..94b36731 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -860,7 +860,7 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetAvailableDatasetType.t #### Create a Dataset -Creates a new Dataset in a collection, given a [DatasetDTO](../src/datasets/domain/dtos/DatasetDTO.ts) object and an optional collection identifier, which defaults to `:root`. +Creates a new Dataset in a collection, given a [DatasetDTO](../src/datasets/domain/dtos/DatasetDTO.ts) object, an optional collection identifier, which defaults to `:root`, and an optional dataset type. This use case validates the submitted fields of each metadata block and can return errors of type [ResourceValidationError](../src/core/domain/useCases/validators/errors/ResourceValidationError.ts), which include sufficient information to determine which field value is invalid and why. @@ -915,7 +915,7 @@ createDataset.execute(datasetDTO).then((newDatasetIds: CreatedDatasetIdentifiers _See [use case](../src/datasets/domain/useCases/CreateDataset.ts) implementation_. -The above example creates the new dataset in the root collection since no collection identifier is specified. If you want to create the dataset in a different collection, you must add the collection identifier as a second parameter in the use case call. +The above example creates the new dataset in the root collection since no collection identifier is specified. If you want to create the dataset in a different collection, you must add the collection identifier as a second parameter in the use case call. If you want the dataset type to be anything other than dataset, first [check available dataset types](#get-dataset-available-dataset-types) and then add the name of the dataset type as the third parameter. The use case returns a [CreatedDatasetIdentifiers](../src/datasets/domain/models/CreatedDatasetIdentifiers.ts) object, which includes the persistent and numeric identifiers of the created dataset. diff --git a/src/datasets/domain/models/Dataset.ts b/src/datasets/domain/models/Dataset.ts index caecd3ae..e858de9e 100644 --- a/src/datasets/domain/models/Dataset.ts +++ b/src/datasets/domain/models/Dataset.ts @@ -14,6 +14,7 @@ export interface Dataset { citationDate?: string metadataBlocks: DatasetMetadataBlocks isPartOf: DvObjectOwnerNode + datasetType?: string } export interface DatasetVersionInfo { diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 3fe1c7ab..e78816c4 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -46,7 +46,8 @@ export interface IDatasetsRepository { createDataset( newDataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], - collectionId: string + collectionId: string, + datasetType?: string ): Promise publishDataset(datasetId: number | string, versionUpdateType: VersionUpdateType): Promise updateDataset( diff --git a/src/datasets/domain/useCases/CreateDataset.ts b/src/datasets/domain/useCases/CreateDataset.ts index 65bffae4..090c0721 100644 --- a/src/datasets/domain/useCases/CreateDataset.ts +++ b/src/datasets/domain/useCases/CreateDataset.ts @@ -20,6 +20,7 @@ export class CreateDataset extends DatasetWriteUseCase} * @throws {ResourceValidationError} - If there are validation errors related to the provided information. * @throws {ReadError} - If there are errors while reading data. @@ -27,10 +28,16 @@ export class CreateDataset extends DatasetWriteUseCase { const metadataBlocks = await this.getNewDatasetMetadataBlocks(newDataset) this.getNewDatasetValidator().validate(newDataset, metadataBlocks) - return this.getDatasetsRepository().createDataset(newDataset, metadataBlocks, collectionId) + return this.getDatasetsRepository().createDataset( + newDataset, + metadataBlocks, + collectionId, + datasetType + ) } } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 99b380df..1545a43d 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -208,11 +208,16 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi public async createDataset( newDataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], - collectionId: string + collectionId: string, + datasetType?: string ): Promise { return this.doPost( `/dataverses/${collectionId}/datasets`, - transformDatasetModelToNewDatasetRequestPayload(newDataset, datasetMetadataBlocks) + transformDatasetModelToNewDatasetRequestPayload( + newDataset, + datasetMetadataBlocks, + datasetType + ) ) .then((response) => { const responseData = response.data.data diff --git a/src/datasets/infra/repositories/transformers/DatasetPayload.ts b/src/datasets/infra/repositories/transformers/DatasetPayload.ts index bf6a70fb..b0535677 100644 --- a/src/datasets/infra/repositories/transformers/DatasetPayload.ts +++ b/src/datasets/infra/repositories/transformers/DatasetPayload.ts @@ -36,6 +36,7 @@ export interface DatasetPayload { files: FilePayload[] isPartOf: OwnerNodePayload deaccessionNote?: string + datasetType?: string } export interface DatasetLicensePayload { diff --git a/src/datasets/infra/repositories/transformers/datasetTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTransformers.ts index 7d771fe5..bbb4c9fc 100644 --- a/src/datasets/infra/repositories/transformers/datasetTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTransformers.ts @@ -31,6 +31,7 @@ import { MetadataBlock, MetadataFieldInfo } from '../../../../metadataBlocks' const turndownService = new TurndownService() export interface NewDatasetRequestPayload { + datasetType?: string datasetVersion: { license?: DatasetLicense metadataBlocks: Record @@ -96,9 +97,11 @@ export const transformDatasetModelToUpdateDatasetRequestPayload = ( export const transformDatasetModelToNewDatasetRequestPayload = ( dataset: DatasetDTO, - metadataBlocks: MetadataBlock[] + metadataBlocks: MetadataBlock[], + datasetType?: string ): NewDatasetRequestPayload => { return { + datasetType, datasetVersion: { ...(dataset.license && { license: dataset.license }), metadataBlocks: transformMetadataBlockModelsToRequestPayload( @@ -293,6 +296,9 @@ export const transformVersionPayloadToDataset = ( if ('citationDate' in versionPayload) { datasetModel.citationDate = versionPayload.citationDate } + if ('datasetType' in versionPayload) { + datasetModel.datasetType = versionPayload.datasetType + } return datasetModel } diff --git a/test/functional/datasets/CreateDataset.test.ts b/test/functional/datasets/CreateDataset.test.ts index f90eff00..1394a3a4 100644 --- a/test/functional/datasets/CreateDataset.test.ts +++ b/test/functional/datasets/CreateDataset.test.ts @@ -1,5 +1,5 @@ import { createDataset, DatasetDTO } from '../../../src/datasets' -import { ApiConfig } from '../../../src' +import { ApiConfig, WriteError } from '../../../src' import { TestConstants } from '../../testHelpers/TestConstants' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' import { FieldValidationError } from '../../../src/datasets/domain/useCases/validators/errors/FieldValidationError' @@ -61,6 +61,58 @@ describe('execute', () => { } }) + test('should successfully create a new dataset when a valid dataset type is sent', async () => { + const testNewDataset = { + metadataBlockValues: [ + { + name: 'citation', + fields: { + title: 'Dataset created using the createDataset use case', + author: [ + { + authorName: 'Admin, Dataverse', + authorAffiliation: 'Dataverse.org' + }, + { + authorName: 'Owner, Dataverse', + authorAffiliation: 'Dataversedemo.org' + } + ], + datasetContact: [ + { + datasetContactEmail: 'finch@mailinator.com', + datasetContactName: 'Finch, Fiona' + } + ], + dsDescription: [ + { + dsDescriptionValue: 'This is the description of the dataset.' + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } + ] + } + expect.assertions(3) + + try { + const defaultDatasetType = 'dataset' + const createdDatasetIdentifiers = await createDataset.execute( + testNewDataset, + ':root', + defaultDatasetType + ) + + expect(createdDatasetIdentifiers).not.toBeNull() + expect(createdDatasetIdentifiers.numericId).not.toBeNull() + expect(createdDatasetIdentifiers.persistentId).not.toBeNull() + await deleteUnpublishedDatasetViaApi(createdDatasetIdentifiers.numericId) + } catch (error) { + throw new Error('Dataset should be created') + } + }) + test('should throw an error when a first level required field is missing', async () => { const testNewDataset = { metadataBlockValues: [ @@ -213,4 +265,52 @@ describe('execute', () => { ) } }) + + test('should throw an error when an invalid dataset type is sent', async () => { + const testNewDataset = { + metadataBlockValues: [ + { + name: 'citation', + fields: { + title: 'Dataset created using the createDataset use case', + author: [ + { + authorName: 'Admin, Dataverse', + authorAffiliation: 'Dataverse.org' + }, + { + authorName: 'Owner, Dataverse', + authorAffiliation: 'Dataversedemo.org' + } + ], + datasetContact: [ + { + datasetContactEmail: 'finch@mailinator.com', + datasetContactName: 'Finch, Fiona' + } + ], + dsDescription: [ + { + dsDescriptionValue: 'This is the description of the dataset.' + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } + ] + } + expect.assertions(1) + let writeError: WriteError | undefined = undefined + try { + const invalidDatasetType = 'doesNotExist' + await createDataset.execute(testNewDataset, ':root', invalidDatasetType) + throw new Error('Use case should throw an error') + } catch (error) { + writeError = error as WriteError + } finally { + expect(writeError?.message).toEqual( + 'There was an error when writing the resource. Reason was: [400] Error parsing Json: Invalid dataset type: doesNotExist' + ) + } + }) }) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 5374b5f8..af669e7c 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -110,6 +110,7 @@ describe('DatasetsRepository', () => { const filesRepositorySut = new FilesRepository() const directUploadSut: DirectUploadClient = new DirectUploadClient(filesRepositorySut) + const defaultDatasetType = 'dataset' beforeAll(async () => { ApiConfig.init( @@ -827,6 +828,64 @@ describe('DatasetsRepository', () => { expect(actualCreatedDataset.metadataBlocks[0].fields.subject).toContain( 'Medicine, Health and Life Sciences' ) + // even though we didn't provide a dataset type, it should be created with the default one + expect(actualCreatedDataset.datasetType).toBe(defaultDatasetType) + }) + }) + + describe('createDatasetWithDatasetType', () => { + test('should create a dataset with the provided dataset type', async () => { + const testNewDataset = { + metadataBlockValues: [ + { + name: 'citation', + fields: { + title: 'Dataset created using the createDataset use case', + author: [ + { + authorName: 'Admin, Dataverse', + authorAffiliation: 'Dataverse.org' + }, + { + authorName: 'Owner, Dataverse', + authorAffiliation: 'Dataversedemo.org' + } + ], + datasetContact: [ + { + datasetContactEmail: 'finch@mailinator.com', + datasetContactName: 'Finch, Fiona' + } + ], + dsDescription: [ + { + dsDescriptionValue: 'This is the description of the dataset.' + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } + ] + } + + const metadataBlocksRepository = new MetadataBlocksRepository() + const citationMetadataBlock = await metadataBlocksRepository.getMetadataBlockByName( + 'citation' + ) + const createdDataset = await sut.createDataset( + testNewDataset, + [citationMetadataBlock], + ROOT_COLLECTION_ALIAS, + defaultDatasetType + ) + const actualCreatedDataset = await sut.getDataset( + createdDataset.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + + expect(actualCreatedDataset.datasetType).toBe(defaultDatasetType) }) }) diff --git a/test/testHelpers/datasets/datasetHelper.ts b/test/testHelpers/datasets/datasetHelper.ts index 65575bdc..4cb1ee79 100644 --- a/test/testHelpers/datasets/datasetHelper.ts +++ b/test/testHelpers/datasets/datasetHelper.ts @@ -694,9 +694,11 @@ export const createDatasetMetadataBlockModel = (): MetadataBlock => { } export const createNewDatasetRequestPayload = ( - license?: DatasetLicense + license?: DatasetLicense, + datasetType?: string ): NewDatasetRequestPayload => { return { + datasetType, datasetVersion: { ...(license && { license }), metadataBlocks: { diff --git a/test/unit/datasets/CreateDataset.test.ts b/test/unit/datasets/CreateDataset.test.ts index 92c73d78..edc69abb 100644 --- a/test/unit/datasets/CreateDataset.test.ts +++ b/test/unit/datasets/CreateDataset.test.ts @@ -51,7 +51,49 @@ describe('execute', () => { expect(datasetsRepositoryStub.createDataset).toHaveBeenCalledWith( testDataset, testMetadataBlocks, - ROOT_COLLECTION_ID + ROOT_COLLECTION_ID, + undefined + ) + }) + + test('should return a dataset type', async () => { + const testCreatedDatasetIdentifiers: CreatedDatasetIdentifiers = { + persistentId: 'test', + numericId: 1 + } + + const datasetsRepositoryStub = {} + datasetsRepositoryStub.createDataset = jest + .fn() + .mockResolvedValue(testCreatedDatasetIdentifiers) + + const datasetValidatorStub = {} + datasetValidatorStub.validate = jest.fn().mockResolvedValue(undefined) + + const metadataBlocksRepositoryStub = {} + metadataBlocksRepositoryStub.getMetadataBlockByName = jest + .fn() + .mockResolvedValue(testMetadataBlocks[0]) + + const sut = new CreateDataset( + datasetsRepositoryStub, + metadataBlocksRepositoryStub, + datasetValidatorStub + ) + + const actual = await sut.execute(testDataset, ROOT_COLLECTION_ID, 'software') + + expect(actual).toEqual(testCreatedDatasetIdentifiers) + + expect(metadataBlocksRepositoryStub.getMetadataBlockByName).toHaveBeenCalledWith( + testMetadataBlocks[0].name + ) + expect(datasetValidatorStub.validate).toHaveBeenCalledWith(testDataset, testMetadataBlocks) + expect(datasetsRepositoryStub.createDataset).toHaveBeenCalledWith( + testDataset, + testMetadataBlocks, + ROOT_COLLECTION_ID, + 'software' ) }) @@ -111,7 +153,8 @@ describe('execute', () => { expect(datasetsRepositoryStub.createDataset).toHaveBeenCalledWith( testDataset, testMetadataBlocks, - ROOT_COLLECTION_ID + ROOT_COLLECTION_ID, + undefined ) }) diff --git a/test/unit/datasets/datasetTransformers.test.ts b/test/unit/datasets/datasetTransformers.test.ts index e659f533..7e4185c7 100644 --- a/test/unit/datasets/datasetTransformers.test.ts +++ b/test/unit/datasets/datasetTransformers.test.ts @@ -33,4 +33,28 @@ describe('transformNewDatasetModelToRequestPayload', () => { expect(actual).toEqual(expectedNewDatasetRequestPayload) }) + + it('should correctly transform a new dataset model to a new dataset request payload when it contains a license and a datasetType', () => { + const testDataset = createDatasetDTO( + undefined, + undefined, + undefined, + undefined, + undefined, + createDatasetLicenseModel() + ) + const testMetadataBlocks = [createDatasetMetadataBlockModel()] + const datasetType = 'software' + const expectedNewDatasetRequestPayload = createNewDatasetRequestPayload( + createDatasetLicenseModel(), + datasetType + ) + const actual = transformDatasetModelToNewDatasetRequestPayload( + testDataset, + testMetadataBlocks, + datasetType + ) + + expect(actual).toEqual(expectedNewDatasetRequestPayload) + }) }) From d1ee2763f88f02acf0bc5671d9e7336f4b7e73d5 Mon Sep 17 00:00:00 2001 From: Philip Durbin Date: Tue, 23 Sep 2025 15:39:48 -0400 Subject: [PATCH 117/123] update getCollectionMetadataBlocks to support dataset types #210 --- docs/useCases.md | 2 ++ .../domain/repositories/IMetadataBlocksRepository.ts | 3 ++- .../domain/useCases/GetCollectionMetadataBlocks.ts | 7 +++++-- .../infra/repositories/MetadataBlocksRepository.ts | 6 ++++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index 94b36731..60704c23 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -2012,6 +2012,8 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe There is a second optional parameter called `onlyDisplayedOnCreate` which indicates whether or not to return only the metadata blocks that are displayed on dataset creation. The default value is false. +There is a third optional parameter called `datasetType` which will include additional fields from metadata blocks linked to the provided type, if any. Before using this parameter, you will probably want to [list available dataset types](#get-dataset-available-dataset-types) for your installation. + ## Users ### Users read use cases diff --git a/src/metadataBlocks/domain/repositories/IMetadataBlocksRepository.ts b/src/metadataBlocks/domain/repositories/IMetadataBlocksRepository.ts index dd7a802d..7acc63db 100644 --- a/src/metadataBlocks/domain/repositories/IMetadataBlocksRepository.ts +++ b/src/metadataBlocks/domain/repositories/IMetadataBlocksRepository.ts @@ -5,7 +5,8 @@ export interface IMetadataBlocksRepository { getCollectionMetadataBlocks( collectionIdOrAlias: number | string, - onlyDisplayedOnCreate: boolean + onlyDisplayedOnCreate: boolean, + datasetType?: string ): Promise getAllMetadataBlocks(): Promise diff --git a/src/metadataBlocks/domain/useCases/GetCollectionMetadataBlocks.ts b/src/metadataBlocks/domain/useCases/GetCollectionMetadataBlocks.ts index c953c16a..1f71be22 100644 --- a/src/metadataBlocks/domain/useCases/GetCollectionMetadataBlocks.ts +++ b/src/metadataBlocks/domain/useCases/GetCollectionMetadataBlocks.ts @@ -16,15 +16,18 @@ export class GetCollectionMetadataBlocks implements UseCase { * @param {number | string} [collectionIdOrAlias = ':root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) * If this parameter is not set, the default value is: ':root' * @param {boolean} [onlyDisplayedOnCreate=false] - Indicates whether or not to return only the metadata blocks that are displayed on dataset creation. The default value is false. + * @param {string} [datasetType] - The name of the dataset type. If provided, additional fields from metadata blocks linked to this dataset type will be returned. * @returns {Promise} */ async execute( collectionIdOrAlias: number | string = ROOT_COLLECTION_ID, - onlyDisplayedOnCreate = false + onlyDisplayedOnCreate = false, + datasetType?: string ): Promise { return await this.metadataBlocksRepository.getCollectionMetadataBlocks( collectionIdOrAlias, - onlyDisplayedOnCreate + onlyDisplayedOnCreate, + datasetType ) } } diff --git a/src/metadataBlocks/infra/repositories/MetadataBlocksRepository.ts b/src/metadataBlocks/infra/repositories/MetadataBlocksRepository.ts index ab308240..6b4f0287 100644 --- a/src/metadataBlocks/infra/repositories/MetadataBlocksRepository.ts +++ b/src/metadataBlocks/infra/repositories/MetadataBlocksRepository.ts @@ -17,11 +17,13 @@ export class MetadataBlocksRepository extends ApiRepository implements IMetadata public async getCollectionMetadataBlocks( collectionIdOrAlias: string | number, - onlyDisplayedOnCreate: boolean + onlyDisplayedOnCreate: boolean, + datasetType?: string ): Promise { return this.doGet(`/dataverses/${collectionIdOrAlias}/metadatablocks`, true, { onlyDisplayedOnCreate: onlyDisplayedOnCreate, - returnDatasetFieldTypes: true + returnDatasetFieldTypes: true, + datasetType: datasetType }) .then((response) => transformMetadataBlocksResponseToMetadataBlocks(response)) .catch((error) => { From b1ada1778e4e739a38f9bd22f506771654e4ab1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 24 Sep 2025 15:28:14 -0300 Subject: [PATCH 118/123] docs: remove dataverse wording --- docs/useCases.md | 2 +- src/collections/domain/useCases/GetCollectionsForLinking.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index c87f08e8..db3a8cd3 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -344,7 +344,7 @@ const collectionIdOrAlias: number | string = 'collectionAlias' // or 123 const searchTerm = 'searchOn' getCollectionsForLinking - .execute('dataverse', collectionIdOrAlias, searchTerm) + .execute('collection', collectionIdOrAlias, searchTerm) .then((collections) => { // collections: CollectionSummary[] /* ... */ diff --git a/src/collections/domain/useCases/GetCollectionsForLinking.ts b/src/collections/domain/useCases/GetCollectionsForLinking.ts index c9486c9f..e01e156e 100644 --- a/src/collections/domain/useCases/GetCollectionsForLinking.ts +++ b/src/collections/domain/useCases/GetCollectionsForLinking.ts @@ -13,8 +13,8 @@ export class GetCollectionsForLinking implements UseCase { /** * Returns an array of CollectionSummary (id, alias, displayName) to which the given Dataverse collection or Dataset may be linked. - * @param objectType - 'dataverse' when providing a collection identifier/alias; 'dataset' when providing a dataset persistentId. - * @param id - For objectType 'dataverse', a numeric id or alias string. For 'dataset', the persistentId string (e.g., doi:...) + * @param objectType - 'collection' when providing a collection identifier/alias; 'dataset' when providing a dataset persistentId. + * @param id - For objectType 'collection', a numeric id or alias string. For 'dataset', the persistentId string (e.g., doi:...) * @param searchTerm - Optional search term to filter by collection name. Defaults to empty string (no filtering). * @param alreadyLinked - Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. */ From 8cc6b9d5f91cf02c06b3996d2a57cd12ad40ac11 Mon Sep 17 00:00:00 2001 From: German Gonzalo Saracca Date: Thu, 25 Sep 2025 13:16:50 +0200 Subject: [PATCH 119/123] Revert "Get Collections For Linking Use Case" --- docs/useCases.md | 64 ----------- .../repositories/ICollectionsRepository.ts | 8 -- .../useCases/GetCollectionsForLinking.ts | 34 ------ src/collections/index.ts | 6 +- .../repositories/CollectionsRepository.ts | 45 +------- .../transformers/collectionTransformers.ts | 8 +- .../repositories/IDatasetsRepository.ts | 4 +- src/datasets/domain/useCases/LinkDataset.ts | 8 +- src/datasets/domain/useCases/UnlinkDataset.ts | 8 +- .../infra/repositories/DatasetsRepository.ts | 24 +---- test/environment/.env | 4 +- .../collections/CollectionsRepository.test.ts | 100 +----------------- .../datasets/DatasetsRepository.test.ts | 36 ------- .../collections/CollectionsRepository.test.ts | 49 --------- .../GetCollectionsForLinking.test.ts | 33 ------ 15 files changed, 20 insertions(+), 411 deletions(-) delete mode 100644 src/collections/domain/useCases/GetCollectionsForLinking.ts delete mode 100644 test/unit/collections/GetCollectionsForLinking.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index cb95e01f..94b36731 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -16,7 +16,6 @@ The different use cases currently available in the package are classified below, - [List All Collection Items](#list-all-collection-items) - [List My Data Collection Items](#list-my-data-collection-items) - [Get Collection Featured Items](#get-collection-featured-items) - - [Get Collections for Linking](#get-collections-for-linking) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) @@ -337,69 +336,6 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe If no collection identifier is specified, the default collection identifier; `:root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. -#### Get Collections for Linking - -Returns an array of [CollectionSummary](../src/collections/domain/models/CollectionSummary.ts) (id, alias, displayName) representing the Dataverse collections to which a given Dataverse collection or Dataset may be linked. - -This use case supports an optional `searchTerm` to filter by collection name. - -##### Example calls: - -```typescript -import { getCollectionsForLinking } from '@iqss/dataverse-client-javascript' - -/* ... */ - -// Case 1: For a given Dataverse collection (by numeric id or alias) -const collectionIdOrAlias: number | string = 'collectionAlias' // or 123 -const searchTerm = 'searchOn' - -getCollectionsForLinking - .execute('collection', collectionIdOrAlias, searchTerm) - .then((collections) => { - // collections: CollectionSummary[] - /* ... */ - }) - .catch((error: Error) => { - /* ... */ - }) - -/* ... */ - -// Case 2: For a given Dataset (by persistent identifier) -const persistentId = 'doi:10.5072/FK2/J8SJZB' - -getCollectionsForLinking - .execute('dataset', persistentId, searchTerm) - .then((collections) => { - // collections: CollectionSummary[] - /* ... */ - }) - .catch((error: Error) => { - /* ... */ - }) - -// Case 3: [alreadyLinked] Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. -const alreadyLinked = true - -getCollectionsForLinking - .execute('dataset', persistentId, searchTerm, alreadyLinked) - .then((collections) => { - // collections: CollectionSummary[] - /* ... */ - }) - .catch((error: Error) => { - /* ... */ - }) -``` - -_See [use case](../src/collections/domain/useCases/GetCollectionsForLinking.ts) implementation_. - -Notes: - -- When the first argument is `'collection'`, the second argument can be a numeric collection id or a collection alias. -- When the first argument is `'dataset'`, the second argument must be the dataset persistent identifier string (e.g., `doi:...`). - ### Collections Write Use Cases #### Create a Collection diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index bc8960c8..820a1356 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -10,8 +10,6 @@ import { CollectionUserPermissions } from '../models/CollectionUserPermissions' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { CollectionItemType } from '../../../collections/domain/models/CollectionItemType' import { CollectionLinks } from '../models/CollectionLinks' -import { CollectionSummary } from '../models/CollectionSummary' -import { LinkingObjectType } from '../useCases/GetCollectionsForLinking' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -62,10 +60,4 @@ export interface ICollectionsRepository { linkingCollectionIdOrAlias: number | string ): Promise getCollectionLinks(collectionIdOrAlias: number | string): Promise - getCollectionsForLinking( - objectType: LinkingObjectType, - id: number | string, - searchTerm: string, - alreadyLinked: boolean - ): Promise } diff --git a/src/collections/domain/useCases/GetCollectionsForLinking.ts b/src/collections/domain/useCases/GetCollectionsForLinking.ts deleted file mode 100644 index e01e156e..00000000 --- a/src/collections/domain/useCases/GetCollectionsForLinking.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UseCase } from '../../../core/domain/useCases/UseCase' -import { ICollectionsRepository } from '../repositories/ICollectionsRepository' -import { CollectionSummary } from '../models/CollectionSummary' - -export type LinkingObjectType = 'collection' | 'dataset' - -export class GetCollectionsForLinking implements UseCase { - private collectionsRepository: ICollectionsRepository - - constructor(collectionsRepository: ICollectionsRepository) { - this.collectionsRepository = collectionsRepository - } - - /** - * Returns an array of CollectionSummary (id, alias, displayName) to which the given Dataverse collection or Dataset may be linked. - * @param objectType - 'collection' when providing a collection identifier/alias; 'dataset' when providing a dataset persistentId. - * @param id - For objectType 'collection', a numeric id or alias string. For 'dataset', the persistentId string (e.g., doi:...) - * @param searchTerm - Optional search term to filter by collection name. Defaults to empty string (no filtering). - * @param alreadyLinked - Optional flag. When true, returns collections currently linked (candidates to unlink). Defaults to false. - */ - async execute( - objectType: LinkingObjectType, - id: number | string, - searchTerm = '', - alreadyLinked = false - ): Promise { - return await this.collectionsRepository.getCollectionsForLinking( - objectType, - id, - searchTerm, - alreadyLinked - ) - } -} diff --git a/src/collections/index.ts b/src/collections/index.ts index 59e2e50b..05e49954 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -15,7 +15,6 @@ import { DeleteCollectionFeaturedItem } from './domain/useCases/DeleteCollection import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' -import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking' const collectionsRepository = new CollectionsRepository() @@ -35,7 +34,6 @@ const deleteCollectionFeaturedItem = new DeleteCollectionFeaturedItem(collection const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) -const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository) export { getCollection, @@ -53,8 +51,7 @@ export { deleteCollectionFeaturedItem, linkCollection, unlinkCollection, - getCollectionLinks, - getCollectionsForLinking + getCollectionLinks } export { Collection, CollectionInputLevel } from './domain/models/Collection' export { CollectionFacet } from './domain/models/CollectionFacet' @@ -65,4 +62,3 @@ export { CollectionItemType } from './domain/models/CollectionItemType' export { CollectionSearchCriteria } from './domain/models/CollectionSearchCriteria' export { FeaturedItem } from './domain/models/FeaturedItem' export { FeaturedItemsDTO } from './domain/dtos/FeaturedItemsDTO' -export { CollectionSummary } from './domain/models/CollectionSummary' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index e0e459b0..704367e2 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -38,8 +38,6 @@ import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { PublicationStatus } from '../../../core/domain/models/PublicationStatus' import { ReadError } from '../../../core/domain/repositories/ReadError' import { CollectionLinks } from '../../domain/models/CollectionLinks' -import { CollectionSummary } from '../../domain/models/CollectionSummary' -import { LinkingObjectType } from '../../domain/useCases/GetCollectionsForLinking' export interface NewCollectionRequestPayload { alias: string @@ -95,6 +93,7 @@ export enum GetMyDataCollectionItemsQueryParams { export class CollectionsRepository extends ApiRepository implements ICollectionsRepository { private readonly collectionsResourceName: string = 'dataverses' + public async getCollection( collectionIdOrAlias: number | string = ROOT_COLLECTION_ID ): Promise { @@ -486,46 +485,4 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } - - public async getCollectionsForLinking( - objectType: LinkingObjectType, - id: number | string, - searchTerm: string, - alreadyLinked: boolean - ): Promise { - let path: string - const queryParams = new URLSearchParams() - if (objectType === 'collection') { - path = `/${this.collectionsResourceName}/${id}/dataverse/linkingDataverses` - } else { - path = `/${this.collectionsResourceName}/:persistentId/dataset/linkingDataverses` - queryParams.set('persistentId', String(id)) - } - - if (searchTerm) { - queryParams.set('searchTerm', searchTerm) - } - - if (alreadyLinked) { - queryParams.set('alreadyLinking', 'true') - } - - return this.doGet(path, true, queryParams) - .then((response) => { - const payload = response.data.data as { - id: number - alias: string - name: string - }[] - - return payload.map((item) => ({ - id: item.id, - alias: item.alias, - displayName: item.name - })) - }) - .catch((error) => { - throw error - }) - } } diff --git a/src/collections/infra/repositories/transformers/collectionTransformers.ts b/src/collections/infra/repositories/transformers/collectionTransformers.ts index fa23b8ed..a26c4718 100644 --- a/src/collections/infra/repositories/transformers/collectionTransformers.ts +++ b/src/collections/infra/repositories/transformers/collectionTransformers.ts @@ -159,13 +159,7 @@ export const transformCollectionLinksResponseToCollectionLinks = ( const responseDataPayload = response.data.data const linkedCollections = responseDataPayload.linkedDataverses const collectionsLinkingToThis = responseDataPayload.dataversesLinkingToThis - const linkedDatasets = responseDataPayload.linkedDatasets.map( - (ld: { identifier: string; title: string }) => ({ - persistentId: ld.identifier, - title: ld.title - }) - ) - + const linkedDatasets = responseDataPayload.linkedDatasets return { linkedCollections, collectionsLinkingToThis, diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 621a5a06..e78816c4 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -67,8 +67,8 @@ export interface IDatasetsRepository { ): Promise getDatasetVersionsSummaries(datasetId: number | string): Promise deleteDatasetDraft(datasetId: number | string): Promise - linkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise - unlinkDataset(datasetId: number | string, collectionIdOrAlias: number | string): Promise + linkDataset(datasetId: number, collectionAlias: string): Promise + unlinkDataset(datasetId: number, collectionAlias: string): Promise getDatasetLinkedCollections(datasetId: number | string): Promise getDatasetAvailableCategories(datasetId: number | string): Promise getDatasetCitationInOtherFormats( diff --git a/src/datasets/domain/useCases/LinkDataset.ts b/src/datasets/domain/useCases/LinkDataset.ts index 4e953b17..be7f732f 100644 --- a/src/datasets/domain/useCases/LinkDataset.ts +++ b/src/datasets/domain/useCases/LinkDataset.ts @@ -11,11 +11,11 @@ export class LinkDataset implements UseCase { /** * Creates a link between a Dataset and a Collection. * - * @param {number | string} [datasetId] - The dataset id (numeric) or persistent identifier string. - * @param {number | string} [collectionIdOrAlias] - The collection identifier (numeric id) or alias. + * @param {number} [datasetId] - The dataset id. + * @param {string} [collectionAlias] - The collection alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { - return await this.datasetsRepository.linkDataset(datasetId, collectionIdOrAlias) + async execute(datasetId: number, collectionAlias: string): Promise { + return await this.datasetsRepository.linkDataset(datasetId, collectionAlias) } } diff --git a/src/datasets/domain/useCases/UnlinkDataset.ts b/src/datasets/domain/useCases/UnlinkDataset.ts index 8b2142fb..d2d8eff5 100644 --- a/src/datasets/domain/useCases/UnlinkDataset.ts +++ b/src/datasets/domain/useCases/UnlinkDataset.ts @@ -11,11 +11,11 @@ export class UnlinkDataset implements UseCase { /** * Removes a link between a Dataset and a Collection. * - * @param {number | string} [datasetId] - The dataset id (numeric) or persistent identifier string. - * @param {number | string} [collectionIdOrAlias] - The collection identifier (numeric id) or alias. + * @param {number} [datasetId] - The dataset id. + * @param {string} [collectionAlias] - The collection alias. * @returns {Promise} - This method does not return anything upon successful completion. */ - async execute(datasetId: number | string, collectionIdOrAlias: number | string): Promise { - return await this.datasetsRepository.unlinkDataset(datasetId, collectionIdOrAlias) + async execute(datasetId: number, collectionAlias: string): Promise { + return await this.datasetsRepository.unlinkDataset(datasetId, collectionAlias) } } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 326516a8..1545a43d 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -329,32 +329,16 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - public async linkDataset( - datasetId: number | string, - collectionIdOrAlias: number | string - ): Promise { - const endpoint = this.buildApiEndpoint( - this.datasetsResourceName, - `link/${collectionIdOrAlias}`, - datasetId - ) - return this.doPut(endpoint, {}) + public async linkDataset(datasetId: number, collectionAlias: string): Promise { + return this.doPut(`/${this.datasetsResourceName}/${datasetId}/link/${collectionAlias}`, {}) .then(() => undefined) .catch((error) => { throw error }) } - public async unlinkDataset( - datasetId: number | string, - collectionIdOrAlias: number | string - ): Promise { - const endpoint = this.buildApiEndpoint( - this.datasetsResourceName, - `deleteLink/${collectionIdOrAlias}`, - datasetId - ) - return this.doDelete(endpoint) + public async unlinkDataset(datasetId: number, collectionAlias: string): Promise { + return this.doDelete(`/${this.datasetsResourceName}/${datasetId}/deleteLink/${collectionAlias}`) .then(() => undefined) .catch((error) => { throw error diff --git a/test/environment/.env b/test/environment/.env index 3a9a818d..e7b54bde 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=11710-find-dataverses-for-linking +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 116457e1..d1afd76d 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -804,101 +804,6 @@ describe('CollectionsRepository', () => { }) }) - describe('getCollectionsForLinking', () => { - const linkingParentCollection = 'collectionsRepositoryLinkingTestParentCollection' - const linkingTargetAlias = 'collectionsRepositoryLinkTarget' - - beforeAll(async () => { - await createCollectionViaApi(linkingParentCollection) - await createCollectionViaApi(linkingTargetAlias, linkingParentCollection) - }) - - afterAll(async () => { - await deleteCollectionViaApi(linkingTargetAlias) - await deleteCollectionViaApi(linkingParentCollection) - }) - - test('should list collections for linking for a given collection alias', async () => { - const results = await sut.getCollectionsForLinking( - 'collection', - linkingParentCollection, - 'Scientific', - false - ) - - expect(Array.isArray(results)).toBe(true) - // Should contain the newly created linking target collection among candidates - const found = results.find((c) => c.alias === linkingTargetAlias) - expect(found).toBeDefined() - expect(found?.id).toBeGreaterThan(0) - expect(found?.displayName).toBe('Scientific Research') - }) - - test('should list collections for linking for a given dataset persistentId', async () => { - // Create a temporary dataset to query linking candidates - const { persistentId, numericId } = await createDataset.execute( - TestConstants.TEST_NEW_DATASET_DTO, - linkingParentCollection - ) - - const results = await sut.getCollectionsForLinking( - 'dataset', - persistentId, - 'Scientific', - false - ) - - // Cleanup dataset (unpublished) - await deleteUnpublishedDatasetViaApi(numericId) - - expect(Array.isArray(results)).toBe(true) - const found = results.find((c) => c.alias === linkingTargetAlias) - expect(found).toBeDefined() - expect(found?.displayName).toBe('Scientific Research') - }) - - test('should return collections for unlinking when sending alreadyLinked param to true', async () => { - const collectionsForUnlinkingBefore = await sut.getCollectionsForLinking( - 'collection', - linkingParentCollection, - '', - true - ) - - // Link the test collection with the linking target collection - await sut.linkCollection(linkingParentCollection, linkingTargetAlias) - - const collectionsForUnlinkingAfter = await sut.getCollectionsForLinking( - 'collection', - linkingParentCollection, - '', - true - ) - - expect(collectionsForUnlinkingBefore.length).toBe(0) - expect(collectionsForUnlinkingAfter.length).toBeGreaterThan(0) - expect(collectionsForUnlinkingAfter[0].alias).toBe(linkingTargetAlias) - expect(collectionsForUnlinkingAfter[0].displayName).toBe('Scientific Research') - }) - - it('should return error when collection does not exist', async () => { - await expect( - sut.getCollectionsForLinking( - 'collection', - TestConstants.TEST_DUMMY_COLLECTION_ALIAS, - '', - false - ) - ).rejects.toThrow(ReadError) - }) - - it('should return error when dataset does not exist', async () => { - await expect( - sut.getCollectionsForLinking('dataset', TestConstants.TEST_DUMMY_PERSISTENT_ID, '', false) - ).rejects.toThrow(ReadError) - }) - }) - describe('getCollectionItems for published tabular file', () => { let testDatasetIds: CreatedDatasetIdentifiers const testTextFile4Name = 'test-file-4.tab' @@ -2093,18 +1998,16 @@ describe('CollectionsRepository', () => { const thirdCollectionAlias = 'getCollectionLinksThird' const fourthCollectionAlias = 'getCollectionLinksFourth' let childDatasetNumericId: number - let childDatasetPersistentId: string beforeAll(async () => { await createCollectionViaApi(firstCollectionAlias) await createCollectionViaApi(secondCollectionAlias) await createCollectionViaApi(thirdCollectionAlias) await createCollectionViaApi(fourthCollectionAlias) - const { numericId: createdId, persistentId: createdPid } = await createDataset.execute( + const { numericId: createdId } = await createDataset.execute( TestConstants.TEST_NEW_DATASET_DTO, fourthCollectionAlias ) childDatasetNumericId = createdId - childDatasetPersistentId = createdPid await sut.linkCollection(secondCollectionAlias, firstCollectionAlias) await sut.linkCollection(firstCollectionAlias, thirdCollectionAlias) await sut.linkCollection(firstCollectionAlias, fourthCollectionAlias) @@ -2133,7 +2036,6 @@ describe('CollectionsRepository', () => { expect(collectionLinks.linkedDatasets[0].title).toBe( 'Dataset created using the createDataset use case' ) - expect(collectionLinks.linkedDatasets[0].persistentId).toBe(childDatasetPersistentId) }) test('should return error when collection does not exist', async () => { diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 812fc43b..af669e7c 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1599,21 +1599,6 @@ describe('DatasetsRepository', () => { sut.linkDataset(testDatasetIds.numericId, 'nonExistentCollectionAlias') ).rejects.toThrow() }) - - test('should link a dataset to another collection using persistent id', async () => { - const persistentCollectionAlias = 'testLinkDatasetCollectionPersistent' - await createCollectionViaApi(persistentCollectionAlias) - - const actual = await sut.linkDataset(testDatasetIds.persistentId, persistentCollectionAlias) - - expect(actual).toBeUndefined() - - const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) - const aliases = linkedCollections.map((c) => c.alias) - expect(aliases).toContain(persistentCollectionAlias) - - await deleteCollectionViaApi(persistentCollectionAlias) - }) }) describe('unlinkDataset', () => { @@ -1659,27 +1644,6 @@ describe('DatasetsRepository', () => { sut.unlinkDataset(testDatasetIds.numericId, testCollectionAlias) ).rejects.toThrow() }) - - test('should unlink a dataset from a collection using persistent id', async () => { - const persistentCollectionAlias = 'testUnlinkDatasetCollectionPersistent' - await createCollectionViaApi(persistentCollectionAlias) - - await sut.linkDataset(testDatasetIds.persistentId, persistentCollectionAlias) - const linkedCollections = await sut.getDatasetLinkedCollections(testDatasetIds.numericId) - const aliases = linkedCollections.map((c) => c.alias) - expect(aliases).toContain(persistentCollectionAlias) - - const actual = await sut.unlinkDataset(testDatasetIds.persistentId, persistentCollectionAlias) - - expect(actual).toBeUndefined() - const updatedLinkedCollections = await sut.getDatasetLinkedCollections( - testDatasetIds.numericId - ) - const updatedAliases = updatedLinkedCollections.map((c) => c.alias) - expect(updatedAliases).not.toContain(persistentCollectionAlias) - - await deleteCollectionViaApi(persistentCollectionAlias) - }) }) describe('getDatasetLinkedCollections', () => { diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index d099acb1..950a9cdf 100644 --- a/test/unit/collections/CollectionsRepository.test.ts +++ b/test/unit/collections/CollectionsRepository.test.ts @@ -567,55 +567,6 @@ describe('CollectionsRepository', () => { }) }) - describe('getCollectionsForLinking', () => { - test('should call dataverse variant with numeric id and search term', async () => { - const payload = { - data: { - status: 'OK', - data: [ - { id: 1, alias: 'dv1', name: 'DV 1' }, - { id: 2, alias: 'dv2', name: 'DV 2' } - ] - } - } - jest.spyOn(axios, 'get').mockResolvedValue(payload) - - const actual = await sut.getCollectionsForLinking('collection', 99, 'abc', false) - const expectedEndpoint = `${TestConstants.TEST_API_URL}/dataverses/99/dataverse/linkingDataverses` - const expectedParams = new URLSearchParams({ searchTerm: 'abc' }) - const expectedConfig = { - params: expectedParams, - headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers - } - expect(axios.get).toHaveBeenCalledWith(expectedEndpoint, expectedConfig) - expect(actual).toEqual([ - { id: 1, alias: 'dv1', displayName: 'DV 1' }, - { id: 2, alias: 'dv2', displayName: 'DV 2' } - ]) - }) - - test('should call dataset variant using persistentId and map results', async () => { - const payload = { - data: { - status: 'OK', - data: [{ id: 3, alias: 'dv3', name: 'DV 3' }] - } - } - jest.spyOn(axios, 'get').mockResolvedValue(payload) - - const pid = 'doi:10.5072/FK2/J8SJZB' - const actual = await sut.getCollectionsForLinking('dataset', pid, '', false) - const expectedEndpoint = `${TestConstants.TEST_API_URL}/dataverses/:persistentId/dataset/linkingDataverses` - const expectedParams = new URLSearchParams({ persistentId: pid }) - const expectedConfig = { - params: expectedParams, - headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers - } - expect(axios.get).toHaveBeenCalledWith(expectedEndpoint, expectedConfig) - expect(actual).toEqual([{ id: 3, alias: 'dv3', displayName: 'DV 3' }]) - }) - }) - describe('deleteCollection', () => { const deleteTestCollectionAlias = 'deleteCollection-unit-test' const deleteTestCollectionId = 123 diff --git a/test/unit/collections/GetCollectionsForLinking.test.ts b/test/unit/collections/GetCollectionsForLinking.test.ts deleted file mode 100644 index 79511fe8..00000000 --- a/test/unit/collections/GetCollectionsForLinking.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' -import { GetCollectionsForLinking } from '../../../src/collections/domain/useCases/GetCollectionsForLinking' -import { CollectionSummary, ReadError } from '../../../src' - -const sample: CollectionSummary[] = [ - { id: 1, alias: 'col1', displayName: 'Collection 1' }, - { id: 2, alias: 'col2', displayName: 'Collection 2' } -] - -describe('GetCollectionsForLinking', () => { - test('should return collections for linking on success', async () => { - const repo: ICollectionsRepository = {} as ICollectionsRepository - repo.getCollectionsForLinking = jest.fn().mockResolvedValue(sample) - - const uc = new GetCollectionsForLinking(repo) - await expect(uc.execute('collection', 123, 'foo')).resolves.toEqual(sample) - expect(repo.getCollectionsForLinking).toHaveBeenCalledWith('collection', 123, 'foo', false) - }) - - test('should return error result on repository error', async () => { - const repo: ICollectionsRepository = {} as ICollectionsRepository - repo.getCollectionsForLinking = jest.fn().mockRejectedValue(new ReadError('x')) - - const uc = new GetCollectionsForLinking(repo) - await expect(uc.execute('dataset', 'doi:10.123/ABC')).rejects.toThrow(ReadError) - expect(repo.getCollectionsForLinking).toHaveBeenCalledWith( - 'dataset', - 'doi:10.123/ABC', - '', - false - ) - }) -}) From c53ffd7f84bdb2d9c00bc289d4dbce3e2ff0db4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 29 Sep 2025 08:58:06 -0300 Subject: [PATCH 120/123] chore: fixes with npm run audit fix --- package-lock.json | 1085 +++++++++++++++++++++++++++------------------ package.json | 4 +- 2 files changed, 646 insertions(+), 443 deletions(-) diff --git a/package-lock.json b/package-lock.json index fed98650..dd37bbf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@types/node": "^18.15.11", "@types/turndown": "^5.0.1", - "axios": "^1.7.2", + "axios": "^1.12.2", "turndown": "^7.1.2", "typescript": "^4.9.5" }, @@ -50,12 +50,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -107,24 +110,27 @@ "dev": true }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", - "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.21.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -159,10 +165,11 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -182,27 +189,12 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -272,19 +264,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -299,100 +293,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", - "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.28.4" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", - "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -578,58 +500,48 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", - "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.5", - "@babel/types": "^7.21.5", - "debug": "^4.1.0", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", - "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.21.5", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -639,7 +551,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", @@ -712,10 +625,63 @@ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", + "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -1232,17 +1198,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1254,36 +1217,34 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1330,6 +1291,80 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1433,16 +1468,18 @@ "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "node_modules/@types/dockerode": { - "version": "3.3.31", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.31.tgz", - "integrity": "sha512-42R9eoVqJDSvVspV89g7RwRqfNExgievLNWoHkg7NoWIqAmavIbgQBb4oc0qRtHkxE+I3Xxvqv7qVXFABKPBTg==", + "version": "3.3.44", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.44.tgz", + "integrity": "sha512-fUpIHlsbYpxAJb285xx3vp7q5wf5mjqSn3cYwl/MhiM+DB99OdO5sOCPlO0PjO+TyOtphPs7tMVLU/RtOo/JjA==", "dev": true, + "license": "MIT", "dependencies": { "@types/docker-modem": "*", "@types/node": "*", @@ -1533,10 +1570,11 @@ "dev": true }, "node_modules/@types/ssh2": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.0.tgz", - "integrity": "sha512-YcT8jP5F8NzWeevWvcyrrLB3zcneVjzYY9ZDSMAMboI+2zR1qYWFhwsyOFVzT7Jorn67vqxC0FRiw8YyG9P1ww==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^18.11.18" } @@ -2267,10 +2305,11 @@ } }, "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2390,17 +2429,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/archiver/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2554,12 +2582,13 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2667,49 +2696,92 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", "dev": true, - "optional": true + "license": "Apache-2.0" }, "node_modules/bare-fs": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", - "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.5.tgz", + "integrity": "sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^2.0.0", - "bare-stream": "^2.0.0" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, "node_modules/bare-os": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", - "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", "dev": true, - "optional": true + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } }, "node_modules/bare-path": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", - "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-os": "^2.1.0" + "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", - "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "streamx": "^2.18.0" + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz", + "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" } }, "node_modules/base64-js": { @@ -2746,6 +2818,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -2753,22 +2826,24 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2842,6 +2917,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -2899,6 +2975,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2966,7 +3055,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/ci-info": { "version": "3.8.0", @@ -3168,15 +3258,15 @@ "dev": true }, "node_modules/cpu-features": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.8.tgz", - "integrity": "sha512-BbHBvtYhUhksqTjr6bhNOjGgMnhwhGTQmOoZGD+K7BCaQDCuZl/Ve1ZxUSMRwVC4D/rkCPQ2MAIeYzrWyK7eEg==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { "buildcheck": "~0.0.6", - "nan": "^2.17.0" + "nan": "^2.19.0" }, "engines": { "node": ">=10.0.0" @@ -3254,10 +3344,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3432,44 +3523,68 @@ } }, "node_modules/docker-modem": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz", - "integrity": "sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", - "ssh2": "^1.11.0" + "ssh2": "^1.15.0" }, "engines": { "node": ">= 8.0" } }, "node_modules/dockerode": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.5.tgz", - "integrity": "sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.8.tgz", + "integrity": "sha512-HdPBprWmwfHMHi12AVIFDhXIqIS+EpiOVkZaAZxgML4xf5McqEZjJZtahTPkLDxWOt84ApfWPAH9EoQwOiaAIQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@balena/dockerignore": "^1.0.2", - "docker-modem": "^3.0.0", - "tar-fs": "~2.0.1" + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "~2.1.3", + "uuid": "^10.0.0" }, "engines": { "node": ">= 8.0" } }, "node_modules/dockerode/node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.0.0" + "tar-stream": "^2.1.4" + } + }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" } }, "node_modules/doctrine": { @@ -3502,6 +3617,20 @@ "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3533,10 +3662,11 @@ "dev": true }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -3615,24 +3745,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, "engines": { "node": ">= 0.4" } }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3673,15 +3824,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/escodegen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", @@ -4178,6 +4320,16 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4303,10 +4455,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4406,12 +4559,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4422,7 +4578,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fs.realpath": { "version": "1.0.0", @@ -4448,7 +4605,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4499,16 +4655,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4527,17 +4688,31 @@ } }, "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4649,12 +4824,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4727,10 +4902,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4742,7 +4917,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -4754,10 +4928,10 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -5110,6 +5284,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -5279,10 +5454,11 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -5942,7 +6118,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -6002,15 +6179,16 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-parse-even-better-errors": { @@ -6143,6 +6321,13 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6155,17 +6340,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } + "license": "Apache-2.0" }, "node_modules/make-dir": { "version": "3.1.0", @@ -6183,10 +6363,11 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -6206,6 +6387,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6222,12 +6412,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -6308,7 +6499,8 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ms": { "version": "2.1.2", @@ -6317,10 +6509,11 @@ "dev": true }, "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "dev": true, + "license": "MIT", "optional": true }, "node_modules/natural-compare": { @@ -6629,10 +6822,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6908,6 +7102,31 @@ "url": "https://github.com/steveukx/properties?sponsor=1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6929,10 +7148,11 @@ } }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -6989,12 +7209,6 @@ } ] }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -7006,6 +7220,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -7025,10 +7240,11 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7282,13 +7498,11 @@ } }, "node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -7421,7 +7635,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/sprintf-js": { "version": "1.0.3", @@ -7450,9 +7665,9 @@ } }, "node_modules/ssh2": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz", - "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -7463,8 +7678,8 @@ "node": ">=10.16.0" }, "optionalDependencies": { - "cpu-features": "~0.0.8", - "nan": "^2.17.0" + "cpu-features": "~0.0.10", + "nan": "^2.23.0" } }, "node_modules/stack-utils": { @@ -7489,17 +7704,15 @@ } }, "node_modules/streamx": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", - "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "dev": true, + "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -7684,46 +7897,32 @@ "dev": true }, "node_modules/tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, - "node_modules/tar-fs/node_modules/tar-stream": { + "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, + "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7739,26 +7938,27 @@ } }, "node_modules/testcontainers": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.11.0.tgz", - "integrity": "sha512-TYgpR+MjZSuX7kSUxTa0f/CsN6eErbMFrAFumW08IvOnU8b+EoRzpzEu7mF0d29M1ItnHfHPUP44HYiE4yP3Zg==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.28.0.tgz", + "integrity": "sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg==", "dev": true, + "license": "MIT", "dependencies": { "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.29", + "@types/dockerode": "^3.3.35", "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.5", "docker-compose": "^0.24.8", - "dockerode": "^3.3.5", - "get-port": "^5.1.1", + "dockerode": "^4.0.5", + "get-port": "^7.1.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.6", + "tar-fs": "^3.0.7", "tmp": "^0.2.3", - "undici": "^5.28.4" + "undici": "^5.29.0" } }, "node_modules/text-decoder": { @@ -7777,10 +7977,11 @@ "dev": true }, "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.14" } @@ -7791,20 +7992,12 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -8134,10 +8327,11 @@ } }, "node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "dev": true, + "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -8222,6 +8416,20 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8384,10 +8592,11 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8491,12 +8700,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yaml": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", diff --git a/package.json b/package.json index bcfedaf3..a4268939 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,8 @@ "dependencies": { "@types/node": "^18.15.11", "@types/turndown": "^5.0.1", - "axios": "^1.7.2", + "axios": "^1.12.2", "turndown": "^7.1.2", "typescript": "^4.9.5" } -} +} \ No newline at end of file From 84834b7167c0497a6070c7e81987ba138b579904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 29 Sep 2025 09:04:59 -0300 Subject: [PATCH 121/123] chore: replace pre-commit with husky --- .husky/pre-commit | 4 ++ package-lock.json | 167 +++++----------------------------------------- package.json | 12 +--- 3 files changed, 24 insertions(+), 159 deletions(-) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..ed1489a7 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +npm run format +npm run typecheck +npm run lint:fix +git add . diff --git a/package-lock.json b/package-lock.json index dd37bbf4..9f0c10dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,9 @@ "eslint-plugin-prettier": "4.2.1", "eslint-plugin-simple-import-sort": "10.0.0", "eslint-plugin-unused-imports": "2.0.0", + "husky": "9.1.7", "jest": "^29.4.3", "jest-environment-jsdom": "29.7.0", - "pre-commit": "1.2.2", "prettier": "2.8.4", "testcontainers": "^10.11.0", "ts-jest": "^29.0.5", @@ -3200,51 +3200,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4993,6 +4948,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6661,15 +6632,6 @@ "node": ">= 0.8.0" } }, - "node_modules/os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6913,78 +6875,6 @@ "node": ">=8" } }, - "node_modules/pre-commit": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz", - "integrity": "sha512-qokTiqxD6GjODy5ETAIgzsRgnBWWQHQH2ghy86PU7mIn/wuWeTwF3otyNQZxWBwVn8XNr8Tdzj/QfUXpH+gRZA==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "cross-spawn": "^5.0.1", - "spawn-sync": "^1.0.15", - "which": "1.2.x" - } - }, - "node_modules/pre-commit/node_modules/cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", - "dev": true, - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/pre-commit/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/pre-commit/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pre-commit/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pre-commit/node_modules/which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha512-16uPglFkRPzgiUXYMi1Jf8Z5EzN1iB4V0ZtMXcHZnwsBtQhhHeCqoWw7tsUY42hJGNDWtUsVLTjakIa5BgAxCw==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/pre-commit/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7132,12 +7022,6 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true - }, "node_modules/psl": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.13.0.tgz", @@ -7620,17 +7504,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -8293,12 +8166,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", diff --git a/package.json b/package.json index a4268939..91fb04cb 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,8 @@ "lint:prettier": "prettier --check '**/*.(yml|json|md)'", "format": "prettier --write './**/*.{js,ts,md,json,yml,md}' --config ./.prettierrc", "typecheck": "tsc --noEmit", - "git:add": "git add ." + "prepare": "husky" }, - "pre-commit": [ - "format", - "typecheck", - "lint:fix", - "git:add" - ], "repository": { "type": "git", "url": "git+https://github.com/IQSS/dataverse-client-javascript.git" @@ -54,9 +48,9 @@ "eslint-plugin-prettier": "4.2.1", "eslint-plugin-simple-import-sort": "10.0.0", "eslint-plugin-unused-imports": "2.0.0", + "husky": "9.1.7", "jest": "^29.4.3", "jest-environment-jsdom": "29.7.0", - "pre-commit": "1.2.2", "prettier": "2.8.4", "testcontainers": "^10.11.0", "ts-jest": "^29.0.5", @@ -69,4 +63,4 @@ "turndown": "^7.1.2", "typescript": "^4.9.5" } -} \ No newline at end of file +} From d6a09a4566900e4998d5cf8a4015ef3e90fcef33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 29 Sep 2025 09:08:23 -0300 Subject: [PATCH 122/123] docs: add to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32bb51b6..2cd43604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Fixed +- Dependencies updated to address security vulnerabilities found by `npm audit`. + ### Removed --- From 5febe7cb5d7bde0d56b27063bfdefa94ca15b50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 29 Sep 2025 15:37:33 -0300 Subject: [PATCH 123/123] release v2.1.0 --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++++++++---- docs/making-releases.md | 7 +++--- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd43604..2ea3e0f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,52 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Fixed -- Dependencies updated to address security vulnerabilities found by `npm audit`. - ### Removed +[Unreleased]: https://github.com/IQSS/dataverse-client-javascript/compare/v2.1.0...develop + --- -## [v2.0.0] -- 2025-07-04 +## [v2.1.0] -- 2025-09-29 + +### Added + +- CHANGELOG.md file to track changes in a standard way. + +- New property isAdvancedSearchFieldType returned by API in GetCollectionMetadataBlocks and GetMetadataBlockByName use cases. + +- Use cases for Notifications: GetAllNotifications, DeleteNotification. + +- Use cases for Dataset Linking: LinkDataset, UnlinkDataset, GetDatasetLinkedCollections. + +- Use case: GetCitationInOtherFormats. + +- Use case: GetDatasetAvailableCategories. + +- Use cases for Collections Linking: LinkCollection, UnlinkCollection, GetCollectionLinks. + +- Use cases for External Tools: GetExternalTools, GetDatasetExternalToolResolved, GetFileExternalToolResolved. + +- Use case: GetDatasetTemplates. + +- Use case: GetAvailableStandardLicenses. + +- Use case: GetAvailableDatasetMetadataExportFormats. + +- Use cases for Dataset Types: GetDatasetAvailableDatasetTypes, GetDatasetAvailableDatasetType, AddDatasetType, LinkDatasetTypeWithMetadataBlocks, SetAvailableLicensesForDatasetType, DeleteDatasetType. + +### Changed + +- CreateDataset use case updated to allow non-default dataset types. + +- GetCollectionMetadataBlocks use case updated to support passing a dataset type. + +### Fixed + +- Integration tests in Roles Repository. + +- Incorrect Filter Queries split that caused value parts to be truncated. + +### Security -[Unreleased]: https://github.com/IQSS/dataverse-frontend/compare/v2.0.0...develop +- Dependencies updated to address vulnerabilities found by npm audit. diff --git a/docs/making-releases.md b/docs/making-releases.md index ce405014..0890f1b2 100644 --- a/docs/making-releases.md +++ b/docs/making-releases.md @@ -41,8 +41,6 @@ npm version 3.5.0 --no-git-tag-version This command will update the version in the `package.json` and `package-lock.json`. -If everything looks good, you can push the changes to the repository. - ## Update the changelog **Note**: Contributors should have already added their changes to the `[Unreleased]` section as part of their pull requests (see [CONTRIBUTING.md](../.github/CONTRIBUTING.md#changelog-guidelines) for details). @@ -86,8 +84,9 @@ Before releasing, ensure the changelog is properly prepared: ### Removed ``` -4. **Update the version links** at the bottom of the changelog files -5. **Commit the changelog updates** as part of the release preparation +4. **Commit the changelog updates** as part of the release preparation + +If everything looks good, you can push the changes to the repository. ## Merge "release branch" into "main" diff --git a/package-lock.json b/package-lock.json index 9f0c10dd..40941f67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@iqss/dataverse-client-javascript", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@iqss/dataverse-client-javascript", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 91fb04cb..541fc681 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iqss/dataverse-client-javascript", - "version": "2.0.0", + "version": "2.1.0", "description": "Dataverse API wrapper package for JavaScript/TypeScript-based applications", "main": "./dist/index.js", "types": "./dist/index.d.ts",