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 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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 ) }) })