From f9e7389fbe04d7342dabb1e176534f20cd5c39d4 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 24 Sep 2025 17:32:59 -0400 Subject: [PATCH 001/110] feat: use case set dataset license --- docs/useCases.md | 38 +++++ .../dtos/DatasetLicenseUpdateRequest.ts | 6 + .../repositories/IDatasetsRepository.ts | 5 + .../domain/useCases/UpdateDatasetLicense.ts | 23 +++ src/datasets/index.ts | 6 +- .../infra/repositories/DatasetsRepository.ts | 15 ++ .../datasets/UpdateDatasetLicense.test.ts | 72 +++++++++ .../datasets/DatasetsRepository.test.ts | 147 +++++++++++++++++- .../datasets/UpdateDatasetLicense.test.ts | 28 ++++ 9 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts create mode 100644 src/datasets/domain/useCases/UpdateDatasetLicense.ts create mode 100644 test/functional/datasets/UpdateDatasetLicense.test.ts create mode 100644 test/unit/datasets/UpdateDatasetLicense.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index ff537daa..4def0c57 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -44,6 +44,7 @@ The different use cases currently available in the package are classified below, - [Datasets write use cases](#datasets-write-use-cases) - [Create a Dataset](#create-a-dataset) - [Update a Dataset](#update-a-dataset) + - [Update a Dataset License](#update-a-dataset-license) - [Publish a Dataset](#publish-a-dataset) - [Deaccession a Dataset](#deaccession-a-dataset) - [Delete a Draft Dataset](#delete-a-draft-dataset) @@ -977,6 +978,43 @@ updateDataset.execute(datasetId, datasetDTO) _See [use case](../src/datasets/domain/useCases/UpdateDataset.ts) implementation_. +#### Update a Dataset License + +Updates the license of a dataset by applying it to the draft version. If no draft exists, a new one is automatically created by the API. Supports predefined licenses (by name) or custom terms of use and access. + +##### Example calls: + +```typescript +import { + updateDatasetLicense, + DatasetLicenseUpdateRequest +} from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 1 + +const predefinedPayload: DatasetLicenseUpdateRequest = { name: 'CC BY 4.0' } +await updateDatasetLicense.execute(datasetId, predefinedPayload) + +const customPayload: DatasetLicenseUpdateRequest = { + customTerms: { + termsOfUse: 'Your terms of use', + confidentialityDeclaration: 'Your confidentiality declaration', + specialPermissions: 'Your special permissions', + restrictions: 'Your restrictions', + citationRequirements: 'Your citation requirements', + depositorRequirements: 'Your depositor requirements', + conditions: 'Your conditions', + disclaimer: 'Your disclaimer' + } +} + +updateDatasetLicense.execute(datasetId, customPayload) +``` + +_See [use case](../src/datasets/domain/useCases/UpdateDatasetLicense.ts) implementation_. + The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. #### Publish a Dataset diff --git a/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts b/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts new file mode 100644 index 00000000..2102e26c --- /dev/null +++ b/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts @@ -0,0 +1,6 @@ +import { CustomTerms } from '../../domain/models/Dataset' + +export interface DatasetLicenseUpdateRequest { + name?: string + customTerms?: CustomTerms +} diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 3fe1c7ab..57c314b0 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -14,6 +14,7 @@ import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' import { DatasetTemplate } from '../models/DatasetTemplate' import { DatasetType } from '../models/DatasetType' +import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest' export interface IDatasetsRepository { getDataset( @@ -89,4 +90,8 @@ export interface IDatasetsRepository { licenses: string[] ): Promise deleteDatasetType(datasetTypeId: number): Promise + updateDatasetLicense( + datasetId: number | string, + payload: DatasetLicenseUpdateRequest + ): Promise } diff --git a/src/datasets/domain/useCases/UpdateDatasetLicense.ts b/src/datasets/domain/useCases/UpdateDatasetLicense.ts new file mode 100644 index 00000000..3886ed2b --- /dev/null +++ b/src/datasets/domain/useCases/UpdateDatasetLicense.ts @@ -0,0 +1,23 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest' + +export class UpdateDatasetLicense implements UseCase { + private readonly datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Updates the license of a dataset by applying it to the draft version. If no draft exists, a new one is created by the API. + * Supports either predefined license by name or custom terms of use and access. + * + * @param {number | string} datasetId - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {DatasetLicenseUpdateRequest} payload - The payload containing the license name or custom terms of use and access. + * @returns {Promise} - This method does not return anything upon successful completion. + */ + async execute(datasetId: number | string, payload: DatasetLicenseUpdateRequest): Promise { + return this.datasetsRepository.updateDatasetLicense(datasetId, payload) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 6b93a7cd..2acedddf 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -32,6 +32,7 @@ import { SetAvailableLicensesForDatasetType } from './domain/useCases/SetAvailab import { DeleteDatasetType } from './domain/useCases/DeleteDatasetType' import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' +import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense' const datasetsRepository = new DatasetsRepository() @@ -80,6 +81,7 @@ const setAvailableLicensesForDatasetType = new SetAvailableLicensesForDatasetTyp const deleteDatasetType = new DeleteDatasetType(datasetsRepository) const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) const getDatasetTemplates = new GetDatasetTemplates(datasetsRepository) +const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository) export { getDataset, @@ -109,7 +111,8 @@ export { addDatasetType, linkDatasetTypeWithMetadataBlocks, setAvailableLicensesForDatasetType, - deleteDatasetType + deleteDatasetType, + updateDatasetLicense } export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion' export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions' @@ -136,6 +139,7 @@ export { DatasetMetadataBlockValuesDTO, DatasetMetadataChildFieldValueDTO } from './domain/dtos/DatasetDTO' +export { DatasetLicenseUpdateRequest } from './domain/dtos/DatasetLicenseUpdateRequest' export { DatasetDeaccessionDTO } from './domain/dtos/DatasetDeaccessionDTO' export { CreatedDatasetIdentifiers } from './domain/models/CreatedDatasetIdentifiers' export { VersionUpdateType } from './domain/models/Dataset' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 99b380df..cc89136a 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -29,6 +29,7 @@ import { DatasetTemplate } from '../../domain/models/DatasetTemplate' import { DatasetTemplatePayload } from './transformers/DatasetTemplatePayload' import { transformDatasetTemplatePayloadToDatasetTemplate } from './transformers/datasetTemplateTransformers' import { DatasetType } from '../../domain/models/DatasetType' +import { DatasetLicenseUpdateRequest } from '../../domain/dtos/DatasetLicenseUpdateRequest' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -448,4 +449,18 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async updateDatasetLicense( + datasetId: number | string, + payload: DatasetLicenseUpdateRequest + ): Promise { + return this.doPut( + this.buildApiEndpoint(this.datasetsResourceName, 'license', datasetId), + payload + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/test/functional/datasets/UpdateDatasetLicense.test.ts b/test/functional/datasets/UpdateDatasetLicense.test.ts new file mode 100644 index 00000000..4f1683a8 --- /dev/null +++ b/test/functional/datasets/UpdateDatasetLicense.test.ts @@ -0,0 +1,72 @@ +import { + ApiConfig, + createDataset, + getDataset, + publishDataset, + updateDatasetLicense, + DatasetLicenseUpdateRequest +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { + waitForNoLocks, + deleteUnpublishedDatasetViaApi, + deletePublishedDatasetViaApi +} from '../../testHelpers/datasets/datasetHelper' +import { DatasetNotNumberedVersion, VersionUpdateType } from '../../../src/datasets' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should update the license of a draft dataset (predefined by name)', async () => { + const created = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + const payload: DatasetLicenseUpdateRequest = { name: 'CC BY 4.0' } + const response = await updateDatasetLicense.execute(created.numericId, payload) + + expect(response).toBeUndefined() + + const after = await getDataset.execute( + created.numericId, + DatasetNotNumberedVersion.DRAFT, + false, + false + ) + expect(after.license?.name).toBe('CC BY 4.0') + + await deleteUnpublishedDatasetViaApi(created.numericId) + }) + + test('should update the license of a published dataset (custom terms creates draft)', async () => { + const created = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await publishDataset.execute(created.numericId, VersionUpdateType.MAJOR) + await waitForNoLocks(created.numericId, 10) + + const payload: DatasetLicenseUpdateRequest = { + customTerms: { + termsOfUse: 'Updated terms of use (functional test)' + } + } + const response = await updateDatasetLicense.execute(created.numericId, payload) + + expect(response).toBeUndefined() + + const draft = await getDataset.execute( + created.numericId, + DatasetNotNumberedVersion.DRAFT, + false, + false + ) + expect(draft.license).toBeUndefined() + expect(draft.termsOfUse.customTerms?.termsOfUse).toBe('Updated terms of use (functional test)') + + await deletePublishedDatasetViaApi(created.persistentId) + }) +}) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 5374b5f8..799aa0f1 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -27,7 +27,8 @@ import { addDatasetType, deleteDatasetType, linkDatasetTypeWithMetadataBlocks, - setAvailableLicensesForDatasetType + setAvailableLicensesForDatasetType, + DatasetLicenseUpdateRequest } from '../../../src/datasets' import { ApiConfig, WriteError } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -1800,4 +1801,148 @@ describe('DatasetsRepository', () => { }) }) }) + + describe('updateDatasetLicense', () => { + test('should update the license of a published dataset', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + + const DatasetBefore = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + expect(DatasetBefore.license?.name).toBe('CC0 1.0') // default license + + const payload: DatasetLicenseUpdateRequest = { name: 'CC BY 4.0' } + await sut.updateDatasetLicense(testDatasetIds.numericId, payload) + + const DatasetAfter = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + expect(DatasetAfter.license?.name).toBe('CC BY 4.0') + }) + + test('should update the license of a draft dataset', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + const DatasetBefore = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + expect(DatasetBefore.license?.name).toBe('CC0 1.0') // default license + const predefined: DatasetLicenseUpdateRequest = { name: 'CC BY 4.0' } + await sut.updateDatasetLicense(testDatasetIds.numericId, predefined) + + const datasetAfter = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.DRAFT, + false, + false + ) + + expect(datasetAfter.license?.name).toBe('CC BY 4.0') + + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + }) + + test('should set custom terms of use and access on the draft version', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + const custom: DatasetLicenseUpdateRequest = { + customTerms: { + termsOfUse: 'Your terms of use', + confidentialityDeclaration: 'Your confidentiality declaration', + specialPermissions: 'Your special permissions', + restrictions: 'Your restrictions', + citationRequirements: 'Your citation requirements', + depositorRequirements: 'Your depositor requirements', + conditions: 'Your conditions', + disclaimer: 'Your disclaimer' + } + } + const actual = await sut.updateDatasetLicense(testDatasetIds.numericId, custom) + + expect(actual).toBeUndefined() + + const datasetAfter = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.DRAFT, + false, + false + ) + + expect(datasetAfter.license).toBeUndefined() + expect(datasetAfter.termsOfUse.customTerms?.termsOfUse).toBe('Your terms of use') + + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + }) + + test('should set custom terms of use and access on the published version', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + + const custom: DatasetLicenseUpdateRequest = { + customTerms: { + termsOfUse: 'Your terms of use', + confidentialityDeclaration: 'Your confidentiality declaration', + specialPermissions: 'Your special permissions', + restrictions: 'Your restrictions', + citationRequirements: 'Your citation requirements', + depositorRequirements: 'Your depositor requirements', + conditions: 'Your conditions', + disclaimer: 'Your disclaimer' + } + } + const actual = await sut.updateDatasetLicense(testDatasetIds.numericId, custom) + + expect(actual).toBeUndefined() + + const datasetAfter = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.DRAFT, + false, + false + ) + + expect(datasetAfter.license).toBeUndefined() + expect(datasetAfter.termsOfUse.customTerms?.termsOfUse).toBe('Your terms of use') + + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + }) + + test('should return error when dataset does not exist', async () => { + const expectedError = new WriteError( + `[404] Dataset with ID ${nonExistentTestDatasetId} not found.` + ) + + await expect( + sut.updateDatasetLicense(nonExistentTestDatasetId, { name: 'CC BY 4.0' }) + ).rejects.toThrow(expectedError) + }) + + test('should accept persistent id when updating license on draft dataset', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await sut.updateDatasetLicense(testDatasetIds.persistentId, { name: 'CC BY 4.0' }) + + const draftAfter = await sut.getDataset( + testDatasetIds.persistentId, + DatasetNotNumberedVersion.DRAFT, + false, + false + ) + expect(draftAfter.license?.name).toBe('CC BY 4.0') + + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + }) + }) }) diff --git a/test/unit/datasets/UpdateDatasetLicense.test.ts b/test/unit/datasets/UpdateDatasetLicense.test.ts new file mode 100644 index 00000000..10e8348b --- /dev/null +++ b/test/unit/datasets/UpdateDatasetLicense.test.ts @@ -0,0 +1,28 @@ +import { UpdateDatasetLicense } from '../../../src/datasets/domain/useCases/UpdateDatasetLicense' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { DatasetLicenseUpdateRequest } from '../../../src/datasets/domain/dtos/DatasetLicenseUpdateRequest' +import { WriteError } from '../../../src' + +describe('execute', () => { + test('should return undefined when success', async () => { + const repo: IDatasetsRepository = {} as IDatasetsRepository + repo.updateDatasetLicense = jest.fn().mockResolvedValue(undefined) + const sut = new UpdateDatasetLicense(repo) + + const payload: DatasetLicenseUpdateRequest = { name: 'CC BY 4.0' } + const actual = await sut.execute(1, payload) + + expect(repo.updateDatasetLicense).toHaveBeenCalledWith(1, payload) + expect(actual).toBeUndefined() + }) + + test('should throw WriteError when repository raises an error', async () => { + const repo: IDatasetsRepository = {} as IDatasetsRepository + repo.updateDatasetLicense = jest.fn().mockRejectedValue(new WriteError()) + const sut = new UpdateDatasetLicense(repo) + + const payload: DatasetLicenseUpdateRequest = { name: 'CC BY 4.0' } + await expect(sut.execute(999, payload)).rejects.toThrow(WriteError) + expect(repo.updateDatasetLicense).toHaveBeenCalledWith(999, payload) + }) +}) From 04487146acc5f16cc5221e6bad05f47fcb96a976 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 24 Sep 2025 17:46:07 -0400 Subject: [PATCH 002/110] add changelog --- CHANGELOG.md | 2 ++ test/integration/datasets/DatasetsRepository.test.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32bb51b6..b9c7890f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added +- Datasets: Added `updateDatasetLicense` use case and repository method to support Dataverse endpoint `PUT /datasets/{id}/license`, for updating dataset license or custom terms + ### Changed ### Fixed diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 799aa0f1..94976790 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1944,5 +1944,15 @@ describe('DatasetsRepository', () => { await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) }) + + test('should return error when payload is empty', async () => { + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await expect( + sut.updateDatasetLicense(testDatasetIds.numericId, {} as unknown as never) + ).rejects.toBeInstanceOf(WriteError) + + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + }) }) }) From d74bab3055c4e5bd99db2ebba6892d60b99d8453 Mon Sep 17 00:00:00 2001 From: German Gonzalo Saracca Date: Thu, 25 Sep 2025 13:31:44 +0200 Subject: [PATCH 003/110] Revert "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, 411 insertions(+), 20 deletions(-) create mode 100644 src/collections/domain/useCases/GetCollectionsForLinking.ts create mode 100644 test/unit/collections/GetCollectionsForLinking.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 94b36731..cb95e01f 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) @@ -336,6 +337,69 @@ 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 820a1356..bc8960c8 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,10 @@ 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 new file mode 100644 index 00000000..e01e156e --- /dev/null +++ b/src/collections/domain/useCases/GetCollectionsForLinking.ts @@ -0,0 +1,34 @@ +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 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..e0e459b0 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,46 @@ 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 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/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index e78816c4..621a5a06 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, 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 1545a43d..326516a8 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -329,16 +329,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/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 diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index d1afd76d..116457e1 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -804,6 +804,101 @@ 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' @@ -1998,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) @@ -2036,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 () => { diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index af669e7c..812fc43b 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1599,6 +1599,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', () => { @@ -1644,6 +1659,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', () => { diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index 950a9cdf..d099acb1 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', 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 new file mode 100644 index 00000000..79511fe8 --- /dev/null +++ b/test/unit/collections/GetCollectionsForLinking.test.ts @@ -0,0 +1,33 @@ +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 0c54f47db69eeae9959cb9878e0018b11928fc46 Mon Sep 17 00:00:00 2001 From: Cheng Shi <91049239+ChengShi-1@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:13:56 -0400 Subject: [PATCH 004/110] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts | 2 +- test/integration/datasets/DatasetsRepository.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts b/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts index 2102e26c..db20307e 100644 --- a/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts +++ b/src/datasets/domain/dtos/DatasetLicenseUpdateRequest.ts @@ -1,4 +1,4 @@ -import { CustomTerms } from '../../domain/models/Dataset' +import { CustomTerms } from '../models/Dataset' export interface DatasetLicenseUpdateRequest { name?: string diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 94976790..fbf00568 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1882,7 +1882,7 @@ describe('DatasetsRepository', () => { expect(datasetAfter.license).toBeUndefined() expect(datasetAfter.termsOfUse.customTerms?.termsOfUse).toBe('Your terms of use') - await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) }) test('should set custom terms of use and access on the published version', async () => { From 14d7990b801de8cff06903cb2130a2a720a3d8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 25 Sep 2025 15:12:50 -0300 Subject: [PATCH 005/110] fix: change lastUpdateTime type to string in DatasetVersionInfo and related transformers --- src/datasets/domain/models/Dataset.ts | 2 +- .../repositories/transformers/datasetPreviewsTransformers.ts | 4 ++-- .../infra/repositories/transformers/datasetTransformers.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/datasets/domain/models/Dataset.ts b/src/datasets/domain/models/Dataset.ts index e858de9e..ac4c4623 100644 --- a/src/datasets/domain/models/Dataset.ts +++ b/src/datasets/domain/models/Dataset.ts @@ -22,7 +22,7 @@ export interface DatasetVersionInfo { minorNumber: number state: DatasetVersionState createTime: Date - lastUpdateTime: Date + lastUpdateTime: string releaseTime?: Date deaccessionNote?: string } diff --git a/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts b/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts index 8b60828d..3ae56800 100644 --- a/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetPreviewsTransformers.ts @@ -39,7 +39,7 @@ export const transformDatasetPreviewPayloadToDatasetPreview = ( minorNumber: datasetPreviewPayload.minorVersion, state: datasetPreviewPayload.versionState as DatasetVersionState, createTime: new Date(datasetPreviewPayload.createdAt), - lastUpdateTime: new Date(datasetPreviewPayload.updatedAt), + lastUpdateTime: datasetPreviewPayload.updatedAt, ...(datasetPreviewPayload.published_at && { releaseTime: new Date(datasetPreviewPayload.published_at) }) @@ -72,7 +72,7 @@ export const transformMyDataDatasetPreviewPayloadToDatasetPreview = ( minorNumber: datasetPreviewPayload.minorVersion, state: datasetPreviewPayload.versionState as DatasetVersionState, createTime: new Date(datasetPreviewPayload.createdAt), - lastUpdateTime: new Date(datasetPreviewPayload.updatedAt), + lastUpdateTime: datasetPreviewPayload.updatedAt, ...(datasetPreviewPayload.published_at && { releaseTime: new Date(datasetPreviewPayload.published_at) }) diff --git a/src/datasets/infra/repositories/transformers/datasetTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTransformers.ts index bbb4c9fc..7f186fb2 100644 --- a/src/datasets/infra/repositories/transformers/datasetTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTransformers.ts @@ -235,7 +235,7 @@ export const transformVersionPayloadToDataset = ( minorNumber: versionPayload.versionMinorNumber, state: versionPayload.versionState as DatasetVersionState, createTime: new Date(versionPayload.createTime), - lastUpdateTime: new Date(versionPayload.lastUpdateTime), + lastUpdateTime: versionPayload.lastUpdateTime, releaseTime: new Date(versionPayload.releaseTime), deaccessionNote: versionPayload.deaccessionNote }, From 85e1b876bf1b659f5daf6135c8d0bfd94b736a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 25 Sep 2025 15:20:41 -0300 Subject: [PATCH 006/110] refactor: update internalVersionNumber to sourceLastUpdateTime in dataset update methods --- src/datasets/domain/repositories/IDatasetsRepository.ts | 2 +- src/datasets/domain/useCases/UpdateDataset.ts | 6 +++--- src/datasets/infra/repositories/DatasetsRepository.ts | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index e78816c4..10843afa 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -54,7 +54,7 @@ export interface IDatasetsRepository { datasetId: number | string, dataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], - internalVersionNumber?: number + sourceLastUpdateTime?: string ): Promise deaccessionDataset( datasetId: number | string, diff --git a/src/datasets/domain/useCases/UpdateDataset.ts b/src/datasets/domain/useCases/UpdateDataset.ts index ed90f4d1..d87764f2 100644 --- a/src/datasets/domain/useCases/UpdateDataset.ts +++ b/src/datasets/domain/useCases/UpdateDataset.ts @@ -18,7 +18,7 @@ export class UpdateDataset extends DatasetWriteUseCase { * * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {DatasetDTO} [updatedDataset] - DatasetDTO object including the updated dataset metadata field values for each metadata block. - * @param {number} [internalVersionNumber] - The internal version number of the dataset. If another user updates the dataset version metadata before you send the update request, data inconsistencies may occur. To prevent this, you can use the optional internalVersionNumber parameter. This parameter must include the internal version number corresponding to the dataset version being updated. Note that internal version numbers increase sequentially with each version update. + * @param {string} [sourceLastUpdateTime] - The lastUpdateTime value from the dataset. Provide it to ensure optimistic concurrency: if the dataset was updated since you retrieved it, the backend can reject the update to prevent overwriting newer changes. * @returns {Promise} - This method does not return anything upon successful completion. * @throws {ResourceValidationError} - If there are validation errors related to the provided information. * @throws {ReadError} - If there are errors while reading data. @@ -27,7 +27,7 @@ export class UpdateDataset extends DatasetWriteUseCase { async execute( datasetId: number | string, updatedDataset: DatasetDTO, - internalVersionNumber?: number + sourceLastUpdateTime?: string ): Promise { const metadataBlocks = await this.getNewDatasetMetadataBlocks(updatedDataset) this.getNewDatasetValidator().validate(updatedDataset, metadataBlocks) @@ -35,7 +35,7 @@ export class UpdateDataset extends DatasetWriteUseCase { datasetId, updatedDataset, metadataBlocks, - internalVersionNumber + sourceLastUpdateTime ) } } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 1545a43d..17a77dce 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -252,16 +252,14 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi datasetId: string | number, dataset: DatasetDTO, datasetMetadataBlocks: MetadataBlock[], - internalVersionNumber?: number + sourceLastUpdateTime?: string ): Promise { return this.doPut( this.buildApiEndpoint(this.datasetsResourceName, `editMetadata`, datasetId), transformDatasetModelToUpdateDatasetRequestPayload(dataset, datasetMetadataBlocks), { replace: true, - ...(typeof internalVersionNumber === 'number' && { - sourceInternalVersionNumber: internalVersionNumber - }) + ...(sourceLastUpdateTime && { sourceLastUpdateTime }) } ) .then(() => undefined) From 866ce06d15c7ca8962e2f2bb7984c0fb74f1b534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 25 Sep 2025 16:35:47 -0300 Subject: [PATCH 007/110] test: update integration test --- .../datasets/DatasetsRepository.test.ts | 33 +++++++------------ test/testHelpers/datasets/datasetHelper.ts | 2 +- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index af669e7c..71b444bf 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -235,7 +235,6 @@ describe('DatasetsRepository', () => { false ) expect(actual.id).toBe(testDatasetIds.numericId) - expect(actual.internalVersionNumber).toBe(1) }) test('should return dataset when it is deaccessioned and includeDeaccessioned param is set', async () => { @@ -1132,8 +1131,8 @@ describe('DatasetsRepository', () => { } ]) }) - // 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 () => { + + test('should throw error if sending an outdated lastUpdateTime', async () => { const testDataset = { metadataBlockValues: [ { @@ -1184,35 +1183,27 @@ describe('DatasetsRepository', () => { false, false ) - const actualCreatedDatasetInternalVersionNumber = actualCreatedDataset.internalVersionNumber - - expect(actualCreatedDataset.internalVersionNumber).toBe(1) + const firstLastUpdateTime = actualCreatedDataset.versionInfo.lastUpdateTime - // Now update the dataset and then update again with the same internal version number + // Now update the dataset and then update again with the same source last update time const updatedDsDescription = 'This is the updated description of the dataset.' testDataset.metadataBlockValues[0].fields.dsDescription[0].dsDescriptionValue = updatedDsDescription - // First update sending the correct internal version number + // Wait for 2 seconds + await new Promise((resolve) => setTimeout(resolve, 2000)) + + // First update sending the correct lastUpdateTime await sut.updateDataset( createdDataset.numericId, testDataset, [citationMetadataBlock], - actualCreatedDatasetInternalVersionNumber - ) - - const afterFirstUpdateDataset = await sut.getDataset( - createdDataset.numericId, - DatasetNotNumberedVersion.LATEST, - false, - false + firstLastUpdateTime ) - expect(afterFirstUpdateDataset.internalVersionNumber).toBe(2) - - //Now try to update again with the previous internal version number + //Now try to update again with the previous lastUpdateTime const expectedError = new WriteError( - `[400] Dataset internal version number ${actualCreatedDatasetInternalVersionNumber} is outdated` + `[400] Internal version timestamp ${firstLastUpdateTime} is outdated` ) await expect( @@ -1220,7 +1211,7 @@ describe('DatasetsRepository', () => { createdDataset.numericId, testDataset, [citationMetadataBlock], - actualCreatedDatasetInternalVersionNumber + firstLastUpdateTime ) ).rejects.toThrow(expectedError) }) diff --git a/test/testHelpers/datasets/datasetHelper.ts b/test/testHelpers/datasets/datasetHelper.ts index 4cb1ee79..d9f9405c 100644 --- a/test/testHelpers/datasets/datasetHelper.ts +++ b/test/testHelpers/datasets/datasetHelper.ts @@ -51,7 +51,7 @@ export const createDatasetModel = ( minorNumber: 0, state: DatasetVersionState.RELEASED, createTime: new Date(DATASET_CREATE_TIME_STR), - lastUpdateTime: new Date(DATASET_UPDATE_TIME_STR), + lastUpdateTime: DATASET_UPDATE_TIME_STR, releaseTime: new Date(DATASET_RELEASE_TIME_STR), deaccessionNote: undefined }, From 9366f9bdb74a44e98464407e66c4a0deb0a55577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 25 Sep 2025 16:40:49 -0300 Subject: [PATCH 008/110] fixes --- src/datasets/domain/useCases/UpdateDataset.ts | 2 +- test/testHelpers/datasets/datasetPreviewHelper.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datasets/domain/useCases/UpdateDataset.ts b/src/datasets/domain/useCases/UpdateDataset.ts index d87764f2..26ca23b9 100644 --- a/src/datasets/domain/useCases/UpdateDataset.ts +++ b/src/datasets/domain/useCases/UpdateDataset.ts @@ -18,7 +18,7 @@ export class UpdateDataset extends DatasetWriteUseCase { * * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {DatasetDTO} [updatedDataset] - DatasetDTO object including the updated dataset metadata field values for each metadata block. - * @param {string} [sourceLastUpdateTime] - The lastUpdateTime value from the dataset. Provide it to ensure optimistic concurrency: if the dataset was updated since you retrieved it, the backend can reject the update to prevent overwriting newer changes. + * @param {string} [sourceLastUpdateTime] - The lastUpdateTime value from the dataset. If another user updates the dataset version metadata before you send the update request, data inconsistencies may occur. To prevent this, you can use the optional sourceLastUpdateTime parameter. This parameter must include the lastUpdateTime value corresponding to the dataset version being updated. * @returns {Promise} - This method does not return anything upon successful completion. * @throws {ResourceValidationError} - If there are validation errors related to the provided information. * @throws {ReadError} - If there are errors while reading data. diff --git a/test/testHelpers/datasets/datasetPreviewHelper.ts b/test/testHelpers/datasets/datasetPreviewHelper.ts index ee162d9a..4f65c077 100644 --- a/test/testHelpers/datasets/datasetPreviewHelper.ts +++ b/test/testHelpers/datasets/datasetPreviewHelper.ts @@ -25,7 +25,7 @@ export const createDatasetPreviewModel = (): DatasetPreview => { minorNumber: 0, state: DatasetVersionState.RELEASED, createTime: new Date(DATASET_CREATE_TIME_STR), - lastUpdateTime: new Date(DATASET_UPDATE_TIME_STR), + lastUpdateTime: DATASET_UPDATE_TIME_STR, releaseTime: new Date(DATASET_RELEASE_TIME_STR) }, citation: DATASET_CITATION_HTML, From 09a40480ebac5141a7790358bd76b7e7eb196d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 08:58:40 -0300 Subject: [PATCH 009/110] feat: add sourceLastUpdateTime argument to update dataset metadata use case --- src/files/domain/models/FileModel.ts | 1 + .../domain/repositories/IFilesRepository.ts | 3 +- .../domain/useCases/UpdateFileMetadata.ts | 10 +++- .../infra/repositories/FilesRepository.ts | 7 ++- .../repositories/transformers/FilePayload.ts | 1 + .../transformers/fileTransformers.ts | 3 +- .../integration/files/FilesRepository.test.ts | 55 +++++++++++++++++++ test/testHelpers/files/filesHelper.ts | 6 +- 8 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/files/domain/models/FileModel.ts b/src/files/domain/models/FileModel.ts index abf95d71..61b621a6 100644 --- a/src/files/domain/models/FileModel.ts +++ b/src/files/domain/models/FileModel.ts @@ -30,6 +30,7 @@ export interface FileModel { tabularTags?: string[] creationDate?: string publicationDate?: string + lastUpdateTime: string deleted: boolean tabularData: boolean fileAccessRequest?: boolean diff --git a/src/files/domain/repositories/IFilesRepository.ts b/src/files/domain/repositories/IFilesRepository.ts index 8049010c..9256d92d 100644 --- a/src/files/domain/repositories/IFilesRepository.ts +++ b/src/files/domain/repositories/IFilesRepository.ts @@ -72,7 +72,8 @@ export interface IFilesRepository { updateFileMetadata( fileId: number | string, - updateFileMetadataDTO: UpdateFileMetadataDTO + updateFileMetadataDTO: UpdateFileMetadataDTO, + sourceLastUpdateTime?: string ): Promise updateFileTabularTags( diff --git a/src/files/domain/useCases/UpdateFileMetadata.ts b/src/files/domain/useCases/UpdateFileMetadata.ts index f06c96c3..c3b62d8a 100644 --- a/src/files/domain/useCases/UpdateFileMetadata.ts +++ b/src/files/domain/useCases/UpdateFileMetadata.ts @@ -15,12 +15,18 @@ export class UpdateFileMetadata implements UseCase { * * @param {number | string} [fileId] - The file identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). * @param {UpdateFileMetadataDTO} [updateFileMetadataDTO] - The DTO containing the metadata updates. + * @param {string} [sourceLastUpdateTime] - The lastUpdateTime value from the file. If another user updates the file metadata before you send the update request, data inconsistencies may occur. To prevent this, you can use the optional sourceLastUpdateTime parameter. This parameter must include the lastUpdateTime value corresponding to the file being updated. * @returns {Promise} */ async execute( fileId: number | string, - updateFileMetadataDTO: UpdateFileMetadataDTO + updateFileMetadataDTO: UpdateFileMetadataDTO, + sourceLastUpdateTime?: string ): Promise { - await this.filesRepository.updateFileMetadata(fileId, updateFileMetadataDTO) + await this.filesRepository.updateFileMetadata( + fileId, + updateFileMetadataDTO, + sourceLastUpdateTime + ) } } diff --git a/src/files/infra/repositories/FilesRepository.ts b/src/files/infra/repositories/FilesRepository.ts index 3430cd4d..3d24edaf 100644 --- a/src/files/infra/repositories/FilesRepository.ts +++ b/src/files/infra/repositories/FilesRepository.ts @@ -369,7 +369,8 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { public async updateFileMetadata( fileId: string | number, - updateFileMetadata: UpdateFileMetadataDTO + updateFileMetadata: UpdateFileMetadataDTO, + sourceLastUpdateTime?: string ): Promise { const formData = new FormData() formData.append('jsonData', JSON.stringify(updateFileMetadata)) @@ -377,7 +378,9 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { return this.doPost( this.buildApiEndpoint(this.filesResourceName, `${fileId}/metadata`), formData, - {}, + { + ...(sourceLastUpdateTime && { sourceLastUpdateTime }) + }, ApiConstants.CONTENT_TYPE_MULTIPART_FORM_DATA ) .then(() => undefined) diff --git a/src/files/infra/repositories/transformers/FilePayload.ts b/src/files/infra/repositories/transformers/FilePayload.ts index 58afcc4a..cc7ad6b1 100644 --- a/src/files/infra/repositories/transformers/FilePayload.ts +++ b/src/files/infra/repositories/transformers/FilePayload.ts @@ -29,6 +29,7 @@ export interface FilePayload { tabularTags?: string[] creationDate?: string publicationDate?: string + lastUpdateTime: string deleted: boolean tabularData: boolean fileAccessRequest?: boolean diff --git a/src/files/infra/repositories/transformers/fileTransformers.ts b/src/files/infra/repositories/transformers/fileTransformers.ts index 5350af01..a93b7674 100644 --- a/src/files/infra/repositories/transformers/fileTransformers.ts +++ b/src/files/infra/repositories/transformers/fileTransformers.ts @@ -93,7 +93,8 @@ const transformFilePayloadToFile = (filePayload: FilePayload): FileModel => { }), ...(filePayload.dataFile.isPartOf && { isPartOf: transformPayloadToOwnerNode(filePayload.dataFile.isPartOf) - }) + }), + lastUpdateTime: filePayload.dataFile.lastUpdateTime } } diff --git a/test/integration/files/FilesRepository.test.ts b/test/integration/files/FilesRepository.test.ts index cde53fcd..c20fd0c4 100644 --- a/test/integration/files/FilesRepository.test.ts +++ b/test/integration/files/FilesRepository.test.ts @@ -767,6 +767,61 @@ describe('FilesRepository', () => { errorExpected ) }) + + test('should throw error when using outdated sourceLastUpdateTime', async () => { + const newDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await uploadFileViaApi(newDatasetIds.numericId, testTextFile1Name) + const filesSubset = await sut.getDatasetFiles( + newDatasetIds.numericId, + latestDatasetVersionId, + false, + FileOrderCriteria.NAME_AZ + ) + const fileId = filesSubset.files[0].id + + await registerFileViaApi(fileId) + + // Fetch file to obtain initial lastUpdateTime from returned model including dataset version + const fileInfo: FileModel = (await sut.getFile( + fileId, + DatasetNotNumberedVersion.LATEST, + false, + false + )) as FileModel + + const lastUpdateTimeOne = fileInfo.lastUpdateTime + + // Wait for 2 seconds + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + // First update using correct lastUpdateTime should succeed + await sut.updateFileMetadata(fileId, { description: 'First update desc.' }, lastUpdateTimeOne) + + // Refetch to get new lastUpdateTime + const fileInfoAfterFirstUpdate: FileModel = (await sut.getFile( + fileId, + DatasetNotNumberedVersion.LATEST, + false, + false + )) as FileModel + + const lastUpdateTimeTwo = fileInfoAfterFirstUpdate.lastUpdateTime + + expect(lastUpdateTimeTwo).not.toBe(lastUpdateTimeOne) + + // Wait for 2 seconds + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + // Second update using stale lastUpdateTimeOne should fail + const expectedError = new WriteError( + `[400] Internal version timestamp ${lastUpdateTimeOne} is outdated` + ) + await expect( + sut.updateFileMetadata(fileId, { description: 'Second update attempt.' }, lastUpdateTimeOne) + ).rejects.toThrow(expectedError) + + await deletePublishedDatasetViaApi(newDatasetIds.persistentId) + }) }) describe('updateFileTabularTags', () => { diff --git a/test/testHelpers/files/filesHelper.ts b/test/testHelpers/files/filesHelper.ts index 1f0e6296..a4fce9ed 100644 --- a/test/testHelpers/files/filesHelper.ts +++ b/test/testHelpers/files/filesHelper.ts @@ -60,7 +60,8 @@ export const createFileModel = (): FileModel => { originalSize: 127426, originalName: 'originalName', tabularTags: ['tag1', 'tag2'], - publicationDate: '2023-07-11' + publicationDate: '2023-07-11', + lastUpdateTime: '2023-07-11' } } @@ -122,7 +123,8 @@ export const createFilePayload = (): FilePayload => { originalSize: 127426, originalName: 'originalName', tabularTags: ['tag1', 'tag2'], - publicationDate: '2023-07-11' + publicationDate: '2023-07-11', + lastUpdateTime: '2023-07-11' } } } From e76e9d99362ba825bbdcae6d2a4fec1cfd8309ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 10:03:42 -0300 Subject: [PATCH 010/110] fix: unit test --- test/unit/files/UpdateFileMetadata.test.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/unit/files/UpdateFileMetadata.test.ts b/test/unit/files/UpdateFileMetadata.test.ts index 41255e48..2175e4f2 100644 --- a/test/unit/files/UpdateFileMetadata.test.ts +++ b/test/unit/files/UpdateFileMetadata.test.ts @@ -13,7 +13,11 @@ describe('UpdateFileMetadata', () => { await sut.execute(1, testFileMetadata) - expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith(1, testFileMetadata) + expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith( + 1, + testFileMetadata, + undefined + ) expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledTimes(1) }) @@ -28,7 +32,8 @@ describe('UpdateFileMetadata', () => { expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith( 'doi:10.5072/FK2/HC6KTB', - testFileMetadata + testFileMetadata, + undefined ) expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledTimes(1) }) @@ -41,6 +46,10 @@ describe('UpdateFileMetadata', () => { const sut = new UpdateFileMetadata(filesRepositoryStub) await expect(sut.execute(1, testFileMetadata)).rejects.toThrow(WriteError) - expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith(1, testFileMetadata) + expect(filesRepositoryStub.updateFileMetadata).toHaveBeenCalledWith( + 1, + testFileMetadata, + undefined + ) }) }) From a1cef4be352c35c0d82ffe8458fc51b31849808c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 26 Sep 2025 10:17:05 -0300 Subject: [PATCH 011/110] feat: add jsdoc about lastUpdateTime --- src/datasets/domain/models/Dataset.ts | 5 +++ src/files/domain/models/FileModel.ts | 56 ++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/datasets/domain/models/Dataset.ts b/src/datasets/domain/models/Dataset.ts index ac4c4623..f76f11f2 100644 --- a/src/datasets/domain/models/Dataset.ts +++ b/src/datasets/domain/models/Dataset.ts @@ -22,6 +22,11 @@ export interface DatasetVersionInfo { minorNumber: number state: DatasetVersionState createTime: Date + /** + * The timestamp of the last update to this dataset version. + * Format: ISO 8601 string (e.g., "2023-06-01T12:34:56Z"). + * Used for optimistic concurrency control to detect concurrent updates. + */ lastUpdateTime: string releaseTime?: Date deaccessionNote?: string diff --git a/src/files/domain/models/FileModel.ts b/src/files/domain/models/FileModel.ts index 61b621a6..396a2379 100644 --- a/src/files/domain/models/FileModel.ts +++ b/src/files/domain/models/FileModel.ts @@ -1,4 +1,4 @@ -import { DvObjectOwnerNode } from '../../../core/domain/models/DvObjectOwnerNode' +import { DvObjectOwnerNode, DvObjectType } from '../../../core/domain/models/DvObjectOwnerNode' export interface FileModel { id: number @@ -30,6 +30,11 @@ export interface FileModel { tabularTags?: string[] creationDate?: string publicationDate?: string + /** + * The timestamp of the last update to this file record. + * Format: ISO 8601 string (e.g., "2023-06-01T12:34:56Z"). + * Used for optimistic concurrency control to detect concurrent updates. + */ lastUpdateTime: string deleted: boolean tabularData: boolean @@ -46,3 +51,52 @@ export interface FileChecksum { type: string value: string } + +const fileModelExample: FileModel = { + id: 123, + persistentId: 'doi:10.1234/example', + name: 'data.csv', + pidURL: 'http://example.com/file/123', + sizeBytes: 2048, + version: 1, + description: 'Sample data file', + restricted: false, + latestRestricted: false, + directoryLabel: '/data', + datasetVersionId: 1, + categories: ['survey', 'demographics'], + contentType: 'text/csv', + friendlyType: 'CSV', + embargo: { + dateAvailable: new Date('2024-12-31'), + reason: 'Embargo until publication' + }, + storageIdentifier: 's3://bucket/key', + originalFormat: 'csv', + originalFormatLabel: 'CSV', + originalSize: 2048, + originalName: 'data_original.csv', + UNF: 'UNF:6:abc123', + rootDataFileId: 123, + previousDataFileId: 122, + md5: 'd41d8cd98f00b204e9800998ecf8427e', + checksum: { + type: 'MD5', + value: 'd41d8cd98f00b204e9800998ecf8427e' + }, + metadataId: 456, + tabularTags: ['tabular', 'data'], + creationDate: '2024-01-01T12:00:00Z', + publicationDate: '2024-06-01T12:00:00Z', + lastUpdateTime: '2024-06-15T12:34:56Z', + deleted: false, + tabularData: true, + fileAccessRequest: false, + isPartOf: { + type: 'dataverse' as DvObjectType, + displayName: 'My Dataverse', + identifier: 'my-dataverse' + } +} + +fileModelExample.lastUpdateTime From 0f1fd84fa4fbca53bdce1932e2110ea894b72047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 29 Sep 2025 11:36:33 -0300 Subject: [PATCH 012/110] remove unused --- src/files/domain/models/FileModel.ts | 51 +--------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/files/domain/models/FileModel.ts b/src/files/domain/models/FileModel.ts index 396a2379..559396b8 100644 --- a/src/files/domain/models/FileModel.ts +++ b/src/files/domain/models/FileModel.ts @@ -1,4 +1,4 @@ -import { DvObjectOwnerNode, DvObjectType } from '../../../core/domain/models/DvObjectOwnerNode' +import { DvObjectOwnerNode } from '../../../core/domain/models/DvObjectOwnerNode' export interface FileModel { id: number @@ -51,52 +51,3 @@ export interface FileChecksum { type: string value: string } - -const fileModelExample: FileModel = { - id: 123, - persistentId: 'doi:10.1234/example', - name: 'data.csv', - pidURL: 'http://example.com/file/123', - sizeBytes: 2048, - version: 1, - description: 'Sample data file', - restricted: false, - latestRestricted: false, - directoryLabel: '/data', - datasetVersionId: 1, - categories: ['survey', 'demographics'], - contentType: 'text/csv', - friendlyType: 'CSV', - embargo: { - dateAvailable: new Date('2024-12-31'), - reason: 'Embargo until publication' - }, - storageIdentifier: 's3://bucket/key', - originalFormat: 'csv', - originalFormatLabel: 'CSV', - originalSize: 2048, - originalName: 'data_original.csv', - UNF: 'UNF:6:abc123', - rootDataFileId: 123, - previousDataFileId: 122, - md5: 'd41d8cd98f00b204e9800998ecf8427e', - checksum: { - type: 'MD5', - value: 'd41d8cd98f00b204e9800998ecf8427e' - }, - metadataId: 456, - tabularTags: ['tabular', 'data'], - creationDate: '2024-01-01T12:00:00Z', - publicationDate: '2024-06-01T12:00:00Z', - lastUpdateTime: '2024-06-15T12:34:56Z', - deleted: false, - tabularData: true, - fileAccessRequest: false, - isPartOf: { - type: 'dataverse' as DvObjectType, - displayName: 'My Dataverse', - identifier: 'my-dataverse' - } -} - -fileModelExample.lastUpdateTime From 686ff792931f704adf99fc8863ca47680b913ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 2 Oct 2025 16:09:27 -0300 Subject: [PATCH 013/110] refactor: remove date format validation logic and associated error handling --- .../SingleMetadataFieldValidator.ts | 49 +--------------- .../validators/errors/DateFormatFieldError.ts | 19 ------ .../datasets/DatasetResourceValidator.test.ts | 58 ------------------- 3 files changed, 1 insertion(+), 125 deletions(-) delete mode 100644 src/datasets/domain/useCases/validators/errors/DateFormatFieldError.ts diff --git a/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts b/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts index f39f0ed4..f233e450 100644 --- a/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts +++ b/src/datasets/domain/useCases/validators/SingleMetadataFieldValidator.ts @@ -3,14 +3,12 @@ import { DatasetMetadataFieldAndValueInfo } from './BaseMetadataFieldValidator' import { ControlledVocabularyFieldError } from './errors/ControlledVocabularyFieldError' -import { DateFormatFieldError } from './errors/DateFormatFieldError' import { MetadataFieldValidator } from './MetadataFieldValidator' import { DatasetMetadataChildFieldValueDTO } from '../../dtos/DatasetDTO' import { MultipleMetadataFieldValidator } from './MultipleMetadataFieldValidator' import { MetadataFieldInfo, - MetadataFieldType, - MetadataFieldWatermark + MetadataFieldType } from '../../../../metadataBlocks/domain/models/MetadataBlock' export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator { @@ -50,10 +48,6 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator { this.validateControlledVocabularyFieldValue(datasetMetadataFieldAndValueInfo) } - if (metadataFieldInfo.type == MetadataFieldType.Date) { - this.validateDateFieldValue(datasetMetadataFieldAndValueInfo) - } - if (metadataFieldInfo.childMetadataFields != undefined) { this.validateChildMetadataFieldValues(datasetMetadataFieldAndValueInfo) } @@ -76,47 +70,6 @@ export class SingleMetadataFieldValidator extends BaseMetadataFieldValidator { } } - private validateDateFieldValue( - datasetMetadataFieldAndValueInfo: DatasetMetadataFieldAndValueInfo - ) { - const { - metadataFieldInfo: { watermark }, - metadataFieldValue - } = datasetMetadataFieldAndValueInfo - - const acceptsAllDateFormats = watermark === MetadataFieldWatermark.YYYYOrYYYYMMOrYYYYMMDD - - const YYYY_MM_DD_DATE_FORMAT_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/ - - const YYYY_MM_FORMAT_REGEX = /^\d{4}-(0[1-9]|1[0-2])$/ - - const YYYY_FORMAT_REGEX = /^\d{4}$/ - - const isValidDateFormat = (value: string): boolean => { - if (acceptsAllDateFormats) { - // Check if it matches any of the formats - return ( - YYYY_MM_DD_DATE_FORMAT_REGEX.test(value) || - YYYY_MM_FORMAT_REGEX.test(value) || - YYYY_FORMAT_REGEX.test(value) - ) - } else { - // Only accepts YYYY-MM-DD format - return YYYY_MM_DD_DATE_FORMAT_REGEX.test(value) - } - } - - if (!isValidDateFormat(metadataFieldValue as string)) { - throw new DateFormatFieldError( - datasetMetadataFieldAndValueInfo.metadataFieldKey, - datasetMetadataFieldAndValueInfo.metadataBlockName, - watermark, - datasetMetadataFieldAndValueInfo.metadataParentFieldKey, - datasetMetadataFieldAndValueInfo.metadataFieldPosition - ) - } - } - private validateChildMetadataFieldValues( datasetMetadataFieldAndValueInfo: DatasetMetadataFieldAndValueInfo ) { diff --git a/src/datasets/domain/useCases/validators/errors/DateFormatFieldError.ts b/src/datasets/domain/useCases/validators/errors/DateFormatFieldError.ts deleted file mode 100644 index 6a174837..00000000 --- a/src/datasets/domain/useCases/validators/errors/DateFormatFieldError.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FieldValidationError } from './FieldValidationError' - -export class DateFormatFieldError extends FieldValidationError { - constructor( - metadataFieldName: string, - citationBlockName: string, - validDateFormat: string, - parentMetadataFieldName?: string, - fieldPosition?: number - ) { - super( - metadataFieldName, - citationBlockName, - parentMetadataFieldName, - fieldPosition, - `The field requires a valid date format (${validDateFormat}).` - ) - } -} diff --git a/test/unit/datasets/DatasetResourceValidator.test.ts b/test/unit/datasets/DatasetResourceValidator.test.ts index 91d5f10e..c83bb241 100644 --- a/test/unit/datasets/DatasetResourceValidator.test.ts +++ b/test/unit/datasets/DatasetResourceValidator.test.ts @@ -154,64 +154,6 @@ describe('validate', () => { expect(() => sut.validate(testDataset, testMetadataBlocks)).not.toThrow() }) - test('should raise a date format validation error when a date field has an invalid format', () => { - const testDataset = createDatasetDTO(undefined, undefined, undefined, '1-1-2020') - - expect.assertions(6) - runValidateExpectingFieldValidationError( - testDataset, - 'timePeriodCoveredStart', - 'There was an error when validating the field timePeriodCoveredStart from metadata block citation. Reason was: The field requires a valid date format (YYYY or YYYY-MM or YYYY-MM-DD).' - ) - }) - - test('should not raise a date format validation error when a date field has a valid YYYY-MM-DD format', () => { - const testDataset = createDatasetDTO(undefined, undefined, undefined, '2020-01-01') - expect(() => sut.validate(testDataset, testMetadataBlocks)).not.toThrow() - }) - - test('should not raise a date format validation error when a date field has a valid YYYY-MM format', () => { - const testDataset = createDatasetDTO(undefined, undefined, undefined, '2020-01') - expect(() => sut.validate(testDataset, testMetadataBlocks)).not.toThrow() - }) - - test('should not raise a date format validation error when a date field has a valid YYYY format', () => { - const testDataset = createDatasetDTO(undefined, undefined, undefined, '2020') - expect(() => sut.validate(testDataset, testMetadataBlocks)).not.toThrow() - }) - - test('should raise a date format validation error when a date field has a wrong date format according to the field watermark', () => { - const testDataset = createDatasetDTO( - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - '01-03' - ) - - expect.assertions(6) - runValidateExpectingFieldValidationError( - testDataset, - 'dateOfCreation', - 'There was an error when validating the field dateOfCreation from metadata block citation. Reason was: The field requires a valid date format (YYYY-MM-DD).' - ) - }) - - test('should not raise a date format validation error when a date field has a valid format according to the field watermark', () => { - const testDataset = createDatasetDTO( - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - '2024-01-03' - ) - expect(() => sut.validate(testDataset, testMetadataBlocks)).not.toThrow() - }) - test('should raise a controlled vocabulary error when a controlled vocabulary field has an invalid format', () => { const testDataset = createDatasetDTO( undefined, From 565bcd5d3c21cf86631fe54c828d6eecaface351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 2 Oct 2025 16:22:24 -0300 Subject: [PATCH 014/110] docs: add to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea3e0f7..cb116c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Removed +- Removed date fields validations in create and update dataset use cases, since validation is already handled in the backend and SPA frontend (other clients should perform client side validation also). This avoids duplicated logic and keeps the package focused on its core responsibility. + [Unreleased]: https://github.com/IQSS/dataverse-client-javascript/compare/v2.1.0...develop --- From bb9affc552876ca5d64ea583f63d2d461b4a290c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 2 Oct 2025 16:50:59 -0300 Subject: [PATCH 015/110] test: fix test after change in the backend --- test/integration/collections/CollectionsRepository.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index d1afd76d..2974e482 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -996,8 +996,8 @@ describe('CollectionsRepository', () => { expect(updatedCollection.alias).toBe(testUpdatedCollectionAlias) expect(updatedCollection.name).toBe(updatedCollectionName) expect(updatedCollection.affiliation).toBe(updatedCollectionAffiliation) - expect(updatedCollection.inputLevels?.length).toBe(1) - const updatedInputLevel = updatedCollection.inputLevels?.[0] + expect(updatedCollection.inputLevels?.length).toBe(2) + const updatedInputLevel = updatedCollection.inputLevels?.[1] expect(updatedInputLevel?.datasetFieldName).toBe('country') expect(updatedInputLevel?.include).toBe(true) expect(updatedInputLevel?.required).toBe(false) From 93da193f7953e42bd219d041619057745bf54c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 3 Oct 2025 12:35:45 -0300 Subject: [PATCH 016/110] docs: add to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb116c44..57ea977c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added +- New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). + ### Changed ### Fixed From cfe34ea764047b839a7e3f8b65e40a85076a4587 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 3 Oct 2025 18:08:08 -0400 Subject: [PATCH 017/110] feat: add pagination --- .../repositories/INotificationsRepository.ts | 7 +- .../useCases/GetAllNotificationsByUser.ts | 12 +++- .../repositories/NotificationsRepository.ts | 21 +++++- .../NotificationsRepository.test.ts | 65 ++++++++++++++++++- 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index 9392c543..dc8895ca 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -1,7 +1,12 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { - getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise + getAllNotificationsByUser( + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number + ): Promise deleteNotification(notificationId: number): Promise getUnreadNotificationsCount(): Promise markNotificationAsRead(notificationId: number): Promise diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts index 43555ccc..14d3b813 100644 --- a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -11,9 +11,17 @@ export class GetAllNotificationsByUser implements UseCase { * @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(inAppNotificationFormat?: boolean): Promise { + async execute( + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number + ): Promise { return (await this.notificationsRepository.getAllNotificationsByUser( - inAppNotificationFormat + inAppNotificationFormat, + onlyUnread, + limit, + offset )) as Notification[] } } diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index f310c34a..196a3d9d 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -7,13 +7,28 @@ export class NotificationsRepository extends ApiRepository implements INotificat private readonly notificationsResourceName: string = 'notifications' public async getAllNotificationsByUser( - inAppNotificationFormat?: boolean + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number ): Promise { - const queryParams = inAppNotificationFormat ? { inAppNotificationFormat: 'true' } : undefined + const queryParams = new URLSearchParams() + if (inAppNotificationFormat) { + queryParams.set('inAppNotificationFormat', 'true') + } + if (onlyUnread) { + queryParams.set('onlyUnread', 'true') + } + if (typeof limit === 'number') { + queryParams.set('limit', limit.toString()) + } + if (typeof offset === 'number') { + queryParams.set('offset', offset.toString()) + } return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'all'), true, - queryParams + queryParams.toString().length > 0 ? queryParams : undefined ) .then((response) => { const notifications = response.data.data.notifications diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 5333e48d..5feb23b3 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -9,7 +9,11 @@ import { NotificationType } from '../../../src/notifications/domain/models/Notification' import { createDataset, CreatedDatasetIdentifiers } from '../../../src/datasets' -import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' +import { + publishDatasetViaApi, + waitForNoLocks, + deletePublishedDatasetViaApi +} from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' import { createCollection } from '../../../src/collections' import { @@ -95,7 +99,24 @@ describe('NotificationsRepository', () => { expect(notification).toHaveProperty('displayAsRead') }) - test('should find notification with ASSIGNROLE type that has not been deleted', async () => { + test('should return only unread notifications when onlyUnread is true (if any exist)', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(true, true) + + expect(Array.isArray(notifications)).toBe(true) + expect(Array.isArray(notifications)).toBe(true) + expect(notifications.every((n) => n.displayAsRead === false)).toBe(true) + }) + + test('should paginate results using limit and offset', async () => { + const limit = 1 + const page1: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 0) + const page2: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 1) + + expect(page1.length).toBeLessThanOrEqual(limit) + expect(page2.length).toBeLessThanOrEqual(limit) + + // Always run the assertion, but only if both pages have one notification each + expect(page1.length !== 1 || page2.length !== 1 || page1[0].id !== page2[0].id).toBe(true) const notifications: Notification[] = await sut.getAllNotificationsByUser(true) const assignRoleNotification = notifications.find( @@ -116,6 +137,46 @@ describe('NotificationsRepository', () => { expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) + test('should generate 5+ notifications and verify pagination across pages', async () => { + const createdDatasets: CreatedDatasetIdentifiers[] = [] + try { + const howMany = 5 + for (let i = 0; i < howMany; i++) { + const ids = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + createdDatasets.push(ids) + await publishDatasetViaApi(ids.numericId) + await waitForNoLocks(ids.numericId, 10) + } + + const limit = 5 + const page1: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 0) + const page2: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 5) + const page3: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 10) + expect(page1.length).toBeLessThanOrEqual(limit) + expect(page2.length).toBeLessThanOrEqual(limit) + expect(page3.length).toBeLessThanOrEqual(limit) + + const ids1 = new Set(page1.map((n) => n.id)) + const ids2 = new Set(page2.map((n) => n.id)) + const ids3 = new Set(page3.map((n) => n.id)) + + const intersects = (a: Set, b: Set): boolean => { + for (const x of a) { + if (b.has(x)) return true + } + return false + } + + expect(page1.length === 0 || page2.length === 0 || !intersects(ids1, ids2)).toBe(true) + expect(page1.length === 0 || page3.length === 0 || !intersects(ids1, ids3)).toBe(true) + expect(page2.length === 0 || page3.length === 0 || !intersects(ids2, ids3)).toBe(true) + } finally { + for (const d of createdDatasets) { + await deletePublishedDatasetViaApi(d.persistentId) + } + } + }) + test('should create a collection and find the notification with CREATEDV type', async () => { const testCollectionAlias = 'test-notification-collection' const createdCollectionId = await createCollection.execute( From 967a016393fbc535f6f066a6561ed602e2ab1329 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 6 Oct 2025 14:52:14 -0400 Subject: [PATCH 018/110] update GetAllNotificationsByUser use case --- src/notifications/domain/models/Notification.ts | 2 +- .../repositories/INotificationsRepository.ts | 7 ++++++- .../domain/useCases/GetAllNotificationsByUser.ts | 15 +++++++++++++-- .../infra/repositories/NotificationsRepository.ts | 15 ++++++++++++--- .../infra/transformers/NotificationPayload.ts | 2 +- .../GetAllNotificationsByUser.test.ts | 9 +++++++++ 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index 001d0933..f60b0ced 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -67,6 +67,6 @@ export interface Notification { dataFileId?: number dataFileDisplayName?: string currentCurationStatus?: string - additionalInfo?: string + additionalInfo?: Record objectDeleted?: boolean } diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index 9392c543..dc8895ca 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -1,7 +1,12 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { - getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise + getAllNotificationsByUser( + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number + ): Promise deleteNotification(notificationId: number): Promise getUnreadNotificationsCount(): Promise markNotificationAsRead(notificationId: number): Promise diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts index 43555ccc..ba53bc96 100644 --- a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -9,11 +9,22 @@ 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 + * @param onlyUnread - Optional parameter to filter only unread notifications + * @param limit - Optional parameter to limit the number of notifications returned + * @param offset - Optional parameter to skip a number of notifications (for pagination) * @returns {Promise} - A promise that resolves to an array of Notification instances. */ - async execute(inAppNotificationFormat?: boolean): Promise { + async execute( + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number + ): Promise { return (await this.notificationsRepository.getAllNotificationsByUser( - inAppNotificationFormat + inAppNotificationFormat, + onlyUnread, + limit, + offset )) as Notification[] } } diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index f310c34a..a2cf5c8d 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -7,13 +7,22 @@ export class NotificationsRepository extends ApiRepository implements INotificat private readonly notificationsResourceName: string = 'notifications' public async getAllNotificationsByUser( - inAppNotificationFormat?: boolean + inAppNotificationFormat?: boolean, + onlyUnread?: boolean, + limit?: number, + offset?: number ): Promise { - const queryParams = inAppNotificationFormat ? { inAppNotificationFormat: 'true' } : undefined + const queryParams: Record = {} + + if (inAppNotificationFormat) queryParams.inAppNotificationFormat = 'true' + if (onlyUnread) queryParams.onlyUnread = 'true' + if (limit !== undefined) queryParams.limit = limit + if (offset !== undefined) queryParams.offset = offset + return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'all'), true, - queryParams + Object.keys(queryParams).length ? queryParams : undefined ) .then((response) => { const notifications = response.data.data.notifications diff --git a/src/notifications/infra/transformers/NotificationPayload.ts b/src/notifications/infra/transformers/NotificationPayload.ts index 96d381ac..d63bde79 100644 --- a/src/notifications/infra/transformers/NotificationPayload.ts +++ b/src/notifications/infra/transformers/NotificationPayload.ts @@ -25,6 +25,6 @@ export interface NotificationPayload { dataFileId?: number dataFileDisplayName?: string currentCurationStatus?: string - additionalInfo?: string + additionalInfo?: Record objectDeleted?: boolean } diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 7ccd7ec1..08582ac2 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -34,4 +34,13 @@ describe('execute', () => { expect(notifications[0]).toHaveProperty('sentTimestamp') expect(notifications[0]).toHaveProperty('displayAsRead') }) + test('should have correct in-app notification properties when filter and paging params are set', async () => { + const notifications = await getAllNotificationsByUser.execute(true, true, 1, 0) + + expect(notifications[0]).toHaveProperty('id') + expect(notifications[0]).toHaveProperty('type') + expect(notifications[0]).toHaveProperty('sentTimestamp') + expect(notifications[0]).toHaveProperty('displayAsRead') + expect(notifications.length).toBeLessThanOrEqual(1) + }) }) From 40189c879f7d4d4808cb723eca957b85af71567c Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 6 Oct 2025 17:43:02 -0400 Subject: [PATCH 019/110] add integration tests --- test/environment/.env | 4 ++-- .../collections/CollectionsRepository.test.ts | 2 +- .../NotificationsRepository.test.ts | 22 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..7d1dda97 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=11852-notifs-api-pagination-unread DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 2974e482..fb913064 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -1381,7 +1381,7 @@ describe('CollectionsRepository', () => { }) it('should return error when the dvObjectIdentifier of a file does not exist', async () => { - const invalidFileId = '99' + const invalidFileId = '99999999' const newFeaturedItems: DvObjectFeaturedItemDTO[] = [ { type: FeaturedItemType.FILE, diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 5333e48d..08659a53 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -183,4 +183,26 @@ describe('NotificationsRepository', () => { expectedError ) }) + test('should only return unread notifications when onlyUnread is true', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(true, true) + + expect(Array.isArray(notifications)).toBe(true) + const originalUnreadCount = notifications.length + expect(notifications.length).toBeGreaterThanOrEqual(0) + + await expect(sut.markNotificationAsRead(notifications[0].id)).resolves.toBeUndefined() + + const updatedNotifications: Notification[] = await sut.getAllNotificationsByUser(true, true) + expect(updatedNotifications.length).toBe(originalUnreadCount - 1) + + const hasReadNotifications = notifications.some((n) => n.displayAsRead === true) + expect(hasReadNotifications).toBe(false) + }) + test('should return limited number of notifications when limit is set', async () => { + const limit = 1 + const notifications: Notification[] = await sut.getAllNotificationsByUser(true, false, limit, 0) + + expect(Array.isArray(notifications)).toBe(true) + expect(notifications.length).toBeLessThanOrEqual(limit) + }) }) From 2ebd8af9a1a96708a7c0388aca9746bfc5d1e8cc Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 8 Oct 2025 14:38:59 -0400 Subject: [PATCH 020/110] add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb116c44..7e344ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,14 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added +- In GetAllNotificationsByUser use case, added support for filtering unread messages and pagination. + ### Changed ### Fixed +- In GetAllNotificationsByUser use case, additionalInfo field is returned as an object instead of a string. + ### Removed - Removed date fields validations in create and update dataset use cases, since validation is already handled in the backend and SPA frontend (other clients should perform client side validation also). This avoids duplicated logic and keeps the package focused on its core responsibility. From b0929de83347676f5884bf6d50ad1bc3077ef809 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 14 Oct 2025 10:44:48 -0400 Subject: [PATCH 021/110] feat: feat: add use case to edit terms of access --- docs/useCases.md | 32 ++++ src/datasets/domain/models/Dataset.ts | 1 + .../repositories/IDatasetsRepository.ts | 2 + .../SetAvailableLicensesForDatasetType.ts | 4 + .../domain/useCases/UpdateTermsOfAccess.ts | 22 +++ src/datasets/index.ts | 3 + .../infra/repositories/DatasetsRepository.ts | 18 +++ .../transformers/termsOfAccessTransformers.ts | 31 ++++ .../datasets/UpdateTermsOfAccess.test.ts | 51 ++++++ .../datasets/DatasetsRepository.test.ts | 145 +++++++++++++++++- .../unit/datasets/UpdateTermsOfAccess.test.ts | 34 ++++ 11 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 src/datasets/domain/useCases/UpdateTermsOfAccess.ts create mode 100644 src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts create mode 100644 test/functional/datasets/UpdateTermsOfAccess.test.ts create mode 100644 test/unit/datasets/UpdateTermsOfAccess.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 60704c23..80f9314c 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -1012,6 +1012,38 @@ The `versionUpdateType` parameter can be a [VersionUpdateType](../src/datasets/d - `VersionUpdateType.MAJOR` - `VersionUpdateType.UPDATE_CURRENT` +#### Update Terms of Access + +Updates the Terms of Access for restricted files on a dataset. + +##### Example call: + +```typescript +import { updateTermsOfAccess } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 3 + +await updateTermsOfAccess.execute(datasetId, { + fileAccessRequest: true, + termsOfAccessForRestrictedFiles: 'Your terms of access for restricted files', + dataAccessPlace: 'Your data access place', + originalArchive: 'Your original archive', + availabilityStatus: 'Your availability status', + contactForAccess: 'Your contact for access', + sizeOfCollection: 'Your size of collection', + studyCompletion: 'Your study completion' +}) +``` + +_See [use case](../src/datasets/domain/useCases/UpdateTermsOfAccess.ts) implementation_. + +Notes: + +- If the dataset is already published, this action creates a DRAFT version containing the new terms. +- Unspecified fields are treated as omissions: sending only `fileAccessRequest` will update that field and leave all other terms absent (undefined). In practice, the new values you send fully replace the previous set of terms — so if you omit a field, you are effectively clearing it unless you include its original value in the new input. + #### Deaccession a Dataset Deaccession a Dataset, given its identifier, version, and deaccessionDatasetDTO to perform. diff --git a/src/datasets/domain/models/Dataset.ts b/src/datasets/domain/models/Dataset.ts index f76f11f2..e1ccfb25 100644 --- a/src/datasets/domain/models/Dataset.ts +++ b/src/datasets/domain/models/Dataset.ts @@ -51,6 +51,7 @@ export interface CustomTerms { conditions?: string disclaimer?: string } + export interface TermsOfAccess { fileAccessRequest: boolean termsOfAccessForRestrictedFiles?: string diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 10843afa..bed323fb 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -14,6 +14,7 @@ import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' import { DatasetTemplate } from '../models/DatasetTemplate' import { DatasetType } from '../models/DatasetType' +import { TermsOfAccess } from '../models/Dataset' export interface IDatasetsRepository { getDataset( @@ -90,4 +91,5 @@ export interface IDatasetsRepository { licenses: string[] ): Promise deleteDatasetType(datasetTypeId: number): Promise + updateTermsOfAccess(datasetId: number | string, termsOfAccess: TermsOfAccess): Promise } diff --git a/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts b/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts index e4df9ab2..9741aa08 100644 --- a/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts +++ b/src/datasets/domain/useCases/SetAvailableLicensesForDatasetType.ts @@ -10,6 +10,10 @@ export class SetAvailableLicensesForDatasetType implements UseCase { /** * Sets the available licenses for a given dataset type. This limits the license options when creating a dataset of this type. + * + * @param {number | string} [datasetTypeId] - The dataset type identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {string[]} licenses - The licenses to set for the dataset type. + * @returns {Promise} - This method does not return anything upon successful completion. */ async execute(datasetTypeId: number | string, licenses: string[]): Promise { return await this.datasetsRepository.setAvailableLicensesForDatasetType(datasetTypeId, licenses) diff --git a/src/datasets/domain/useCases/UpdateTermsOfAccess.ts b/src/datasets/domain/useCases/UpdateTermsOfAccess.ts new file mode 100644 index 00000000..103d4f00 --- /dev/null +++ b/src/datasets/domain/useCases/UpdateTermsOfAccess.ts @@ -0,0 +1,22 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { TermsOfAccess } from '../models/Dataset' + +export class UpdateTermsOfAccess implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Sets the terms of access for a given dataset. + * + * @param {number | string} [datasetId] - The dataset identifier, which can be a string (for persistent identifiers), or a number (for numeric identifiers). + * @param {TermsOfAccess} termsOfAccess - The terms of access to set for the dataset. + * @returns {Promise} - This method does not return anything upon successful completion. + */ + async execute(datasetId: number | string, termsOfAccess: TermsOfAccess): Promise { + return await this.datasetsRepository.updateTermsOfAccess(datasetId, termsOfAccess) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 6b93a7cd..be021070 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -32,6 +32,7 @@ import { SetAvailableLicensesForDatasetType } from './domain/useCases/SetAvailab import { DeleteDatasetType } from './domain/useCases/DeleteDatasetType' import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' +import { UpdateTermsOfAccess } from './domain/useCases/UpdateTermsOfAccess' const datasetsRepository = new DatasetsRepository() @@ -80,6 +81,7 @@ const setAvailableLicensesForDatasetType = new SetAvailableLicensesForDatasetTyp const deleteDatasetType = new DeleteDatasetType(datasetsRepository) const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) const getDatasetTemplates = new GetDatasetTemplates(datasetsRepository) +const updateTermsOfAccess = new UpdateTermsOfAccess(datasetsRepository) export { getDataset, @@ -104,6 +106,7 @@ export { getDatasetAvailableCategories, getDatasetCitationInOtherFormats, getDatasetTemplates, + updateTermsOfAccess, getDatasetAvailableDatasetTypes, getDatasetAvailableDatasetType, addDatasetType, diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 17a77dce..8e4c5831 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -29,6 +29,8 @@ import { DatasetTemplate } from '../../domain/models/DatasetTemplate' import { DatasetTemplatePayload } from './transformers/DatasetTemplatePayload' import { transformDatasetTemplatePayloadToDatasetTemplate } from './transformers/datasetTemplateTransformers' import { DatasetType } from '../../domain/models/DatasetType' +import { TermsOfAccess } from '../../domain/models/Dataset' +import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAccessTransformers' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -451,4 +453,20 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async updateTermsOfAccess( + datasetId: number | string, + termsOfAccess: TermsOfAccess + ): Promise { + return this.doPut( + this.buildApiEndpoint(this.datasetsResourceName, 'access', datasetId), + transformTermsOfAccessToUpdatePayload( + termsOfAccess as TermsOfAccess & { termsOfAccess?: string } + ) + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts b/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts new file mode 100644 index 00000000..dd2fbc0b --- /dev/null +++ b/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts @@ -0,0 +1,31 @@ +import { TermsOfAccess } from '../../../domain/models/Dataset' + +type TermsOfAccessInput = TermsOfAccess & { termsOfAccess?: string } + +export const transformTermsOfAccessToUpdatePayload = (terms: TermsOfAccessInput) => { + const { + fileAccessRequest, + dataAccessPlace, + originalArchive, + availabilityStatus, + contactForAccess, + sizeOfCollection, + studyCompletion + } = terms + + const termsOfAccessForRestrictedFiles = + terms.termsOfAccess ?? terms.termsOfAccessForRestrictedFiles + + return { + customTermsOfAccess: { + fileAccessRequest, + termsOfAccess: termsOfAccessForRestrictedFiles, + dataAccessPlace, + originalArchive, + availabilityStatus, + contactForAccess, + sizeOfCollection, + studyCompletion + } + } +} diff --git a/test/functional/datasets/UpdateTermsOfAccess.test.ts b/test/functional/datasets/UpdateTermsOfAccess.test.ts new file mode 100644 index 00000000..14317a91 --- /dev/null +++ b/test/functional/datasets/UpdateTermsOfAccess.test.ts @@ -0,0 +1,51 @@ +import { TestConstants } from '../../testHelpers/TestConstants' +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { + createDataset, + DatasetNotNumberedVersion, + getDataset, + updateTermsOfAccess +} from '../../../src/datasets' +import { WriteError } from '../../../src' + +describe('UpdateTermsOfAccess (functional)', () => { + beforeAll(() => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should update terms of access with provided fields', async () => { + const ids = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await updateTermsOfAccess.execute(ids.numericId, { + fileAccessRequest: true, + termsOfAccessForRestrictedFiles: 'Your terms', + dataAccessPlace: 'Place' + }) + + const dataset = await getDataset.execute( + ids.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + + expect(dataset.termsOfUse.termsOfAccess.fileAccessRequest).toBe(true) + expect(dataset.termsOfUse.termsOfAccess.termsOfAccessForRestrictedFiles).toBe('Your terms') + expect(dataset.termsOfUse.termsOfAccess.dataAccessPlace).toBe('Place') + }) + + test('should throw when dataset does not exist', async () => { + await expect( + updateTermsOfAccess.execute(999999, { + fileAccessRequest: false + }) + ).rejects.toBeInstanceOf(WriteError) + }) +}) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 71b444bf..cc3b8176 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -27,7 +27,8 @@ import { addDatasetType, deleteDatasetType, linkDatasetTypeWithMetadataBlocks, - setAvailableLicensesForDatasetType + setAvailableLicensesForDatasetType, + updateTermsOfAccess } from '../../../src/datasets' import { ApiConfig, WriteError } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -36,7 +37,8 @@ import { Author, DatasetContact, DatasetDescription, - Publication + Publication, + TermsOfAccess } from '../../../src/datasets/domain/models/Dataset' import { createCollectionViaApi, @@ -1850,4 +1852,143 @@ describe('DatasetsRepository', () => { }) }) }) + + describe('updateTermsOfAccess', () => { + let testDatasetIds: CreatedDatasetIdentifiers + + console.log( + 'authentication', + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + beforeAll(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + }) + + test('should update the terms of access for a dataset', async () => { + const datasetBefore = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + + const termsOfAccessBefore: TermsOfAccess = { + fileAccessRequest: true, + termsOfAccessForRestrictedFiles: undefined, + dataAccessPlace: undefined, + originalArchive: undefined, + availabilityStatus: undefined, + contactForAccess: undefined, + sizeOfCollection: undefined, + studyCompletion: undefined + } + expect(datasetBefore.termsOfUse.termsOfAccess).toEqual(termsOfAccessBefore) + + const termsOfAccessAfter: TermsOfAccess = { + fileAccessRequest: false, + termsOfAccessForRestrictedFiles: 'Your terms of access for restricted files', + dataAccessPlace: 'Your data access place', + originalArchive: 'Your original archive', + availabilityStatus: 'Your availability status', + contactForAccess: 'Your contact for access', + sizeOfCollection: 'Your size of collection', + studyCompletion: 'Your study completion' + } + + await updateTermsOfAccess.execute(testDatasetIds.numericId, termsOfAccessAfter) + + const datasetAfter = await sut.getDataset( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + + expect(datasetAfter.termsOfUse.termsOfAccess).toEqual(termsOfAccessAfter) + }) + + test('should throw error when dataset does not exist', async () => { + const nonExistentId = 999999 + await expect( + updateTermsOfAccess.execute(nonExistentId, { + fileAccessRequest: true + }) + ).rejects.toBeInstanceOf(WriteError) + }) + + test('should accept only fileAccessRequest field', async () => { + const ids = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await updateTermsOfAccess.execute(ids.numericId, { + fileAccessRequest: false + }) + + const dataset = await sut.getDataset( + ids.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + + expect(dataset.termsOfUse.termsOfAccess.fileAccessRequest).toBe(false) + expect(dataset.termsOfUse.termsOfAccess.dataAccessPlace).toBeUndefined() + expect(dataset.termsOfUse.termsOfAccess.originalArchive).toBeUndefined() + expect(dataset.termsOfUse.termsOfAccess.availabilityStatus).toBeUndefined() + expect(dataset.termsOfUse.termsOfAccess.contactForAccess).toBeUndefined() + expect(dataset.termsOfUse.termsOfAccess.sizeOfCollection).toBeUndefined() + expect(dataset.termsOfUse.termsOfAccess.studyCompletion).toBeUndefined() + }) + + test('should work when identifying dataset by persistent id', async () => { + const ids = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await updateTermsOfAccess.execute(ids.persistentId, { + termsOfAccessForRestrictedFiles: 'Persistent terms', + fileAccessRequest: false + }) + + const dataset = await sut.getDataset( + ids.persistentId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + + expect(dataset.persistentId).toBe(ids.persistentId) + expect(dataset.termsOfUse.termsOfAccess.fileAccessRequest).toBe(false) + expect(dataset.termsOfUse.termsOfAccess.termsOfAccessForRestrictedFiles).toBe( + 'Persistent terms' + ) + }) + + test('should update terms on a published dataset (creates a draft)', async () => { + const ids = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await publishDataset.execute(ids.numericId, VersionUpdateType.MAJOR) + await waitForNoLocks(ids.numericId, 10) + + await updateTermsOfAccess.execute(ids.numericId, { + fileAccessRequest: true, + termsOfAccessForRestrictedFiles: 'Updated after publish' + }) + + await waitForNoLocks(ids.numericId, 10) + + const dataset = await sut.getDataset( + ids.numericId, + DatasetNotNumberedVersion.LATEST, + false, + false + ) + + expect(dataset.versionInfo.state).toBe('DRAFT') + expect(dataset.termsOfUse.termsOfAccess.termsOfAccessForRestrictedFiles).toBe( + 'Updated after publish' + ) + + await deletePublishedDatasetViaApi(ids.persistentId) + }) + }) }) diff --git a/test/unit/datasets/UpdateTermsOfAccess.test.ts b/test/unit/datasets/UpdateTermsOfAccess.test.ts new file mode 100644 index 00000000..ec713962 --- /dev/null +++ b/test/unit/datasets/UpdateTermsOfAccess.test.ts @@ -0,0 +1,34 @@ +import { WriteError } from '../../../src' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { UpdateTermsOfAccess } from '../../../src/datasets/domain/useCases/UpdateTermsOfAccess' +import { TermsOfAccess } from '../../../src/datasets/domain/models/Dataset' + +describe('UpdateTermsOfAccess (unit)', () => { + test('should return undefined on updating TermsOfAccess with repository success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.updateTermsOfAccess = jest.fn().mockResolvedValue(undefined) + + const sut = new UpdateTermsOfAccess(datasetsRepositoryStub) + const termsOfAccess: TermsOfAccess = { + fileAccessRequest: true, + termsOfAccessForRestrictedFiles: 'Your terms', + dataAccessPlace: 'Place' + } + + const actual = await sut.execute(1, termsOfAccess) + expect(actual).toEqual(undefined) + }) + + test('should return error result on updating TermsOfAccess with repository error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.updateTermsOfAccess = jest.fn().mockRejectedValue(new WriteError()) + const sut = new UpdateTermsOfAccess(datasetsRepositoryStub) + + const nonExistentDatasetId = 111111 + await expect( + sut.execute(nonExistentDatasetId, { + fileAccessRequest: true + }) + ).rejects.toThrow(WriteError) + }) +}) From cad29c2b5a04e2bcdaa566c8022c7efb504a4184 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 14 Oct 2025 10:54:25 -0400 Subject: [PATCH 022/110] Revert "feat: add pagination" This reverts commit cfe34ea764047b839a7e3f8b65e40a85076a4587. --- .../repositories/INotificationsRepository.ts | 7 +- .../useCases/GetAllNotificationsByUser.ts | 12 +--- .../repositories/NotificationsRepository.ts | 21 +----- .../NotificationsRepository.test.ts | 65 +------------------ 4 files changed, 8 insertions(+), 97 deletions(-) diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index dc8895ca..9392c543 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -1,12 +1,7 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { - getAllNotificationsByUser( - inAppNotificationFormat?: boolean, - onlyUnread?: boolean, - limit?: number, - offset?: number - ): Promise + getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise deleteNotification(notificationId: number): Promise getUnreadNotificationsCount(): Promise markNotificationAsRead(notificationId: number): Promise diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts index 14d3b813..43555ccc 100644 --- a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -11,17 +11,9 @@ export class GetAllNotificationsByUser implements UseCase { * @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( - inAppNotificationFormat?: boolean, - onlyUnread?: boolean, - limit?: number, - offset?: number - ): Promise { + async execute(inAppNotificationFormat?: boolean): Promise { return (await this.notificationsRepository.getAllNotificationsByUser( - inAppNotificationFormat, - onlyUnread, - limit, - offset + inAppNotificationFormat )) as Notification[] } } diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 196a3d9d..f310c34a 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -7,28 +7,13 @@ export class NotificationsRepository extends ApiRepository implements INotificat private readonly notificationsResourceName: string = 'notifications' public async getAllNotificationsByUser( - inAppNotificationFormat?: boolean, - onlyUnread?: boolean, - limit?: number, - offset?: number + inAppNotificationFormat?: boolean ): Promise { - const queryParams = new URLSearchParams() - if (inAppNotificationFormat) { - queryParams.set('inAppNotificationFormat', 'true') - } - if (onlyUnread) { - queryParams.set('onlyUnread', 'true') - } - if (typeof limit === 'number') { - queryParams.set('limit', limit.toString()) - } - if (typeof offset === 'number') { - queryParams.set('offset', offset.toString()) - } + const queryParams = inAppNotificationFormat ? { inAppNotificationFormat: 'true' } : undefined return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'all'), true, - queryParams.toString().length > 0 ? queryParams : undefined + queryParams ) .then((response) => { const notifications = response.data.data.notifications diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 5feb23b3..5333e48d 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 { - publishDatasetViaApi, - waitForNoLocks, - deletePublishedDatasetViaApi -} from '../../testHelpers/datasets/datasetHelper' +import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' import { createCollection } from '../../../src/collections' import { @@ -99,24 +95,7 @@ describe('NotificationsRepository', () => { expect(notification).toHaveProperty('displayAsRead') }) - test('should return only unread notifications when onlyUnread is true (if any exist)', async () => { - const notifications: Notification[] = await sut.getAllNotificationsByUser(true, true) - - expect(Array.isArray(notifications)).toBe(true) - expect(Array.isArray(notifications)).toBe(true) - expect(notifications.every((n) => n.displayAsRead === false)).toBe(true) - }) - - test('should paginate results using limit and offset', async () => { - const limit = 1 - const page1: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 0) - const page2: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 1) - - expect(page1.length).toBeLessThanOrEqual(limit) - expect(page2.length).toBeLessThanOrEqual(limit) - - // Always run the assertion, but only if both pages have one notification each - expect(page1.length !== 1 || page2.length !== 1 || page1[0].id !== page2[0].id).toBe(true) + test('should find notification with ASSIGNROLE type that has not been deleted', async () => { const notifications: Notification[] = await sut.getAllNotificationsByUser(true) const assignRoleNotification = notifications.find( @@ -137,46 +116,6 @@ describe('NotificationsRepository', () => { expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) - test('should generate 5+ notifications and verify pagination across pages', async () => { - const createdDatasets: CreatedDatasetIdentifiers[] = [] - try { - const howMany = 5 - for (let i = 0; i < howMany; i++) { - const ids = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) - createdDatasets.push(ids) - await publishDatasetViaApi(ids.numericId) - await waitForNoLocks(ids.numericId, 10) - } - - const limit = 5 - const page1: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 0) - const page2: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 5) - const page3: Notification[] = await sut.getAllNotificationsByUser(true, undefined, limit, 10) - expect(page1.length).toBeLessThanOrEqual(limit) - expect(page2.length).toBeLessThanOrEqual(limit) - expect(page3.length).toBeLessThanOrEqual(limit) - - const ids1 = new Set(page1.map((n) => n.id)) - const ids2 = new Set(page2.map((n) => n.id)) - const ids3 = new Set(page3.map((n) => n.id)) - - const intersects = (a: Set, b: Set): boolean => { - for (const x of a) { - if (b.has(x)) return true - } - return false - } - - expect(page1.length === 0 || page2.length === 0 || !intersects(ids1, ids2)).toBe(true) - expect(page1.length === 0 || page3.length === 0 || !intersects(ids1, ids3)).toBe(true) - expect(page2.length === 0 || page3.length === 0 || !intersects(ids2, ids3)).toBe(true) - } finally { - for (const d of createdDatasets) { - await deletePublishedDatasetViaApi(d.persistentId) - } - } - }) - test('should create a collection and find the notification with CREATEDV type', async () => { const testCollectionAlias = 'test-notification-collection' const createdCollectionId = await createCollection.execute( From e9423cd25bedc52467ce3b53c35b0e0841e319da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 14 Oct 2025 14:39:20 -0300 Subject: [PATCH 023/110] revert test environment variables --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 843482b6b17f8f90d4388ec08100eab1f2bd6a56 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 17 Oct 2025 13:29:40 -0400 Subject: [PATCH 024/110] feat: create use case for collection template --- CHANGELOG.md | 1 + docs/useCases.md | 36 +++++++++++ .../domain/dtos/TemplateCreateDTO.ts | 45 ++++++++++++++ .../repositories/ICollectionsRepository.ts | 2 + .../domain/useCases/CreateTemplate.ts | 26 ++++++++ src/collections/index.ts | 5 +- .../repositories/CollectionsRepository.ts | 15 +++++ .../collections/CreateTemplate.test.ts | 62 +++++++++++++++++++ .../collections/CollectionsRepository.test.ts | 62 ++++++++++++++++++- test/unit/collections/CreateTemplate.test.ts | 43 +++++++++++++ 10 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 src/collections/domain/dtos/TemplateCreateDTO.ts create mode 100644 src/collections/domain/useCases/CreateTemplate.ts create mode 100644 test/functional/collections/CreateTemplate.test.ts create mode 100644 test/unit/collections/CreateTemplate.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ea977c..c84c5d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added - New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). +- New Use Case: [Create a Template](./docs/useCases.md#create-a-template) under Collections. ### Changed diff --git a/docs/useCases.md b/docs/useCases.md index 6d5565fe..6ebb8dbe 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -18,6 +18,7 @@ The different use cases currently available in the package are classified below, - [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 Template](#create-a-template) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) - [Publish a Collection](#publish-a-collection) @@ -567,6 +568,41 @@ deleteCollectionFeaturedItem.execute(featuredItemId) _See [use case](../src/collections/domain/useCases/DeleteCollectionFeaturedItem.ts)_ definition. +#### Create a Template + +Creates a template for a given Dataverse collection id or alias. + +##### Example call: + +```typescript +import { createTemplate } from '@iqss/dataverse-client-javascript' +import { TemplateCreateDTO } from '@iqss/dataverse-client-javascript' + +const collectionAlias = ':root' +const template: TemplateCreateDTO = { + name: 'Dataverse template', + isDefault: true, + fields: [ + { + typeName: 'author', + typeClass: 'compound', + multiple: true, + value: [ + { + authorName: { typeName: 'authorName', value: 'Belicheck, Bill' }, + authorAffiliation: { typeName: 'authorIdentifierScheme', value: 'ORCID' } + } + ] + } + ], + instructions: [{ instructionField: 'author', instructionText: 'The author data' }] +} + +await createTemplate.execute(template, collectionAlias) +``` + +_See [use case](../src/collections/domain/useCases/CreateTemplate.ts) implementation_. + ## Datasets ### Datasets Read Use Cases diff --git a/src/collections/domain/dtos/TemplateCreateDTO.ts b/src/collections/domain/dtos/TemplateCreateDTO.ts new file mode 100644 index 00000000..e3fcccb8 --- /dev/null +++ b/src/collections/domain/dtos/TemplateCreateDTO.ts @@ -0,0 +1,45 @@ +import { MetadataFieldTypeClass } from '../../../metadataBlocks/domain/models/MetadataBlock' + +export interface TemplateCreateDTO { + name: string + isDefault: boolean | false // defaults to false if not provided + fields?: TemplateFieldDTO[] + instructions?: TemplateInstructionDTO[] +} + +export interface TemplateFieldDTO { + typeName: string + multiple: boolean + typeClass: MetadataFieldTypeClass + value: TemplateFieldValueDTO[] +} + +export interface TemplateFieldValueDTO { + [key: string]: + | TemplateFieldValuePrimitiveDTO + | TemplateFieldValueCompoundDTO + | TemplateFieldValueControlledVocabularyDTO +} + +export interface TemplateFieldValuePrimitiveDTO { + typeName: string + typeClass: MetadataFieldTypeClass.Primitive + value: string | string[] +} + +export interface TemplateFieldValueCompoundDTO { + typeName: string + typeClass: MetadataFieldTypeClass.Compound + value: TemplateFieldValueDTO[] +} + +export interface TemplateFieldValueControlledVocabularyDTO { + typeName: string + typeClass: MetadataFieldTypeClass.ControlledVocabulary + value: string +} + +export interface TemplateInstructionDTO { + instructionField: string + instructionText: string +} diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index bc8960c8..ded23958 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -12,6 +12,7 @@ import { CollectionItemType } from '../../../collections/domain/models/Collectio import { CollectionLinks } from '../models/CollectionLinks' import { CollectionSummary } from '../models/CollectionSummary' import { LinkingObjectType } from '../useCases/GetCollectionsForLinking' +import { TemplateCreateDTO } from '../dtos/TemplateCreateDTO' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -68,4 +69,5 @@ export interface ICollectionsRepository { searchTerm: string, alreadyLinked: boolean ): Promise + createTemplate(collectionIdOrAlias: number | string, template: TemplateCreateDTO): Promise } diff --git a/src/collections/domain/useCases/CreateTemplate.ts b/src/collections/domain/useCases/CreateTemplate.ts new file mode 100644 index 00000000..1a6b243c --- /dev/null +++ b/src/collections/domain/useCases/CreateTemplate.ts @@ -0,0 +1,26 @@ +import { ROOT_COLLECTION_ID } from '../models/Collection' +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' +import { TemplateCreateDTO } from '../dtos/TemplateCreateDTO' + +export class CreateTemplate implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Creates a Dataset Template in the specified collection. + * + * @param {TemplateCreateDTO} template - Template definition payload. + * @param {number | string} [collectionIdOrAlias = ':root'] - Target collection id or alias. + * @returns {Promise} + */ + async execute( + template: TemplateCreateDTO, + collectionIdOrAlias: number | string = ROOT_COLLECTION_ID + ): Promise { + return await this.collectionsRepository.createTemplate(collectionIdOrAlias, template) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 59e2e50b..6e74032d 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -16,6 +16,7 @@ import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking' +import { CreateTemplate } from './domain/useCases/CreateTemplate' const collectionsRepository = new CollectionsRepository() @@ -36,6 +37,7 @@ const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository) +const createTemplate = new CreateTemplate(collectionsRepository) export { getCollection, @@ -54,7 +56,8 @@ export { linkCollection, unlinkCollection, getCollectionLinks, - getCollectionsForLinking + getCollectionsForLinking, + createTemplate } 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 e0e459b0..1a71f061 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -40,6 +40,7 @@ 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' +import { TemplateCreateDTO } from '../../domain/dtos/TemplateCreateDTO' export interface NewCollectionRequestPayload { alias: string @@ -528,4 +529,18 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + + public async createTemplate( + collectionIdOrAlias: number | string, + template: TemplateCreateDTO + ): Promise { + return this.doPost( + `/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, + template + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/test/functional/collections/CreateTemplate.test.ts b/test/functional/collections/CreateTemplate.test.ts new file mode 100644 index 00000000..cd4486ab --- /dev/null +++ b/test/functional/collections/CreateTemplate.test.ts @@ -0,0 +1,62 @@ +import { ApiConfig } from '../../../src' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { getDatasetTemplates } from '../../../src/datasets' +import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' +import { createTemplate } from '../../../src/collections' +import { MetadataFieldTypeClass } from '../../../src/metadataBlocks/domain/models/MetadataBlock' +import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' + +describe('CreateTemplate.execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should create a template in :root with provided JSON', async () => { + const templateDto: TemplateCreateDTO = { + name: 'TestDataverse template', + isDefault: true, + fields: [ + { + typeName: 'author', + typeClass: MetadataFieldTypeClass.Compound, + multiple: true, + value: [ + { + authorName: { + typeName: 'authorName', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'Belicheck, Bill' + }, + authorAffiliation: { + typeName: 'authorIdentifierScheme', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'ORCID' + } + } + ] + } + ], + instructions: [ + { + instructionField: 'author', + instructionText: 'The author data' + } + ] + } + await createTemplate.execute(templateDto) + const templates = await getDatasetTemplates.execute(':root') + + expect(templates[templates.length - 1].name).toBe(templateDto.name) + expect(templates[templates.length - 1].isDefault).toBe(templateDto.isDefault) + expect(templates[templates.length - 1].instructions.length).toBe( + templateDto.instructions?.length ?? 0 + ) + + deleteDatasetTemplateViaApi(templates[templates.length - 1].id) + }) +}) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 1d6041b6..4cd43517 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -16,7 +16,9 @@ import { getDatasetFiles, restrictFile, deleteFile, - linkDataset + linkDataset, + createTemplate, + MetadataFieldTypeClass } from '../../../src' import { ApiConfig } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -58,6 +60,9 @@ import { DvObjectFeaturedItemDTO, FeaturedItemsDTO } from '../../../src/collections/domain/dtos/FeaturedItemsDTO' +import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' +import { getDatasetTemplates } from '../../../src/datasets' +import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' describe('CollectionsRepository', () => { const testCollectionAlias = 'collectionsRepositoryTestCollection' @@ -2143,4 +2148,59 @@ describe('CollectionsRepository', () => { await expect(sut.getCollectionLinks(invalidCollectionId)).rejects.toThrow(expectedError) }) }) + + describe('createTemplate', () => { + const templateDto: TemplateCreateDTO = { + name: 'CollectionsRepository template', + isDefault: true, + fields: [ + { + typeName: 'author', + typeClass: MetadataFieldTypeClass.Compound, + multiple: true, + value: [ + { + authorName: { + typeName: 'authorName', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'Belicheck, Bill' + }, + authorAffiliation: { + typeName: 'authorIdentifierScheme', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'ORCID' + } + } + ] + } + ], + instructions: [ + { + instructionField: 'author', + instructionText: 'The author data' + } + ] + } + test('should create a template in :root with provided JSON', async () => { + await createTemplate.execute(templateDto) + const templates = await getDatasetTemplates.execute(':root') + + expect(templates[templates.length - 1].name).toBe(templateDto.name) + expect(templates[templates.length - 1].isDefault).toBe(templateDto.isDefault) + expect(templates[templates.length - 1].instructions.length).toBe( + templateDto.instructions?.length ?? 0 + ) + + deleteDatasetTemplateViaApi(templates[templates.length - 1].id) + }) + + test('should return error when creating a template with invalidCollectionAlias', async () => { + const expectedError = new WriteError( + `[404] Can't find dataverse with identifier='invalidCollectionAlias'` + ) + await expect(createTemplate.execute(templateDto, 'invalidCollectionAlias')).rejects.toThrow( + expectedError + ) + }) + }) }) diff --git a/test/unit/collections/CreateTemplate.test.ts b/test/unit/collections/CreateTemplate.test.ts new file mode 100644 index 00000000..89c8d64b --- /dev/null +++ b/test/unit/collections/CreateTemplate.test.ts @@ -0,0 +1,43 @@ +import { CreateTemplate } from '../../../src/collections/domain/useCases/CreateTemplate' +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' +import { WriteError } from '../../../src' + +describe('execute', () => { + const testTemplateDTO = { name: 't' } as TemplateCreateDTO + const testCollectionId = 1 + + test('should return undefined when repository call is successful', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.createTemplate = jest.fn().mockResolvedValue(testCollectionId) + const sut = new CreateTemplate(collectionRepositoryStub) + + const actual = await sut.execute(testTemplateDTO) + + expect(collectionRepositoryStub.createTemplate).toHaveBeenCalledWith(':root', testTemplateDTO) + expect(actual).toEqual(testCollectionId) + }) + + test('should call repository with provided collection id/alias', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.createTemplate = jest.fn().mockResolvedValue(testCollectionId) + + const sut = new CreateTemplate(collectionRepositoryStub) + const actual = await sut.execute(testTemplateDTO, 'alias123') + + expect(collectionRepositoryStub.createTemplate).toHaveBeenCalledWith( + 'alias123', + testTemplateDTO + ) + + expect(actual).toEqual(testCollectionId) + }) + + test('should return error result on repository error', async () => { + const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionRepositoryStub.createTemplate = jest.fn().mockRejectedValue(new WriteError()) + const testCreateTemplate = new CreateTemplate(collectionRepositoryStub) + + await expect(testCreateTemplate.execute(testTemplateDTO)).rejects.toThrow(WriteError) + }) +}) From 8a0c6326b0e85e44a75c27d06f9af1c61c64359b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 20 Oct 2025 09:10:46 -0300 Subject: [PATCH 025/110] feat: introduce DatasetTypeDTO and remove optional from id property in DatasetType model --- src/datasets/domain/dtos/DatasetTypeDTO.ts | 3 +++ src/datasets/domain/models/DatasetType.ts | 2 +- src/datasets/domain/repositories/IDatasetsRepository.ts | 3 ++- src/datasets/domain/useCases/AddDatasetType.ts | 3 ++- src/datasets/index.ts | 1 + src/datasets/infra/repositories/DatasetsRepository.ts | 3 ++- 6 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 src/datasets/domain/dtos/DatasetTypeDTO.ts diff --git a/src/datasets/domain/dtos/DatasetTypeDTO.ts b/src/datasets/domain/dtos/DatasetTypeDTO.ts new file mode 100644 index 00000000..3f6b1c1b --- /dev/null +++ b/src/datasets/domain/dtos/DatasetTypeDTO.ts @@ -0,0 +1,3 @@ +import { DatasetType } from '../models/DatasetType' + +export type DatasetTypeDTO = Omit diff --git a/src/datasets/domain/models/DatasetType.ts b/src/datasets/domain/models/DatasetType.ts index 56a5ed43..5475cdaf 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 c8dc1f0f..91674966 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -14,6 +14,7 @@ import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' import { DatasetTemplate } from '../models/DatasetTemplate' import { DatasetType } from '../models/DatasetType' +import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO' export interface IDatasetsRepository { getDataset( @@ -80,7 +81,7 @@ export interface IDatasetsRepository { getDatasetTemplates(collectionIdOrAlias: number | string): Promise getDatasetAvailableDatasetTypes(): Promise getDatasetAvailableDatasetType(datasetTypeId: number | string): Promise - addDatasetType(datasetType: DatasetType): Promise + addDatasetType(datasetType: DatasetTypeDTO): Promise linkDatasetTypeWithMetadataBlocks( datasetTypeId: number | string, metadataBlocks: string[] diff --git a/src/datasets/domain/useCases/AddDatasetType.ts b/src/datasets/domain/useCases/AddDatasetType.ts index 7e2e11c9..84a47bc1 100644 --- a/src/datasets/domain/useCases/AddDatasetType.ts +++ b/src/datasets/domain/useCases/AddDatasetType.ts @@ -1,4 +1,5 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO' import { DatasetType } from '../models/DatasetType' import { IDatasetsRepository } from '../repositories/IDatasetsRepository' @@ -12,7 +13,7 @@ export class AddDatasetType implements UseCase { /** * Add a dataset type that can be selected when creating a dataset. */ - async execute(datasetType: DatasetType): Promise { + async execute(datasetType: DatasetTypeDTO): Promise { return await this.datasetsRepository.addDatasetType(datasetType) } } diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 6b93a7cd..1bd1de0e 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -145,3 +145,4 @@ export { } from './domain/models/DatasetVersionSummaryInfo' export { DatasetLinkedCollection } from './domain/models/DatasetLinkedCollection' export { DatasetType } from './domain/models/DatasetType' +export { DatasetTypeDTO } from './domain/dtos/DatasetTypeDTO' diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 41407964..f75bf1c0 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -29,6 +29,7 @@ import { DatasetTemplate } from '../../domain/models/DatasetTemplate' import { DatasetTemplatePayload } from './transformers/DatasetTemplatePayload' import { transformDatasetTemplatePayloadToDatasetTemplate } from './transformers/datasetTemplateTransformers' import { DatasetType } from '../../domain/models/DatasetType' +import { DatasetTypeDTO } from '../../domain/dtos/DatasetTypeDTO' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -416,7 +417,7 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - public async addDatasetType(datasetType: DatasetType): Promise { + public async addDatasetType(datasetType: DatasetTypeDTO): Promise { return this.doPost( this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes'), datasetType From 7e86b7608c576c04b03c23424718af3222c14127 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 21 Oct 2025 18:06:59 -0400 Subject: [PATCH 026/110] feat: add totalCount to results --- .../domain/models/NotificationSubset.ts | 6 ++ .../repositories/INotificationsRepository.ts | 4 +- .../useCases/GetAllNotificationsByUser.ts | 10 +-- .../repositories/NotificationsRepository.ts | 25 +++--- .../notifications/DeleteNotification.test.ts | 6 +- .../GetAllNotificationsByUser.test.ts | 17 ++-- .../NotificationsRepository.test.ts | 87 +++++++++++-------- 7 files changed, 95 insertions(+), 60 deletions(-) create mode 100644 src/notifications/domain/models/NotificationSubset.ts diff --git a/src/notifications/domain/models/NotificationSubset.ts b/src/notifications/domain/models/NotificationSubset.ts new file mode 100644 index 00000000..fe4644b6 --- /dev/null +++ b/src/notifications/domain/models/NotificationSubset.ts @@ -0,0 +1,6 @@ +import { Notification } from './Notification' + +export interface NotificationSubset { + notifications: Notification[] + totalNotificationCount: number +} diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index dc8895ca..6835f119 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -1,4 +1,4 @@ -import { Notification } from '../models/Notification' +import { NotificationSubset } from '../models/NotificationSubset' export interface INotificationsRepository { getAllNotificationsByUser( @@ -6,7 +6,7 @@ export interface INotificationsRepository { onlyUnread?: boolean, limit?: number, offset?: number - ): Promise + ): Promise deleteNotification(notificationId: number): Promise getUnreadNotificationsCount(): Promise markNotificationAsRead(notificationId: number): Promise diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts index ba53bc96..b4b2e6f6 100644 --- a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -1,8 +1,8 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' -import { Notification } from '../models/Notification' import { INotificationsRepository } from '../repositories/INotificationsRepository' +import { NotificationSubset } from '../models/NotificationSubset' -export class GetAllNotificationsByUser implements UseCase { +export class GetAllNotificationsByUser implements UseCase { constructor(private readonly notificationsRepository: INotificationsRepository) {} /** @@ -12,19 +12,19 @@ export class GetAllNotificationsByUser implements UseCase { * @param onlyUnread - Optional parameter to filter only unread notifications * @param limit - Optional parameter to limit the number of notifications returned * @param offset - Optional parameter to skip a number of notifications (for pagination) - * @returns {Promise} - A promise that resolves to an array of Notification instances. + * @returns {Promise} - A promise that resolves to an array of Notification instances. */ async execute( inAppNotificationFormat?: boolean, onlyUnread?: boolean, limit?: number, offset?: number - ): Promise { + ): Promise { return (await this.notificationsRepository.getAllNotificationsByUser( inAppNotificationFormat, onlyUnread, limit, offset - )) as Notification[] + )) as NotificationSubset } } diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index a2cf5c8d..662fb3a1 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -2,6 +2,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' +import { NotificationSubset } from '../../domain/models/NotificationSubset' export class NotificationsRepository extends ApiRepository implements INotificationsRepository { private readonly notificationsResourceName: string = 'notifications' @@ -11,22 +12,24 @@ export class NotificationsRepository extends ApiRepository implements INotificat onlyUnread?: boolean, limit?: number, offset?: number - ): Promise { - const queryParams: Record = {} - - if (inAppNotificationFormat) queryParams.inAppNotificationFormat = 'true' - if (onlyUnread) queryParams.onlyUnread = 'true' - if (limit !== undefined) queryParams.limit = limit - if (offset !== undefined) queryParams.offset = offset + ): Promise { + const queryParams = new URLSearchParams() + if (inAppNotificationFormat) queryParams.set('inAppNotificationFormat', 'true') + if (onlyUnread) queryParams.set('onlyUnread', 'true') + if (limit !== undefined) queryParams.set('limit', limit.toString()) + if (offset !== undefined) queryParams.set('offset', offset.toString()) + console.log('Fetching notifications with params:', queryParams.toString()) + console.log('keys:', Array.from(queryParams.keys())) + console.log('length:', Object.keys(queryParams).length) return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'all'), true, - Object.keys(queryParams).length ? queryParams : undefined + queryParams ) .then((response) => { - const notifications = response.data.data.notifications - return notifications.map((notification: NotificationPayload) => { + console.log('Notifications API response:', response.data) + const notifications = response.data.data.map((notification: NotificationPayload) => { const { dataverseDisplayName, dataverseAlias, ...restNotification } = notification return { ...restNotification, @@ -34,6 +37,8 @@ export class NotificationsRepository extends ApiRepository implements INotificat ...(dataverseAlias && { collectionAlias: dataverseAlias }) } }) as Notification[] + const totalNotificationCount = response.data.totalCount + return { notifications, totalNotificationCount } }) .catch((error) => { throw error diff --git a/test/functional/notifications/DeleteNotification.test.ts b/test/functional/notifications/DeleteNotification.test.ts index 093fa637..86ceab44 100644 --- a/test/functional/notifications/DeleteNotification.test.ts +++ b/test/functional/notifications/DeleteNotification.test.ts @@ -12,12 +12,14 @@ describe('execute', () => { }) test('should successfully delete a notification for authenticated user', async () => { - const notifications = await getAllNotificationsByUser.execute() + const notificationSubset = await getAllNotificationsByUser.execute() + const notifications = notificationSubset.notifications const notificationId = notifications[notifications.length - 1].id await deleteNotification.execute(notificationId) - const notificationsAfterDelete = await getAllNotificationsByUser.execute() + const notificationsAfterDeleteSubset = await getAllNotificationsByUser.execute() + const notificationsAfterDelete = notificationsAfterDeleteSubset.notifications expect(notificationsAfterDelete.length).toBe(notifications.length - 1) }) diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 08582ac2..1b41aa3c 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -1,7 +1,7 @@ -import { ApiConfig, getAllNotificationsByUser, Notification } from '../../../src' +import { ApiConfig, getAllNotificationsByUser } from '../../../src' import { TestConstants } from '../../testHelpers/TestConstants' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' - +import { NotificationSubset } from '../../../src/notifications/domain/models/NotificationSubset' describe('execute', () => { beforeEach(async () => { ApiConfig.init( @@ -12,14 +12,16 @@ describe('execute', () => { }) test('should successfully return notifications for authenticated user', async () => { - const notifications: Notification[] = await getAllNotificationsByUser.execute() + const result: NotificationSubset = await getAllNotificationsByUser.execute() + const notifications = result.notifications 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() + const result: NotificationSubset = await getAllNotificationsByUser.execute() + const notifications = result.notifications expect(notifications[0]).toHaveProperty('id') expect(notifications[0]).toHaveProperty('type') @@ -27,15 +29,18 @@ describe('execute', () => { }) test('should have correct in-app notification properties when inAppNotificationFormat is true', async () => { - const notifications = await getAllNotificationsByUser.execute(true) + const result: NotificationSubset = await getAllNotificationsByUser.execute(true) + const notifications = result.notifications expect(notifications[0]).toHaveProperty('id') expect(notifications[0]).toHaveProperty('type') expect(notifications[0]).toHaveProperty('sentTimestamp') expect(notifications[0]).toHaveProperty('displayAsRead') }) + test('should have correct in-app notification properties when filter and paging params are set', async () => { - const notifications = await getAllNotificationsByUser.execute(true, true, 1, 0) + const result: NotificationSubset = await getAllNotificationsByUser.execute(true, true, 1, 0) + const notifications = result.notifications expect(notifications[0]).toHaveProperty('id') expect(notifications[0]).toHaveProperty('type') diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 08659a53..a93a1a5b 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -16,6 +16,7 @@ import { createCollectionDTO, deleteCollectionViaApi } from '../../testHelpers/collections/collectionHelper' +import { NotificationSubset } from '../../../src/notifications/domain/models/NotificationSubset' describe('NotificationsRepository', () => { const sut: NotificationsRepository = new NotificationsRepository() @@ -36,12 +37,12 @@ describe('NotificationsRepository', () => { await publishDatasetViaApi(testDatasetIds.numericId) await waitForNoLocks(testDatasetIds.numericId, 10) - const notifications: Notification[] = await sut.getAllNotificationsByUser() + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser() - expect(Array.isArray(notifications)).toBe(true) - expect(notifications.length).toBeGreaterThan(0) + expect(Array.isArray(notificationSubset.notifications)).toBe(true) + expect(notificationSubset.notifications.length).toBeGreaterThan(0) - const publishedNotification = notifications.find( + const publishedNotification = notificationSubset.notifications.find( (n) => n.type === NotificationType.PUBLISHEDDS ) as Notification @@ -62,14 +63,14 @@ describe('NotificationsRepository', () => { }) test('should delete a notification by ID', async () => { - const notifications: Notification[] = await sut.getAllNotificationsByUser() + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser() - const notificationToDelete = notifications[0] + const notificationToDelete = notificationSubset.notifications[0] await sut.deleteNotification(notificationToDelete.id) - const notificationsAfterDelete: Notification[] = await sut.getAllNotificationsByUser() - const deletedNotification = notificationsAfterDelete.find( + const notificationsAfterDelete: NotificationSubset = await sut.getAllNotificationsByUser() + const deletedNotification = notificationsAfterDelete.notifications.find( (n) => n.id === notificationToDelete.id ) expect(deletedNotification).toBeUndefined() @@ -86,9 +87,9 @@ describe('NotificationsRepository', () => { }) test('should return notifications with basic properties when inAppNotificationFormat is true', async () => { - const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser(true) - const notification = notifications[0] + const notification = notificationSubset.notifications[0] expect(notification).toHaveProperty('id') expect(notification).toHaveProperty('type') expect(notification).toHaveProperty('sentTimestamp') @@ -96,9 +97,9 @@ describe('NotificationsRepository', () => { }) test('should find notification with ASSIGNROLE type that has not been deleted', async () => { - const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser(true) - const assignRoleNotification = notifications.find( + const assignRoleNotification = notificationSubset.notifications.find( (n) => n.type === NotificationType.ASSIGNROLE && !n.objectDeleted ) @@ -106,7 +107,6 @@ describe('NotificationsRepository', () => { expect(assignRoleNotification?.type).toBe(NotificationType.ASSIGNROLE) expect(assignRoleNotification?.sentTimestamp).toBeDefined() expect(assignRoleNotification?.displayAsRead).toBeDefined() - expect(assignRoleNotification?.collectionDisplayName).toBeDefined() expect(assignRoleNotification?.roleAssignments).toBeDefined() expect(assignRoleNotification?.roleAssignments?.length).toBeGreaterThan(0) @@ -125,11 +125,11 @@ describe('NotificationsRepository', () => { 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 notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser(true, true) + expect(Array.isArray(notificationSubset.notifications)).toBe(true) + expect(notificationSubset.notifications.length).toBeGreaterThan(0) - const createdvNotification = notifications.find( + const createdvNotification = notificationSubset.notifications.find( (n) => n.collectionAlias === testCollectionAlias ) @@ -145,9 +145,9 @@ describe('NotificationsRepository', () => { }) test('should return array when inAppNotificationFormat is false', async () => { - const notifications: Notification[] = await sut.getAllNotificationsByUser(false) + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser(false) - expect(Array.isArray(notifications)).toBe(true) + expect(Array.isArray(notificationSubset.notifications)).toBe(true) }) test('should return unread count', async () => { @@ -158,16 +158,18 @@ describe('NotificationsRepository', () => { }) test('should mark notification as read successfully', async () => { - const notifications: Notification[] = await sut.getAllNotificationsByUser() + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser() - expect(notifications.length).toBeGreaterThan(0) + expect(notificationSubset.notifications.length).toBeGreaterThan(0) - const unreadNotification = notifications[0] + const unreadNotification = notificationSubset.notifications[0] await expect(sut.markNotificationAsRead(unreadNotification.id)).resolves.toBeUndefined() - const updatedNotifications: Notification[] = await sut.getAllNotificationsByUser() - const updatedNotification = updatedNotifications.find((n) => n.id === unreadNotification.id) + const updatedNotificationSubset: NotificationSubset = await sut.getAllNotificationsByUser() + const updatedNotification = updatedNotificationSubset.notifications.find( + (n) => n.id === unreadNotification.id + ) expect(updatedNotification?.displayAsRead).toBe(true) }) @@ -184,25 +186,40 @@ describe('NotificationsRepository', () => { ) }) test('should only return unread notifications when onlyUnread is true', async () => { - const notifications: Notification[] = await sut.getAllNotificationsByUser(true, true) + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser(true, true) - expect(Array.isArray(notifications)).toBe(true) - const originalUnreadCount = notifications.length - expect(notifications.length).toBeGreaterThanOrEqual(0) + expect(Array.isArray(notificationSubset.notifications)).toBe(true) + const originalUnreadCount = notificationSubset.totalNotificationCount + expect(notificationSubset.notifications.length).toBeGreaterThanOrEqual(0) - await expect(sut.markNotificationAsRead(notifications[0].id)).resolves.toBeUndefined() + await expect( + sut.markNotificationAsRead(notificationSubset.notifications[0].id) + ).resolves.toBeUndefined() - const updatedNotifications: Notification[] = await sut.getAllNotificationsByUser(true, true) - expect(updatedNotifications.length).toBe(originalUnreadCount - 1) + const updatedNotifications: NotificationSubset = await sut.getAllNotificationsByUser( + true, + true, + 10, + 0 + ) + expect(updatedNotifications.totalNotificationCount).toBe(originalUnreadCount - 1) - const hasReadNotifications = notifications.some((n) => n.displayAsRead === true) + const hasReadNotifications = notificationSubset.notifications.some( + (n) => n.displayAsRead === true + ) expect(hasReadNotifications).toBe(false) }) test('should return limited number of notifications when limit is set', async () => { const limit = 1 - const notifications: Notification[] = await sut.getAllNotificationsByUser(true, false, limit, 0) + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser( + true, + false, + limit, + 0 + ) - expect(Array.isArray(notifications)).toBe(true) - expect(notifications.length).toBeLessThanOrEqual(limit) + expect(Array.isArray(notificationSubset.notifications)).toBe(true) + expect(notificationSubset.notifications.length).toBeLessThanOrEqual(limit) + expect(notificationSubset.totalNotificationCount).toBeGreaterThanOrEqual(limit) }) }) From fdaa4a62ee12cc7f3bd677f9bcf62a0d7632aa73 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 22 Oct 2025 16:58:30 -0400 Subject: [PATCH 027/110] fix: making changes based on reviews --- docs/useCases.md | 12 +++++------ .../domain/dtos/TemplateCreateDTO.ts | 6 +++--- .../repositories/ICollectionsRepository.ts | 5 ++++- ...teTemplate.ts => CreateDatasetTemplate.ts} | 7 ++++--- src/collections/index.ts | 6 +++--- .../repositories/CollectionsRepository.ts | 2 +- ....test.ts => createDatasetTemplate.test.ts} | 4 ++-- .../collections/CollectionsRepository.test.ts | 12 +++++------ ...plate.test.ts => createDatasetTemplate.ts} | 21 +++++++++++-------- 9 files changed, 41 insertions(+), 34 deletions(-) rename src/collections/domain/useCases/{CreateTemplate.ts => CreateDatasetTemplate.ts} (69%) rename test/functional/collections/{CreateTemplate.test.ts => createDatasetTemplate.test.ts} (94%) rename test/unit/collections/{CreateTemplate.test.ts => createDatasetTemplate.ts} (60%) diff --git a/docs/useCases.md b/docs/useCases.md index 6ebb8dbe..d0dedb42 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -18,7 +18,7 @@ The different use cases currently available in the package are classified below, - [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 Template](#create-a-template) + - [Create a Dataset Template](#create-a-dataset-template) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) - [Publish a Collection](#publish-a-collection) @@ -568,14 +568,14 @@ deleteCollectionFeaturedItem.execute(featuredItemId) _See [use case](../src/collections/domain/useCases/DeleteCollectionFeaturedItem.ts)_ definition. -#### Create a Template +#### Create a Dataset Template -Creates a template for a given Dataverse collection id or alias. +Creates a dataset template for a given Dataverse collection id or alias. ##### Example call: ```typescript -import { createTemplate } from '@iqss/dataverse-client-javascript' +import { createDatasetTemplate } from '@iqss/dataverse-client-javascript' import { TemplateCreateDTO } from '@iqss/dataverse-client-javascript' const collectionAlias = ':root' @@ -598,10 +598,10 @@ const template: TemplateCreateDTO = { instructions: [{ instructionField: 'author', instructionText: 'The author data' }] } -await createTemplate.execute(template, collectionAlias) +await createDatasetTemplate.execute(template, collectionAlias) ``` -_See [use case](../src/collections/domain/useCases/CreateTemplate.ts) implementation_. +_See [use case](../src/collections/domain/useCases/CreateDatasetTemplate.ts) implementation_. ## Datasets diff --git a/src/collections/domain/dtos/TemplateCreateDTO.ts b/src/collections/domain/dtos/TemplateCreateDTO.ts index e3fcccb8..a8b729b1 100644 --- a/src/collections/domain/dtos/TemplateCreateDTO.ts +++ b/src/collections/domain/dtos/TemplateCreateDTO.ts @@ -2,7 +2,7 @@ import { MetadataFieldTypeClass } from '../../../metadataBlocks/domain/models/Me export interface TemplateCreateDTO { name: string - isDefault: boolean | false // defaults to false if not provided + isDefault?: boolean fields?: TemplateFieldDTO[] instructions?: TemplateInstructionDTO[] } @@ -10,8 +10,8 @@ export interface TemplateCreateDTO { export interface TemplateFieldDTO { typeName: string multiple: boolean - typeClass: MetadataFieldTypeClass - value: TemplateFieldValueDTO[] + typeClass?: MetadataFieldTypeClass + value?: TemplateFieldValueDTO[] } export interface TemplateFieldValueDTO { diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index ded23958..432749a0 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -69,5 +69,8 @@ export interface ICollectionsRepository { searchTerm: string, alreadyLinked: boolean ): Promise - createTemplate(collectionIdOrAlias: number | string, template: TemplateCreateDTO): Promise + createDatasetTemplate( + collectionIdOrAlias: number | string, + template: TemplateCreateDTO + ): Promise } diff --git a/src/collections/domain/useCases/CreateTemplate.ts b/src/collections/domain/useCases/CreateDatasetTemplate.ts similarity index 69% rename from src/collections/domain/useCases/CreateTemplate.ts rename to src/collections/domain/useCases/CreateDatasetTemplate.ts index 1a6b243c..4236332d 100644 --- a/src/collections/domain/useCases/CreateTemplate.ts +++ b/src/collections/domain/useCases/CreateDatasetTemplate.ts @@ -3,7 +3,7 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { ICollectionsRepository } from '../repositories/ICollectionsRepository' import { TemplateCreateDTO } from '../dtos/TemplateCreateDTO' -export class CreateTemplate implements UseCase { +export class CreateDatasetTemplate implements UseCase { private collectionsRepository: ICollectionsRepository constructor(collectionsRepository: ICollectionsRepository) { @@ -14,13 +14,14 @@ export class CreateTemplate implements UseCase { * Creates a Dataset Template in the specified collection. * * @param {TemplateCreateDTO} template - Template definition payload. - * @param {number | string} [collectionIdOrAlias = ':root'] - Target collection id 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( template: TemplateCreateDTO, collectionIdOrAlias: number | string = ROOT_COLLECTION_ID ): Promise { - return await this.collectionsRepository.createTemplate(collectionIdOrAlias, template) + return await this.collectionsRepository.createDatasetTemplate(collectionIdOrAlias, template) } } diff --git a/src/collections/index.ts b/src/collections/index.ts index 6e74032d..df7b6af5 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -16,7 +16,7 @@ import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking' -import { CreateTemplate } from './domain/useCases/CreateTemplate' +import { CreateDatasetTemplate } from './domain/useCases/CreateDatasetTemplate' const collectionsRepository = new CollectionsRepository() @@ -37,7 +37,7 @@ const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository) -const createTemplate = new CreateTemplate(collectionsRepository) +const createDatasetTemplate = new CreateDatasetTemplate(collectionsRepository) export { getCollection, @@ -57,7 +57,7 @@ export { unlinkCollection, getCollectionLinks, getCollectionsForLinking, - createTemplate + createDatasetTemplate } 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 1a71f061..62c8d936 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -530,7 +530,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections }) } - public async createTemplate( + public async createDatasetTemplate( collectionIdOrAlias: number | string, template: TemplateCreateDTO ): Promise { diff --git a/test/functional/collections/CreateTemplate.test.ts b/test/functional/collections/createDatasetTemplate.test.ts similarity index 94% rename from test/functional/collections/CreateTemplate.test.ts rename to test/functional/collections/createDatasetTemplate.test.ts index cd4486ab..bf89e319 100644 --- a/test/functional/collections/CreateTemplate.test.ts +++ b/test/functional/collections/createDatasetTemplate.test.ts @@ -3,7 +3,7 @@ import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ import { TestConstants } from '../../testHelpers/TestConstants' import { getDatasetTemplates } from '../../../src/datasets' import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' -import { createTemplate } from '../../../src/collections' +import { createDatasetTemplate } from '../../../src/collections' import { MetadataFieldTypeClass } from '../../../src/metadataBlocks/domain/models/MetadataBlock' import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' @@ -48,7 +48,7 @@ describe('CreateTemplate.execute', () => { } ] } - await createTemplate.execute(templateDto) + await createDatasetTemplate.execute(templateDto) const templates = await getDatasetTemplates.execute(':root') expect(templates[templates.length - 1].name).toBe(templateDto.name) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 4cd43517..b82afca7 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -17,7 +17,7 @@ import { restrictFile, deleteFile, linkDataset, - createTemplate, + createDatasetTemplate, MetadataFieldTypeClass } from '../../../src' import { ApiConfig } from '../../../src' @@ -2149,7 +2149,7 @@ describe('CollectionsRepository', () => { }) }) - describe('createTemplate', () => { + describe('createDatasetTemplate', () => { const templateDto: TemplateCreateDTO = { name: 'CollectionsRepository template', isDefault: true, @@ -2182,7 +2182,7 @@ describe('CollectionsRepository', () => { ] } test('should create a template in :root with provided JSON', async () => { - await createTemplate.execute(templateDto) + await createDatasetTemplate.execute(templateDto) const templates = await getDatasetTemplates.execute(':root') expect(templates[templates.length - 1].name).toBe(templateDto.name) @@ -2198,9 +2198,9 @@ describe('CollectionsRepository', () => { const expectedError = new WriteError( `[404] Can't find dataverse with identifier='invalidCollectionAlias'` ) - await expect(createTemplate.execute(templateDto, 'invalidCollectionAlias')).rejects.toThrow( - expectedError - ) + await expect( + createDatasetTemplate.execute(templateDto, 'invalidCollectionAlias') + ).rejects.toThrow(expectedError) }) }) }) diff --git a/test/unit/collections/CreateTemplate.test.ts b/test/unit/collections/createDatasetTemplate.ts similarity index 60% rename from test/unit/collections/CreateTemplate.test.ts rename to test/unit/collections/createDatasetTemplate.ts index 89c8d64b..6647bed6 100644 --- a/test/unit/collections/CreateTemplate.test.ts +++ b/test/unit/collections/createDatasetTemplate.ts @@ -1,4 +1,4 @@ -import { CreateTemplate } from '../../../src/collections/domain/useCases/CreateTemplate' +import { CreateDatasetTemplate } from '../../../src/collections/domain/useCases/CreateDatasetTemplate' import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' import { WriteError } from '../../../src' @@ -9,23 +9,26 @@ describe('execute', () => { test('should return undefined when repository call is successful', async () => { const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository - collectionRepositoryStub.createTemplate = jest.fn().mockResolvedValue(testCollectionId) - const sut = new CreateTemplate(collectionRepositoryStub) + collectionRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(testCollectionId) + const sut = new CreateDatasetTemplate(collectionRepositoryStub) const actual = await sut.execute(testTemplateDTO) - expect(collectionRepositoryStub.createTemplate).toHaveBeenCalledWith(':root', testTemplateDTO) + expect(collectionRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith( + ':root', + testTemplateDTO + ) expect(actual).toEqual(testCollectionId) }) test('should call repository with provided collection id/alias', async () => { const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository - collectionRepositoryStub.createTemplate = jest.fn().mockResolvedValue(testCollectionId) + collectionRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(testCollectionId) - const sut = new CreateTemplate(collectionRepositoryStub) + const sut = new CreateDatasetTemplate(collectionRepositoryStub) const actual = await sut.execute(testTemplateDTO, 'alias123') - expect(collectionRepositoryStub.createTemplate).toHaveBeenCalledWith( + expect(collectionRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith( 'alias123', testTemplateDTO ) @@ -35,8 +38,8 @@ describe('execute', () => { test('should return error result on repository error', async () => { const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository - collectionRepositoryStub.createTemplate = jest.fn().mockRejectedValue(new WriteError()) - const testCreateTemplate = new CreateTemplate(collectionRepositoryStub) + collectionRepositoryStub.createDatasetTemplate = jest.fn().mockRejectedValue(new WriteError()) + const testCreateTemplate = new CreateDatasetTemplate(collectionRepositoryStub) await expect(testCreateTemplate.execute(testTemplateDTO)).rejects.toThrow(WriteError) }) From a4206ddbda763868f2293e58b2a290bc1730cba5 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 22 Oct 2025 17:05:26 -0400 Subject: [PATCH 028/110] chore: move the line in usecase.md --- docs/useCases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/useCases.md b/docs/useCases.md index d0dedb42..5ec382c6 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -18,7 +18,6 @@ The different use cases currently available in the package are classified below, - [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 Dataset Template](#create-a-dataset-template) - [Create a Collection](#create-a-collection) - [Update a Collection](#update-a-collection) - [Publish a Collection](#publish-a-collection) @@ -26,6 +25,7 @@ The different use cases currently available in the package are classified below, - [Update Collection Featured Items](#update-collection-featured-items) - [Delete Collection Featured Items](#delete-collection-featured-items) - [Delete a Collection Featured Item](#delete-a-collection-featured-item) + - [Create a Dataset Template](#create-a-dataset-template) - [Datasets](#Datasets) - [Datasets read use cases](#datasets-read-use-cases) - [Get a Dataset](#get-a-dataset) From 7c9a7cb368d56957800b6cfaf76bc60f5a1364f0 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 22 Oct 2025 17:08:14 -0400 Subject: [PATCH 029/110] chore: renaming dto and revise changelog --- CHANGELOG.md | 2 +- .../{TemplateCreateDTO.ts => CreateDatasetTemplateDTO.ts} | 2 +- .../domain/repositories/ICollectionsRepository.ts | 4 ++-- src/collections/domain/useCases/CreateDatasetTemplate.ts | 6 +++--- src/collections/infra/repositories/CollectionsRepository.ts | 4 ++-- test/functional/collections/createDatasetTemplate.test.ts | 4 ++-- test/integration/collections/CollectionsRepository.test.ts | 4 ++-- test/unit/collections/createDatasetTemplate.ts | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) rename src/collections/domain/dtos/{TemplateCreateDTO.ts => CreateDatasetTemplateDTO.ts} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c84c5d99..906a6918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added - New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). -- New Use Case: [Create a Template](./docs/useCases.md#create-a-template) under Collections. +- New Use Case: [Create a Dataset Template](./docs/useCases.md#create-a-dataset-template) under Collections. ### Changed diff --git a/src/collections/domain/dtos/TemplateCreateDTO.ts b/src/collections/domain/dtos/CreateDatasetTemplateDTO.ts similarity index 96% rename from src/collections/domain/dtos/TemplateCreateDTO.ts rename to src/collections/domain/dtos/CreateDatasetTemplateDTO.ts index a8b729b1..52ba24cc 100644 --- a/src/collections/domain/dtos/TemplateCreateDTO.ts +++ b/src/collections/domain/dtos/CreateDatasetTemplateDTO.ts @@ -1,6 +1,6 @@ import { MetadataFieldTypeClass } from '../../../metadataBlocks/domain/models/MetadataBlock' -export interface TemplateCreateDTO { +export interface CreateDatasetTemplateDTO { name: string isDefault?: boolean fields?: TemplateFieldDTO[] diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index 432749a0..cae28415 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -12,7 +12,7 @@ import { CollectionItemType } from '../../../collections/domain/models/Collectio import { CollectionLinks } from '../models/CollectionLinks' import { CollectionSummary } from '../models/CollectionSummary' import { LinkingObjectType } from '../useCases/GetCollectionsForLinking' -import { TemplateCreateDTO } from '../dtos/TemplateCreateDTO' +import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -71,6 +71,6 @@ export interface ICollectionsRepository { ): Promise createDatasetTemplate( collectionIdOrAlias: number | string, - template: TemplateCreateDTO + template: CreateDatasetTemplateDTO ): Promise } diff --git a/src/collections/domain/useCases/CreateDatasetTemplate.ts b/src/collections/domain/useCases/CreateDatasetTemplate.ts index 4236332d..ffb443f6 100644 --- a/src/collections/domain/useCases/CreateDatasetTemplate.ts +++ b/src/collections/domain/useCases/CreateDatasetTemplate.ts @@ -1,7 +1,7 @@ import { ROOT_COLLECTION_ID } from '../models/Collection' import { UseCase } from '../../../core/domain/useCases/UseCase' import { ICollectionsRepository } from '../repositories/ICollectionsRepository' -import { TemplateCreateDTO } from '../dtos/TemplateCreateDTO' +import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO' export class CreateDatasetTemplate implements UseCase { private collectionsRepository: ICollectionsRepository @@ -13,13 +13,13 @@ export class CreateDatasetTemplate implements UseCase { /** * Creates a Dataset Template in the specified collection. * - * @param {TemplateCreateDTO} template - Template definition payload. + * @param {CreateDatasetTemplateDTO} template - Template definition payload. * @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( - template: TemplateCreateDTO, + template: CreateDatasetTemplateDTO, collectionIdOrAlias: number | string = ROOT_COLLECTION_ID ): Promise { return await this.collectionsRepository.createDatasetTemplate(collectionIdOrAlias, template) diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index 62c8d936..53ebfff3 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -40,7 +40,7 @@ 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' -import { TemplateCreateDTO } from '../../domain/dtos/TemplateCreateDTO' +import { CreateDatasetTemplateDTO } from '../../domain/dtos/CreateDatasetTemplateDTO' export interface NewCollectionRequestPayload { alias: string @@ -532,7 +532,7 @@ export class CollectionsRepository extends ApiRepository implements ICollections public async createDatasetTemplate( collectionIdOrAlias: number | string, - template: TemplateCreateDTO + template: CreateDatasetTemplateDTO ): Promise { return this.doPost( `/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, diff --git a/test/functional/collections/createDatasetTemplate.test.ts b/test/functional/collections/createDatasetTemplate.test.ts index bf89e319..84309e52 100644 --- a/test/functional/collections/createDatasetTemplate.test.ts +++ b/test/functional/collections/createDatasetTemplate.test.ts @@ -2,7 +2,7 @@ import { ApiConfig } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' import { TestConstants } from '../../testHelpers/TestConstants' import { getDatasetTemplates } from '../../../src/datasets' -import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' +import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO' import { createDatasetTemplate } from '../../../src/collections' import { MetadataFieldTypeClass } from '../../../src/metadataBlocks/domain/models/MetadataBlock' import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' @@ -17,7 +17,7 @@ describe('CreateTemplate.execute', () => { }) test('should create a template in :root with provided JSON', async () => { - const templateDto: TemplateCreateDTO = { + const templateDto: CreateDatasetTemplateDTO = { name: 'TestDataverse template', isDefault: true, fields: [ diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index b82afca7..c5828eb2 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -60,7 +60,7 @@ import { DvObjectFeaturedItemDTO, FeaturedItemsDTO } from '../../../src/collections/domain/dtos/FeaturedItemsDTO' -import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' +import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO' import { getDatasetTemplates } from '../../../src/datasets' import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' @@ -2150,7 +2150,7 @@ describe('CollectionsRepository', () => { }) describe('createDatasetTemplate', () => { - const templateDto: TemplateCreateDTO = { + const templateDto: CreateDatasetTemplateDTO = { name: 'CollectionsRepository template', isDefault: true, fields: [ diff --git a/test/unit/collections/createDatasetTemplate.ts b/test/unit/collections/createDatasetTemplate.ts index 6647bed6..0004b7a7 100644 --- a/test/unit/collections/createDatasetTemplate.ts +++ b/test/unit/collections/createDatasetTemplate.ts @@ -1,10 +1,10 @@ import { CreateDatasetTemplate } from '../../../src/collections/domain/useCases/CreateDatasetTemplate' import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' -import { TemplateCreateDTO } from '../../../src/collections/domain/dtos/TemplateCreateDTO' +import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO' import { WriteError } from '../../../src' describe('execute', () => { - const testTemplateDTO = { name: 't' } as TemplateCreateDTO + const testTemplateDTO = { name: 't' } as CreateDatasetTemplateDTO const testCollectionId = 1 test('should return undefined when repository call is successful', async () => { From 8d571f35f5458812e7607de743d1f43cec716ff6 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 22 Oct 2025 20:43:45 -0400 Subject: [PATCH 030/110] fix tests and remove logs --- .../repositories/NotificationsRepository.ts | 4 ---- .../notifications/DeleteNotification.test.ts | 3 ++- .../NotificationsRepository.test.ts | 20 +++++++------------ 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 662fb3a1..99e37c82 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -19,16 +19,12 @@ export class NotificationsRepository extends ApiRepository implements INotificat if (onlyUnread) queryParams.set('onlyUnread', 'true') if (limit !== undefined) queryParams.set('limit', limit.toString()) if (offset !== undefined) queryParams.set('offset', offset.toString()) - console.log('Fetching notifications with params:', queryParams.toString()) - console.log('keys:', Array.from(queryParams.keys())) - console.log('length:', Object.keys(queryParams).length) return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'all'), true, queryParams ) .then((response) => { - console.log('Notifications API response:', response.data) const notifications = response.data.data.map((notification: NotificationPayload) => { const { dataverseDisplayName, dataverseAlias, ...restNotification } = notification return { diff --git a/test/functional/notifications/DeleteNotification.test.ts b/test/functional/notifications/DeleteNotification.test.ts index 86ceab44..5902013c 100644 --- a/test/functional/notifications/DeleteNotification.test.ts +++ b/test/functional/notifications/DeleteNotification.test.ts @@ -20,7 +20,8 @@ describe('execute', () => { const notificationsAfterDeleteSubset = await getAllNotificationsByUser.execute() const notificationsAfterDelete = notificationsAfterDeleteSubset.notifications - expect(notificationsAfterDelete.length).toBe(notifications.length - 1) + const deletedExists = notificationsAfterDelete.some((n) => n.id === notificationId) + expect(deletedExists).toBe(false) }) test('should throw an error when the notification id does not exist', async () => { diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index a93a1a5b..f9557202 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -189,22 +189,16 @@ describe('NotificationsRepository', () => { const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser(true, true) expect(Array.isArray(notificationSubset.notifications)).toBe(true) - const originalUnreadCount = notificationSubset.totalNotificationCount expect(notificationSubset.notifications.length).toBeGreaterThanOrEqual(0) + const notificationToMarkRead = notificationSubset.notifications[0] + await expect(sut.markNotificationAsRead(notificationToMarkRead.id)).resolves.toBeUndefined() - await expect( - sut.markNotificationAsRead(notificationSubset.notifications[0].id) - ).resolves.toBeUndefined() - - const updatedNotifications: NotificationSubset = await sut.getAllNotificationsByUser( - true, - true, - 10, - 0 + const updatedNotifications: NotificationSubset = await sut.getAllNotificationsByUser(true, true) + const stillPresent = updatedNotifications.notifications.some( + (n) => n.id === notificationToMarkRead.id ) - expect(updatedNotifications.totalNotificationCount).toBe(originalUnreadCount - 1) - - const hasReadNotifications = notificationSubset.notifications.some( + expect(stillPresent).toBe(false) + const hasReadNotifications = updatedNotifications.notifications.some( (n) => n.displayAsRead === true ) expect(hasReadNotifications).toBe(false) From 4ca712ffd86c7832585a219ccc741115ebd4eff4 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 23 Oct 2025 16:23:47 -0400 Subject: [PATCH 031/110] add pagination query para --- docs/useCases.md | 7 ++- .../repositories/IDatasetsRepository.ts | 6 +- .../useCases/GetDatasetVersionsSummaries.ts | 10 +++- .../infra/repositories/DatasetsRepository.ts | 12 +++- .../domain/repositories/IFilesRepository.ts | 6 +- .../useCases/GetFileVersionSummaries.ts | 10 +++- .../infra/repositories/FilesRepository.ts | 14 ++++- .../datasets/DatasetsRepository.test.ts | 59 +++++++++++++++++++ .../integration/files/FilesRepository.test.ts | 52 ++++++++++++++++ 9 files changed, 165 insertions(+), 11 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index 6d5565fe..4d387ad9 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -858,7 +858,9 @@ getDatasetVersionsSummaries _See [use case](../src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts) implementation_. -The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. +- The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. +- **limit**: (number) Limit for pagination. +- **offset**: (number) Offset for pagination. #### Get Dataset Linked Collections @@ -1984,6 +1986,9 @@ getFileVersionSummaries.execute(fileId).then((fileVersionSummaries: fileVersionS _See [use case](../src/files/domain/useCases/GetFileVersionSummaries.ts) implementation_. +- **limit**: (number) Limit for pagination. +- **offset**: (number) Offset for pagination. + ## Metadata Blocks ### Metadata Blocks read use cases diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 91674966..639d9510 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -66,7 +66,11 @@ export interface IDatasetsRepository { datasetId: number | string, includeMDC?: boolean ): Promise - getDatasetVersionsSummaries(datasetId: number | string): Promise + getDatasetVersionsSummaries( + datasetId: number | string, + limit?: number, + offset?: number + ): 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/GetDatasetVersionsSummaries.ts b/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts index 24458b00..ef449c16 100644 --- a/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts +++ b/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts @@ -14,9 +14,15 @@ export class GetDatasetVersionsSummaries implements UseCase} - An array of DatasetVersionSummaryInfo. */ - async execute(datasetId: number | string): Promise { - return await this.datasetsRepository.getDatasetVersionsSummaries(datasetId) + async execute( + datasetId: number | string, + limit?: number, + offset?: number + ): Promise { + return await this.datasetsRepository.getDatasetVersionsSummaries(datasetId, limit, offset) } } diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index f75bf1c0..4f16e7d5 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -306,11 +306,19 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi } public async getDatasetVersionsSummaries( - datasetId: string | number + datasetId: string | number, + limit?: number, + offset?: number ): Promise { + const queryParams: { per_page?: string; start?: string } = {} + + limit && (queryParams.per_page = limit.toString()) + offset && (queryParams.start = offset.toString()) + return this.doGet( this.buildApiEndpoint(this.datasetsResourceName, 'versions/compareSummary', datasetId), - true + true, + queryParams ) .then((response) => response.data.data) .catch((error) => { diff --git a/src/files/domain/repositories/IFilesRepository.ts b/src/files/domain/repositories/IFilesRepository.ts index 9256d92d..853de627 100644 --- a/src/files/domain/repositories/IFilesRepository.ts +++ b/src/files/domain/repositories/IFilesRepository.ts @@ -88,7 +88,11 @@ export interface IFilesRepository { replace?: boolean ): Promise - getFileVersionSummaries(fileId: number | string): Promise + getFileVersionSummaries( + fileId: number | string, + limit?: number, + offset?: number + ): Promise isFileDeleted(fileId: number | string): Promise } diff --git a/src/files/domain/useCases/GetFileVersionSummaries.ts b/src/files/domain/useCases/GetFileVersionSummaries.ts index c8bafb50..b24fa00a 100644 --- a/src/files/domain/useCases/GetFileVersionSummaries.ts +++ b/src/files/domain/useCases/GetFileVersionSummaries.ts @@ -13,9 +13,15 @@ export class GetFileVersionSummaries implements UseCase} - An array of FileVersionSummaryInfo. */ - async execute(fileId: number | string): Promise { - return await this.filesRepository.getFileVersionSummaries(fileId) + async execute( + fileId: number | string, + limit?: number, + offset?: number + ): Promise { + return await this.filesRepository.getFileVersionSummaries(fileId, limit, offset) } } diff --git a/src/files/infra/repositories/FilesRepository.ts b/src/files/infra/repositories/FilesRepository.ts index 3d24edaf..72debf45 100644 --- a/src/files/infra/repositories/FilesRepository.ts +++ b/src/files/infra/repositories/FilesRepository.ts @@ -423,10 +423,20 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { }) } - public async getFileVersionSummaries(fileId: number | string): Promise { + public async getFileVersionSummaries( + fileId: number | string, + limit?: number, + offset?: number + ): Promise { + const queryParams: { per_page?: string; start?: string } = {} + + limit && (queryParams.per_page = limit.toString()) + offset && (queryParams.start = offset.toString()) + return this.doGet( this.buildApiEndpoint(this.filesResourceName, 'versionDifferences', fileId), - true + true, + queryParams ) .then((response) => transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo(response)) .catch((error) => { diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 7abde006..de9634ba 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1482,6 +1482,65 @@ describe('DatasetsRepository', () => { expectedError ) }) + + test('should return dataset versions summaries with pagination', async () => { + const testDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testDatasetVersionsCollectionAlias + ) + + await publishDataset.execute(testDatasetIds.numericId, VersionUpdateType.MAJOR) + await waitForNoLocks(testDatasetIds.numericId, 10) + + const metadataBlocksRepository = new MetadataBlocksRepository() + const citationMetadataBlock = await metadataBlocksRepository.getMetadataBlockByName( + 'citation' + ) + + for (let i = 1; i <= 21; i++) { + await sut.updateDataset( + testDatasetIds.numericId, + { + metadataBlockValues: [ + { + name: 'citation', + fields: { + title: `Updated Dataset Title - Version ${i}` + } + } + ] + }, + [citationMetadataBlock] + ) + + await publishDataset.execute(testDatasetIds.numericId, VersionUpdateType.MINOR) + await waitForNoLocks(testDatasetIds.numericId, 10) + } + + const firstPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 0) + + expect(firstPage.length).toBe(5) + expect(firstPage[0].versionNumber).toBe('1.21') + expect(firstPage[4].versionNumber).toBe('1.17') + + // Test pagination with limit=5, offset=5 (second page) + const secondPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 5) + expect(secondPage.length).toBe(5) + expect(secondPage[0].versionNumber).toBe('1.16') + expect(secondPage[4].versionNumber).toBe('1.12') + + // Test pagination with limit=5, offset=10 (third page) + const thirdPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 10) + expect(thirdPage.length).toBe(5) + expect(thirdPage[0].versionNumber).toBe('1.11') + expect(thirdPage[4].versionNumber).toBe('1.7') + + // Test that all versions are returned without pagination + const allVersions = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) + expect(allVersions.length).toBe(22) // 1 initial + 21 updates + + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + }, 180000) }) describe('getDatasetDownloadCount', () => { diff --git a/test/integration/files/FilesRepository.test.ts b/test/integration/files/FilesRepository.test.ts index c20fd0c4..bc756443 100644 --- a/test/integration/files/FilesRepository.test.ts +++ b/test/integration/files/FilesRepository.test.ts @@ -1047,6 +1047,58 @@ describe('FilesRepository', () => { deletePublishedDatasetViaApi(fileTestDatasetIds.persistentId) }) + test('should return file version summaries with pagination', async () => { + // Create a new dataset and upload a file + const paginationTestDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO + ) + await uploadFileViaApi(paginationTestDatasetIds.numericId, testTextFile1Name) + + // Publish initial version (creates version 1.0) + await publishDatasetViaApi(paginationTestDatasetIds.numericId) + await waitForNoLocks(paginationTestDatasetIds.numericId, 10) + + // Get the file ID + const datasetFiles = await sut.getDatasetFiles( + paginationTestDatasetIds.numericId, + latestDatasetVersionId, + false, + FileOrderCriteria.NAME_AZ + ) + const paginationTestFile = datasetFiles.files[0] + + for (let i = 1; i <= 21; i++) { + await sut.updateFileMetadata(paginationTestFile.id, { + description: `File description update ${i}`, + label: `updated-file-${i}.txt` + }) + + await publishDatasetViaApi(paginationTestDatasetIds.numericId) + await waitForNoLocks(paginationTestDatasetIds.numericId, 10) + } + + const firstPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 0) + + expect(firstPage.length).toBe(5) + expect(firstPage[0].datasetVersion).toBe('1.21') + expect(firstPage[4].datasetVersion).toBe('1.17') + + const secondPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 5) + expect(secondPage.length).toBe(5) + expect(secondPage[0].datasetVersion).toBe('1.16') + expect(secondPage[4].datasetVersion).toBe('1.12') + + const thirdPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 10) + expect(thirdPage.length).toBe(5) + expect(thirdPage[0].datasetVersion).toBe('1.11') + expect(thirdPage[4].datasetVersion).toBe('1.7') + + const allVersions = await sut.getFileVersionSummaries(paginationTestFile.id) + expect(allVersions.length).toBe(22) + + await deletePublishedDatasetViaApi(paginationTestDatasetIds.persistentId) + }, 180000) + test('should return error when file does not exist', async () => { const expectedError = new ReadError(`[404] File with ID ${nonExistentFiledId} not found.`) From 2637a2a25a5614856386e4c27dde57aff15ec422 Mon Sep 17 00:00:00 2001 From: Cheng Shi <91049239+ChengShi-1@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:29:02 -0400 Subject: [PATCH 032/110] Update test/integration/datasets/DatasetsRepository.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/integration/datasets/DatasetsRepository.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index cc3b8176..1d5dca6e 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1856,12 +1856,6 @@ describe('DatasetsRepository', () => { describe('updateTermsOfAccess', () => { let testDatasetIds: CreatedDatasetIdentifiers - console.log( - 'authentication', - TestConstants.TEST_API_URL, - DataverseApiAuthMechanism.API_KEY, - process.env.TEST_API_KEY - ) beforeAll(async () => { testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) }) From 935338ac21da2c41cbd3f28a81a8ce83b40718eb Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 23 Oct 2025 16:40:26 -0400 Subject: [PATCH 033/110] fix: termsOfAccess as TermsOfAccess & { termsOfAccess?: string } --- src/datasets/infra/repositories/DatasetsRepository.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index cdeeb69c..1c9d4bab 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -477,9 +477,7 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi ): Promise { return this.doPut( this.buildApiEndpoint(this.datasetsResourceName, 'access', datasetId), - transformTermsOfAccessToUpdatePayload( - termsOfAccess as TermsOfAccess & { termsOfAccess?: string } - ) + transformTermsOfAccessToUpdatePayload(termsOfAccess) ) .then(() => undefined) .catch((error) => { From 2349c6a8608f0a0426dacd42dafd1369afcf180f Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 24 Oct 2025 09:56:36 -0400 Subject: [PATCH 034/110] remove the unused type add changelog --- CHANGELOG.md | 2 ++ .../repositories/transformers/termsOfAccessTransformers.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ea977c..eadd32ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). +- New Use Case: [Update Terms of Access](./docs/useCases.md#update-terms-of-access). + ### Changed ### Fixed diff --git a/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts b/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts index dd2fbc0b..1e9be77d 100644 --- a/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts +++ b/src/datasets/infra/repositories/transformers/termsOfAccessTransformers.ts @@ -1,8 +1,8 @@ import { TermsOfAccess } from '../../../domain/models/Dataset' -type TermsOfAccessInput = TermsOfAccess & { termsOfAccess?: string } - -export const transformTermsOfAccessToUpdatePayload = (terms: TermsOfAccessInput) => { +export const transformTermsOfAccessToUpdatePayload = ( + terms: TermsOfAccess & { termsOfAccess?: string } +) => { const { fileAccessRequest, dataAccessPlace, From 75436354ed831e2cfab0899c566d8cb232184b70 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Sun, 26 Oct 2025 01:34:07 -0400 Subject: [PATCH 035/110] changes on para --- src/datasets/infra/repositories/DatasetsRepository.ts | 8 ++++---- src/files/infra/repositories/FilesRepository.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 4f16e7d5..a183e800 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -310,10 +310,10 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi limit?: number, offset?: number ): Promise { - const queryParams: { per_page?: string; start?: string } = {} - - limit && (queryParams.per_page = limit.toString()) - offset && (queryParams.start = offset.toString()) + const queryParams: { limit?: string; offset?: string } = { + limit: limit?.toString(), + offset: offset?.toString() + } return this.doGet( this.buildApiEndpoint(this.datasetsResourceName, 'versions/compareSummary', datasetId), diff --git a/src/files/infra/repositories/FilesRepository.ts b/src/files/infra/repositories/FilesRepository.ts index 72debf45..a33c1f77 100644 --- a/src/files/infra/repositories/FilesRepository.ts +++ b/src/files/infra/repositories/FilesRepository.ts @@ -428,10 +428,10 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { limit?: number, offset?: number ): Promise { - const queryParams: { per_page?: string; start?: string } = {} - - limit && (queryParams.per_page = limit.toString()) - offset && (queryParams.start = offset.toString()) + const queryParams: { limit?: string; offset?: string } = { + limit: limit?.toString(), + offset: offset?.toString() + } return this.doGet( this.buildApiEndpoint(this.filesResourceName, 'versionDifferences', fileId), From 050a26860ed25c79dec5d097206d760639563f17 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 27 Oct 2025 10:32:35 -0400 Subject: [PATCH 036/110] fix: test failing --- test/integration/files/FilesRepository.test.ts | 16 +++++++++------- .../datasets/GetDatasetVersionsSummaries.test.ts | 6 +++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/test/integration/files/FilesRepository.test.ts b/test/integration/files/FilesRepository.test.ts index bc756443..b6dacd5d 100644 --- a/test/integration/files/FilesRepository.test.ts +++ b/test/integration/files/FilesRepository.test.ts @@ -925,6 +925,8 @@ describe('FilesRepository', () => { contributors: 'Dataverse Admin', datafileId: testFile.id, persistentId: testFile.persistentId, + publishedDate: '', + versionNote: undefined, fileDifferenceSummary: { file: 'Added' } } @@ -1022,7 +1024,7 @@ describe('FilesRepository', () => { contributors: 'Dataverse Admin', datafileId: testFile.id, persistentId: testFile.persistentId, - publishedDate: actual[0].publishedDate, + publishedDate: '', versionNote: undefined, fileDifferenceSummary: { fileMetadata: [ @@ -1080,18 +1082,18 @@ describe('FilesRepository', () => { const firstPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 0) expect(firstPage.length).toBe(5) - expect(firstPage[0].datasetVersion).toBe('1.21') - expect(firstPage[4].datasetVersion).toBe('1.17') + expect(firstPage[0].datasetVersion).toBe('22.0') + expect(firstPage[4].datasetVersion).toBe('18.0') const secondPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 5) expect(secondPage.length).toBe(5) - expect(secondPage[0].datasetVersion).toBe('1.16') - expect(secondPage[4].datasetVersion).toBe('1.12') + expect(secondPage[0].datasetVersion).toBe('17.0') + expect(secondPage[4].datasetVersion).toBe('13.0') const thirdPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 10) expect(thirdPage.length).toBe(5) - expect(thirdPage[0].datasetVersion).toBe('1.11') - expect(thirdPage[4].datasetVersion).toBe('1.7') + expect(thirdPage[0].datasetVersion).toBe('12.0') + expect(thirdPage[4].datasetVersion).toBe('8.0') const allVersions = await sut.getFileVersionSummaries(paginationTestFile.id) expect(allVersions.length).toBe(22) diff --git a/test/unit/datasets/GetDatasetVersionsSummaries.test.ts b/test/unit/datasets/GetDatasetVersionsSummaries.test.ts index 184ec648..ef4678d9 100644 --- a/test/unit/datasets/GetDatasetVersionsSummaries.test.ts +++ b/test/unit/datasets/GetDatasetVersionsSummaries.test.ts @@ -17,7 +17,11 @@ describe('execute', () => { const actual = await sut.execute(testDatasetId) expect(actual).toEqual(testDatasetVersionsSummaries) - expect(datasetsRepositoryStub.getDatasetVersionsSummaries).toHaveBeenCalledWith(testDatasetId) + expect(datasetsRepositoryStub.getDatasetVersionsSummaries).toHaveBeenCalledWith( + testDatasetId, + undefined, + undefined + ) }) test('should return error result on repository error', async () => { From 55dcbd5e6c5957bd06473a50e7d50e3f487d61b8 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 27 Oct 2025 11:46:38 -0400 Subject: [PATCH 037/110] fix: notficiation and changelog --- CHANGELOG.md | 2 ++ .../integration/notifications/NotificationsRepository.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 906a6918..e56fe504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Changed +- Add pagination query parameters to Dataset Version Summeries and File Version Summaries use cases + ### Fixed ### Removed diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 5333e48d..c74ef73a 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -54,10 +54,10 @@ describe('NotificationsRepository', () => { expect(publishedNotification).toHaveProperty('sentTimestamp') expect(publishedNotification?.subjectText).toContain( - 'Dataset created using the createDataset use case' + `Dataset "${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}" has been published` ) expect(publishedNotification?.messageText).toContain( - 'Your dataset named Dataset created using the createDataset use case' + `Your dataset named "${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}"` ) }) From 8c22c5b2337ed84df439baf4196fde4b2cb9cb96 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 27 Oct 2025 12:01:34 -0400 Subject: [PATCH 038/110] fix: notficiation test result --- test/integration/notifications/NotificationsRepository.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index c74ef73a..78d692f5 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -56,8 +56,9 @@ describe('NotificationsRepository', () => { expect(publishedNotification?.subjectText).toContain( `Dataset "${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}" has been published` ) + expect(publishedNotification?.messageText).toContain( - `Your dataset named "${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}"` + `Your dataset named ${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}` ) }) From ad571e5dfa84b25604929971fa2a548c716cc045 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 29 Oct 2025 15:53:14 -0400 Subject: [PATCH 039/110] fix: add totalCount to version summaries --- .../models/DatasetVersionSummaryInfo.ts | 5 ++ .../repositories/IDatasetsRepository.ts | 4 +- .../useCases/GetDatasetVersionsSummaries.ts | 8 +- src/datasets/index.ts | 3 +- .../infra/repositories/DatasetsRepository.ts | 20 +++-- .../domain/models/FileVersionSummaryInfo.ts | 5 ++ .../domain/repositories/IFilesRepository.ts | 4 +- .../useCases/GetFileVersionSummaries.ts | 8 +- src/files/index.ts | 7 ++ .../infra/repositories/FilesRepository.ts | 15 ++-- .../fileVersionSummaryInfoTransformers.ts | 13 +++- .../datasets/DatasetsRepository.test.ts | 75 +++++++++++-------- .../integration/files/FilesRepository.test.ts | 53 +++++++------ test/unit/datasets/DatasetsRepository.test.ts | 23 ++++-- .../GetDatasetVersionsSummaries.test.ts | 10 ++- .../files/GetFileVersionSummaries.test.ts | 44 ++++++----- 16 files changed, 190 insertions(+), 107 deletions(-) diff --git a/src/datasets/domain/models/DatasetVersionSummaryInfo.ts b/src/datasets/domain/models/DatasetVersionSummaryInfo.ts index 782a2e0f..c012a4b6 100644 --- a/src/datasets/domain/models/DatasetVersionSummaryInfo.ts +++ b/src/datasets/domain/models/DatasetVersionSummaryInfo.ts @@ -6,6 +6,11 @@ export interface DatasetVersionSummaryInfo { publishedOn?: string } +export interface DatasetVersionSummarySubset { + summaries: DatasetVersionSummaryInfo[] + totalCount: number +} + export type DatasetVersionSummary = { [key: string]: | SummaryUpdates diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 639d9510..8521d813 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -8,7 +8,7 @@ import { DatasetDeaccessionDTO } from '../dtos/DatasetDeaccessionDTO' import { MetadataBlock } from '../../../metadataBlocks' import { DatasetVersionDiff } from '../models/DatasetVersionDiff' import { DatasetDownloadCount } from '../models/DatasetDownloadCount' -import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' +import { DatasetVersionSummarySubset } from '../models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' @@ -70,7 +70,7 @@ export interface IDatasetsRepository { datasetId: number | string, limit?: number, offset?: number - ): Promise + ): 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/GetDatasetVersionsSummaries.ts b/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts index ef449c16..d5ea8834 100644 --- a/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts +++ b/src/datasets/domain/useCases/GetDatasetVersionsSummaries.ts @@ -1,8 +1,8 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' -import { DatasetVersionSummaryInfo } from '../models/DatasetVersionSummaryInfo' +import { DatasetVersionSummarySubset } from '../models/DatasetVersionSummaryInfo' import { IDatasetsRepository } from '../repositories/IDatasetsRepository' -export class GetDatasetVersionsSummaries implements UseCase { +export class GetDatasetVersionsSummaries implements UseCase { private datasetsRepository: IDatasetsRepository constructor(datasetsRepository: IDatasetsRepository) { @@ -16,13 +16,13 @@ export class GetDatasetVersionsSummaries implements UseCase} - An array of DatasetVersionSummaryInfo. + * @returns {Promise} - A DatasetVersionSummarySubset containing the summaries and total count. */ async execute( datasetId: number | string, limit?: number, offset?: number - ): Promise { + ): Promise { return await this.datasetsRepository.getDatasetVersionsSummaries(datasetId, limit, offset) } } diff --git a/src/datasets/index.ts b/src/datasets/index.ts index 1bd1de0e..6fcc3963 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -141,7 +141,8 @@ export { CreatedDatasetIdentifiers } from './domain/models/CreatedDatasetIdentif export { VersionUpdateType } from './domain/models/Dataset' export { DatasetVersionSummaryInfo, - DatasetVersionSummaryStringValues + DatasetVersionSummaryStringValues, + DatasetVersionSummarySubset } 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 a183e800..08d950d2 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -20,7 +20,7 @@ import { transformDatasetPreviewsResponseToDatasetPreviewSubset } from './transf import { DatasetVersionDiff } from '../../domain/models/DatasetVersionDiff' import { transformDatasetVersionDiffResponseToDatasetVersionDiff } from './transformers/datasetVersionDiffTransformers' import { DatasetDownloadCount } from '../../domain/models/DatasetDownloadCount' -import { DatasetVersionSummaryInfo } from '../../domain/models/DatasetVersionSummaryInfo' +import { DatasetVersionSummarySubset } from '../../domain/models/DatasetVersionSummaryInfo' import { DatasetLinkedCollection } from '../../domain/models/DatasetLinkedCollection' import { CitationFormat } from '../../domain/models/CitationFormat' import { transformDatasetLinkedCollectionsResponseToDatasetLinkedCollection } from './transformers/datasetLinkedCollectionsTransformers' @@ -309,10 +309,15 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi datasetId: string | number, limit?: number, offset?: number - ): Promise { - const queryParams: { limit?: string; offset?: string } = { - limit: limit?.toString(), - offset: offset?.toString() + ): Promise { + const queryParams = new URLSearchParams() + + if (limit) { + queryParams.set('limit', limit.toString()) + } + + if (offset) { + queryParams.set('offset', offset.toString()) } return this.doGet( @@ -320,7 +325,10 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi true, queryParams ) - .then((response) => response.data.data) + .then((response) => ({ + summaries: response.data.data, + totalCount: response.data.totalCount + })) .catch((error) => { throw error }) diff --git a/src/files/domain/models/FileVersionSummaryInfo.ts b/src/files/domain/models/FileVersionSummaryInfo.ts index 2ee0a7ba..3cf95315 100644 --- a/src/files/domain/models/FileVersionSummaryInfo.ts +++ b/src/files/domain/models/FileVersionSummaryInfo.ts @@ -11,6 +11,11 @@ export interface FileVersionSummaryInfo { versionNote?: string } +export interface FileVersionSummarySubset { + summaries: FileVersionSummaryInfo[] + totalCount: number +} + export type FileDifferenceSummary = { file?: FileChangeType fileAccess?: 'Restricted' | 'Unrestricted' diff --git a/src/files/domain/repositories/IFilesRepository.ts b/src/files/domain/repositories/IFilesRepository.ts index 853de627..4890a38b 100644 --- a/src/files/domain/repositories/IFilesRepository.ts +++ b/src/files/domain/repositories/IFilesRepository.ts @@ -10,7 +10,7 @@ import { FileUploadDestination } from '../models/FileUploadDestination' import { UploadedFileDTO } from '../dtos/UploadedFileDTO' import { UpdateFileMetadataDTO } from '../dtos/UpdateFileMetadataDTO' import { RestrictFileDTO } from '../dtos/RestrictFileDTO' -import { FileVersionSummaryInfo } from '../models/FileVersionSummaryInfo' +import { FileVersionSummarySubset } from '../models/FileVersionSummaryInfo' export interface IFilesRepository { getDatasetFiles( @@ -92,7 +92,7 @@ export interface IFilesRepository { fileId: number | string, limit?: number, offset?: number - ): Promise + ): Promise isFileDeleted(fileId: number | string): Promise } diff --git a/src/files/domain/useCases/GetFileVersionSummaries.ts b/src/files/domain/useCases/GetFileVersionSummaries.ts index b24fa00a..c46b14db 100644 --- a/src/files/domain/useCases/GetFileVersionSummaries.ts +++ b/src/files/domain/useCases/GetFileVersionSummaries.ts @@ -1,8 +1,8 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' -import { FileVersionSummaryInfo } from '../models/FileVersionSummaryInfo' +import { FileVersionSummarySubset } from '../models/FileVersionSummaryInfo' import { IFilesRepository } from '../repositories/IFilesRepository' -export class GetFileVersionSummaries implements UseCase { +export class GetFileVersionSummaries implements UseCase { private filesRepository: IFilesRepository constructor(filesRepository: IFilesRepository) { @@ -15,13 +15,13 @@ export class GetFileVersionSummaries implements UseCase} - An array of FileVersionSummaryInfo. + * @returns {Promise} - A FileVersionSummarySubset containing the summaries and total count. */ async execute( fileId: number | string, limit?: number, offset?: number - ): Promise { + ): Promise { return await this.filesRepository.getFileVersionSummaries(fileId, limit, offset) } } diff --git a/src/files/index.ts b/src/files/index.ts index b13bfc53..a9d38386 100644 --- a/src/files/index.ts +++ b/src/files/index.ts @@ -93,3 +93,10 @@ export { FilesSubset } from './domain/models/FilesSubset' export { FilePreview, FilePreviewChecksum } from './domain/models/FilePreview' export { UploadedFileDTO } from './domain/dtos/UploadedFileDTO' export { UpdateFileMetadataDTO } from './domain/dtos/UpdateFileMetadataDTO' +export { + FileVersionSummaryInfo, + FileDifferenceSummary, + FileChangeType, + FileMetadataChange, + FileVersionSummarySubset +} from './domain/models/FileVersionSummaryInfo' diff --git a/src/files/infra/repositories/FilesRepository.ts b/src/files/infra/repositories/FilesRepository.ts index a33c1f77..00b70ba8 100644 --- a/src/files/infra/repositories/FilesRepository.ts +++ b/src/files/infra/repositories/FilesRepository.ts @@ -22,7 +22,7 @@ import { UploadedFileDTO } from '../../domain/dtos/UploadedFileDTO' import { UpdateFileMetadataDTO } from '../../domain/dtos/UpdateFileMetadataDTO' import { ApiConstants } from '../../../core/infra/repositories/ApiConstants' import { RestrictFileDTO } from '../../domain/dtos/RestrictFileDTO' -import { FileVersionSummaryInfo } from '../../domain/models/FileVersionSummaryInfo' +import { FileVersionSummarySubset } from '../../domain/models/FileVersionSummaryInfo' import { transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo } from './transformers/fileVersionSummaryInfoTransformers' export interface GetFilesQueryParams { @@ -427,10 +427,15 @@ export class FilesRepository extends ApiRepository implements IFilesRepository { fileId: number | string, limit?: number, offset?: number - ): Promise { - const queryParams: { limit?: string; offset?: string } = { - limit: limit?.toString(), - offset: offset?.toString() + ): Promise { + const queryParams = new URLSearchParams() + + if (limit) { + queryParams.set('limit', limit.toString()) + } + + if (offset) { + queryParams.set('offset', offset.toString()) } return this.doGet( diff --git a/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts b/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts index 46eb4049..dd62262d 100644 --- a/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts +++ b/src/files/infra/repositories/transformers/fileVersionSummaryInfoTransformers.ts @@ -2,7 +2,8 @@ import { AxiosResponse } from 'axios' import { FileVersionSummaryInfo, FileMetadataChange, - FileDifferenceSummary + FileDifferenceSummary, + FileVersionSummarySubset } from '../../../domain/models/FileVersionSummaryInfo' import { DatasetVersionState } from '../../../../datasets/domain/models/Dataset' @@ -29,10 +30,11 @@ export interface FileVersionSummaryInfoPayload { export const transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo = ( response: AxiosResponse -): FileVersionSummaryInfo[] => { +): FileVersionSummarySubset => { const payload = response.data.data + const totalCount = response.data.totalCount - return payload.map((item: FileVersionSummaryInfoPayload): FileVersionSummaryInfo => { + const summaries = payload.map((item: FileVersionSummaryInfoPayload): FileVersionSummaryInfo => { const summary = item.fileDifferenceSummary || {} const fileDifferenceSummary: FileDifferenceSummary = { @@ -54,4 +56,9 @@ export const transformFileVersionSummaryInfoResponseToFileVersionSummaryInfo = ( versionNote: item.versionNote } }) + + return { + summaries, + totalCount + } } diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index de9634ba..14c796d2 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -42,8 +42,7 @@ import { createCollectionViaApi, deleteCollectionViaApi, publishCollectionViaApi, - ROOT_COLLECTION_ALIAS, - setStorageDriverViaApi + ROOT_COLLECTION_ALIAS } from '../../testHelpers/collections/collectionHelper' import { calculateBlobChecksum, @@ -1292,7 +1291,6 @@ describe('DatasetsRepository', () => { beforeAll(async () => { await createCollectionViaApi(testDatasetVersionsCollectionAlias) await publishCollectionViaApi(testDatasetVersionsCollectionAlias) - await setStorageDriverViaApi(testDatasetVersionsCollectionAlias, 'LocalStack') }) afterAll(async () => { @@ -1306,10 +1304,12 @@ describe('DatasetsRepository', () => { ) const actual = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) + console.log('actual summaries', actual) - expect(actual.length).toBeGreaterThan(0) - expect(actual[0].versionNumber).toBe('DRAFT') - expect(actual[0].summary).toBe(DatasetVersionSummaryStringValues.firstDraft) + expect(actual.summaries.length).toBeGreaterThan(0) + expect(actual.totalCount).toBeGreaterThan(0) + expect(actual.summaries[0].versionNumber).toBe('DRAFT') + expect(actual.summaries[0].summary).toBe(DatasetVersionSummaryStringValues.firstDraft) await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) }) @@ -1325,9 +1325,10 @@ describe('DatasetsRepository', () => { const actual = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - expect(actual.length).toBeGreaterThan(0) - expect(actual[0].versionNumber).toBe('1.0') - expect(actual[0].summary).toBe(DatasetVersionSummaryStringValues.firstPublished) + expect(actual.summaries.length).toBeGreaterThan(0) + expect(actual.totalCount).toBeGreaterThan(0) + expect(actual.summaries[0].versionNumber).toBe('1.0') + expect(actual.summaries[0].summary).toBe(DatasetVersionSummaryStringValues.firstPublished) await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }) @@ -1348,9 +1349,10 @@ describe('DatasetsRepository', () => { const actual = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - expect(actual.length).toBeGreaterThan(0) - expect(actual[0].versionNumber).toBe('1.0') - expect(actual[0].summary).toStrictEqual(deaccessionReason) + expect(actual.summaries.length).toBeGreaterThan(0) + expect(actual.totalCount).toBeGreaterThan(0) + expect(actual.summaries[0].versionNumber).toBe('1.0') + expect(actual.summaries[0].summary).toStrictEqual(deaccessionReason) await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }) @@ -1387,9 +1389,10 @@ describe('DatasetsRepository', () => { const actual = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - expect(actual.length).toEqual(2) - expect(actual[0].versionNumber).toBe('DRAFT') - expect(actual[0].summary).toMatchObject({ + expect(actual.summaries.length).toEqual(2) + expect(actual.totalCount).toEqual(2) + expect(actual.summaries[0].versionNumber).toBe('DRAFT') + expect(actual.summaries[0].summary).toMatchObject({ 'Citation Metadata': { Title: { added: 0, @@ -1407,8 +1410,8 @@ describe('DatasetsRepository', () => { termsAccessChanged: false }) - expect(actual[1].versionNumber).toBe('1.0') - expect(actual[1].summary).toBe(DatasetVersionSummaryStringValues.firstPublished) + expect(actual.summaries[1].versionNumber).toBe('1.0') + expect(actual.summaries[1].summary).toBe(DatasetVersionSummaryStringValues.firstPublished) await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }) @@ -1454,10 +1457,11 @@ describe('DatasetsRepository', () => { const actual = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - expect(actual.length).toEqual(2) + expect(actual.summaries.length).toEqual(2) + expect(actual.totalCount).toEqual(2) - expect(actual[0].versionNumber).toBe('DRAFT') - expect(actual[0].summary).toMatchObject({ + expect(actual.summaries[0].versionNumber).toBe('DRAFT') + expect(actual.summaries[0].summary).toMatchObject({ files: { added: 1, removed: 0, @@ -1467,8 +1471,8 @@ describe('DatasetsRepository', () => { }, termsAccessChanged: false }) - expect(actual[1].versionNumber).toBe('1.0') - expect(actual[1].summary).toBe(DatasetVersionSummaryStringValues.firstPublished) + expect(actual.summaries[1].versionNumber).toBe('1.0') + expect(actual.summaries[1].summary).toBe(DatasetVersionSummaryStringValues.firstPublished) await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }) @@ -1517,27 +1521,34 @@ describe('DatasetsRepository', () => { await waitForNoLocks(testDatasetIds.numericId, 10) } + const summaries = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) + console.log('summaries', summaries) + const firstPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 0) - expect(firstPage.length).toBe(5) - expect(firstPage[0].versionNumber).toBe('1.21') - expect(firstPage[4].versionNumber).toBe('1.17') + expect(firstPage.summaries.length).toBe(5) + expect(firstPage.totalCount).toBe(22) + expect(firstPage.summaries[0].versionNumber).toBe('1.21') + expect(firstPage.summaries[4].versionNumber).toBe('1.17') // Test pagination with limit=5, offset=5 (second page) const secondPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 5) - expect(secondPage.length).toBe(5) - expect(secondPage[0].versionNumber).toBe('1.16') - expect(secondPage[4].versionNumber).toBe('1.12') + expect(secondPage.summaries.length).toBe(5) + expect(secondPage.totalCount).toBe(22) + expect(secondPage.summaries[0].versionNumber).toBe('1.16') + expect(secondPage.summaries[4].versionNumber).toBe('1.12') // Test pagination with limit=5, offset=10 (third page) const thirdPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 10) - expect(thirdPage.length).toBe(5) - expect(thirdPage[0].versionNumber).toBe('1.11') - expect(thirdPage[4].versionNumber).toBe('1.7') + expect(thirdPage.summaries.length).toBe(5) + expect(thirdPage.totalCount).toBe(22) + expect(thirdPage.summaries[0].versionNumber).toBe('1.11') + expect(thirdPage.summaries[4].versionNumber).toBe('1.7') // Test that all versions are returned without pagination const allVersions = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - expect(allVersions.length).toBe(22) // 1 initial + 21 updates + expect(allVersions.summaries.length).toBe(22) // 1 initial + 21 updates + expect(allVersions.totalCount).toBe(22) await deletePublishedDatasetViaApi(testDatasetIds.persistentId) }, 180000) diff --git a/test/integration/files/FilesRepository.test.ts b/test/integration/files/FilesRepository.test.ts index b6dacd5d..6e0c9bbc 100644 --- a/test/integration/files/FilesRepository.test.ts +++ b/test/integration/files/FilesRepository.test.ts @@ -930,8 +930,9 @@ describe('FilesRepository', () => { fileDifferenceSummary: { file: 'Added' } } - expect(actual).toHaveLength(1) - expect(actual[0]).toEqual(fileSummmaries) + expect(actual.summaries).toHaveLength(1) + expect(actual.totalCount).toBe(1) + expect(actual.summaries[0]).toEqual(fileSummmaries) deleteUnpublishedDatasetViaApi(fileTestDatasetIds.numericId) }) @@ -955,7 +956,7 @@ describe('FilesRepository', () => { const publishedFileVersionSummmaries: FileVersionSummaryInfo = { datasetVersion: '1.0', - publishedDate: publishedFileVersionSummariesActual[0].publishedDate, + publishedDate: publishedFileVersionSummariesActual.summaries[0].publishedDate, versionState: DatasetVersionState.RELEASED, contributors: 'Dataverse Admin', datafileId: testFile.id, @@ -963,8 +964,11 @@ describe('FilesRepository', () => { fileDifferenceSummary: { file: 'Added' } } - expect(publishedFileVersionSummariesActual).toHaveLength(1) - expect(publishedFileVersionSummariesActual[0]).toEqual(publishedFileVersionSummmaries) + expect(publishedFileVersionSummariesActual.summaries).toHaveLength(1) + expect(publishedFileVersionSummariesActual.totalCount).toBe(1) + expect(publishedFileVersionSummariesActual.summaries[0]).toEqual( + publishedFileVersionSummmaries + ) await deaccessionDatasetViaApi(fileTestDatasetIds.numericId, '1.0').catch(() => { throw new Error('Error while deaccessioning test Dataset') @@ -974,7 +978,7 @@ describe('FilesRepository', () => { const fileSummmaries: FileVersionSummaryInfo = { datasetVersion: '1.0', - publishedDate: publishedFileVersionSummariesActual[0].publishedDate, + publishedDate: publishedFileVersionSummariesActual.summaries[0].publishedDate, versionState: DatasetVersionState.DEACCESSIONED, contributors: 'Dataverse Admin', datafileId: testFile.id, @@ -985,8 +989,9 @@ describe('FilesRepository', () => { } } - expect(actual).toHaveLength(1) - expect(actual[0]).toEqual(fileSummmaries) + expect(actual.summaries).toHaveLength(1) + expect(actual.totalCount).toBe(1) + expect(actual.summaries[0]).toEqual(fileSummmaries) deletePublishedDatasetViaApi(fileTestDatasetIds.persistentId) }) @@ -1008,7 +1013,8 @@ describe('FilesRepository', () => { const testFile = datasetFiles.files[0] const actual = await sut.getFileVersionSummaries(testFile.id) - expect(actual).toHaveLength(1) + expect(actual.summaries).toHaveLength(1) + expect(actual.totalCount).toBe(1) await sut.updateFileMetadata(testFile.id, { description: 'My description test.', @@ -1044,8 +1050,9 @@ describe('FilesRepository', () => { } } - expect(updatedFileVersionSummariesActual).toHaveLength(2) - expect(updatedFileVersionSummariesActual[0]).toEqual(updatedFileVersionSummaries) + expect(updatedFileVersionSummariesActual.summaries).toHaveLength(2) + expect(updatedFileVersionSummariesActual.totalCount).toBe(2) + expect(updatedFileVersionSummariesActual.summaries[0]).toEqual(updatedFileVersionSummaries) deletePublishedDatasetViaApi(fileTestDatasetIds.persistentId) }) @@ -1081,22 +1088,26 @@ describe('FilesRepository', () => { const firstPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 0) - expect(firstPage.length).toBe(5) - expect(firstPage[0].datasetVersion).toBe('22.0') - expect(firstPage[4].datasetVersion).toBe('18.0') + expect(firstPage.summaries.length).toBe(5) + expect(firstPage.totalCount).toBe(22) + expect(firstPage.summaries[0].datasetVersion).toBe('22.0') + expect(firstPage.summaries[4].datasetVersion).toBe('18.0') const secondPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 5) - expect(secondPage.length).toBe(5) - expect(secondPage[0].datasetVersion).toBe('17.0') - expect(secondPage[4].datasetVersion).toBe('13.0') + expect(secondPage.summaries.length).toBe(5) + expect(secondPage.totalCount).toBe(22) + expect(secondPage.summaries[0].datasetVersion).toBe('17.0') + expect(secondPage.summaries[4].datasetVersion).toBe('13.0') const thirdPage = await sut.getFileVersionSummaries(paginationTestFile.id, 5, 10) - expect(thirdPage.length).toBe(5) - expect(thirdPage[0].datasetVersion).toBe('12.0') - expect(thirdPage[4].datasetVersion).toBe('8.0') + expect(thirdPage.summaries.length).toBe(5) + expect(thirdPage.totalCount).toBe(22) + expect(thirdPage.summaries[0].datasetVersion).toBe('12.0') + expect(thirdPage.summaries[4].datasetVersion).toBe('8.0') const allVersions = await sut.getFileVersionSummaries(paginationTestFile.id) - expect(allVersions.length).toBe(22) + expect(allVersions.summaries.length).toBe(22) + expect(allVersions.totalCount).toBe(22) await deletePublishedDatasetViaApi(paginationTestDatasetIds.persistentId) }, 180000) diff --git a/test/unit/datasets/DatasetsRepository.test.ts b/test/unit/datasets/DatasetsRepository.test.ts index fe53ed49..3003d27a 100644 --- a/test/unit/datasets/DatasetsRepository.test.ts +++ b/test/unit/datasets/DatasetsRepository.test.ts @@ -1055,7 +1055,8 @@ describe('DatasetsRepository', () => { const testDatasetVersionSummariesResponse = { data: { status: 'OK', - data: [testDatasetVersionSummaries] + data: [testDatasetVersionSummaries], + totalCount: 1 } } @@ -1072,7 +1073,10 @@ describe('DatasetsRepository', () => { expectedApiEndpoint, TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY ) - expect(actual).toStrictEqual([testDatasetVersionSummaries]) + expect(actual).toStrictEqual({ + summaries: [testDatasetVersionSummaries], + totalCount: 1 + }) // Session cookie auth ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) @@ -1083,7 +1087,10 @@ describe('DatasetsRepository', () => { expectedApiEndpoint, TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE ) - expect(actual).toStrictEqual([testDatasetVersionSummaries]) + expect(actual).toStrictEqual({ + summaries: [testDatasetVersionSummaries], + totalCount: 1 + }) }) test('should return error result on error response', async () => { @@ -1113,7 +1120,10 @@ describe('DatasetsRepository', () => { expectedApiEndpoint, TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY ) - expect(actual).toStrictEqual([testDatasetVersionSummaries]) + expect(actual).toStrictEqual({ + summaries: [testDatasetVersionSummaries], + totalCount: 1 + }) // Session cookie auth ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) @@ -1124,7 +1134,10 @@ describe('DatasetsRepository', () => { expectedApiEndpoint, TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE ) - expect(actual).toStrictEqual([testDatasetVersionSummaries]) + expect(actual).toStrictEqual({ + summaries: [testDatasetVersionSummaries], + totalCount: 1 + }) }) test('should return error result on error response', async () => { diff --git a/test/unit/datasets/GetDatasetVersionsSummaries.test.ts b/test/unit/datasets/GetDatasetVersionsSummaries.test.ts index ef4678d9..82850a28 100644 --- a/test/unit/datasets/GetDatasetVersionsSummaries.test.ts +++ b/test/unit/datasets/GetDatasetVersionsSummaries.test.ts @@ -2,21 +2,25 @@ import { ReadError } from '../../../src/core/domain/repositories/ReadError' import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' import { createDatasetVersionSummaryModel } from '../../testHelpers/datasets/datasetVersionsSummariesHelper' import { GetDatasetVersionsSummaries } from '../../../src/datasets/domain/useCases/GetDatasetVersionsSummaries' +import { DatasetVersionSummarySubset } from '../../../src/datasets/domain/models/DatasetVersionSummaryInfo' const testDatasetId = 1 describe('execute', () => { test('should return dataset versions summaries on repository success', async () => { - const testDatasetVersionsSummaries = [createDatasetVersionSummaryModel()] + const testDatasetVersionsSummariesSubset: DatasetVersionSummarySubset = { + summaries: [createDatasetVersionSummaryModel()], + totalCount: 1 + } const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository datasetsRepositoryStub.getDatasetVersionsSummaries = jest .fn() - .mockResolvedValue(testDatasetVersionsSummaries) + .mockResolvedValue(testDatasetVersionsSummariesSubset) const sut = new GetDatasetVersionsSummaries(datasetsRepositoryStub) const actual = await sut.execute(testDatasetId) - expect(actual).toEqual(testDatasetVersionsSummaries) + expect(actual).toEqual(testDatasetVersionsSummariesSubset) expect(datasetsRepositoryStub.getDatasetVersionsSummaries).toHaveBeenCalledWith( testDatasetId, undefined, diff --git a/test/unit/files/GetFileVersionSummaries.test.ts b/test/unit/files/GetFileVersionSummaries.test.ts index d8352603..709ac19e 100644 --- a/test/unit/files/GetFileVersionSummaries.test.ts +++ b/test/unit/files/GetFileVersionSummaries.test.ts @@ -1,32 +1,38 @@ import { IFilesRepository } from '../../../src/files/domain/repositories/IFilesRepository' import { ReadError } from '../../../src' import { GetFileVersionSummaries } from '../../../src/files/domain/useCases/GetFileVersionSummaries' -import { FileVersionSummaryInfo } from '../../../src/files/domain/models/FileVersionSummaryInfo' +import { FileVersionSummarySubset } from '../../../src/files/domain/models/FileVersionSummaryInfo' describe('execute', () => { test('should return file on repository success when passing numeric id', async () => { const filesRepositoryStub: IFilesRepository = {} as IFilesRepository - const fileVersionSummaries: FileVersionSummaryInfo[] = [ - { - datasetVersion: '1.0', - contributors: 'John Doe', - publishedDate: '2023-01-01', - fileDifferenceSummary: { - fileMetadata: [ - { - name: 'file.txt', - action: 'Added' - } - ] - }, - datafileId: 1 - } - ] - filesRepositoryStub.getFileVersionSummaries = jest.fn().mockResolvedValue(fileVersionSummaries) + const fileVersionSummariesSubset: FileVersionSummarySubset = { + totalCount: 1, + summaries: [ + { + datasetVersion: '1.0', + contributors: 'John Doe', + publishedDate: '2023-01-01', + fileDifferenceSummary: { + fileMetadata: [ + { + name: 'file.txt', + action: 'Added' + } + ] + }, + datafileId: 1 + } + ], + totalCount: 1 + } + filesRepositoryStub.getFileVersionSummaries = jest + .fn() + .mockResolvedValue(fileVersionSummariesSubset) const sut = new GetFileVersionSummaries(filesRepositoryStub) const actualFileVersionSummaries = await sut.execute(1) - expect(actualFileVersionSummaries).toEqual(fileVersionSummaries) + expect(actualFileVersionSummaries).toEqual(fileVersionSummariesSubset) }) test('should return error result on repository error', async () => { From 4b27c5823236ac5a67028f477b372686e90b3459 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 29 Oct 2025 16:19:31 -0400 Subject: [PATCH 040/110] fix: test errors --- docs/useCases.md | 8 +- .../datasets/DatasetsRepository.test.ts | 4 +- test/unit/datasets/DatasetsRepository.test.ts | 105 ++++++++++++------ .../files/GetFileVersionSummaries.test.ts | 1 - 4 files changed, 76 insertions(+), 42 deletions(-) diff --git a/docs/useCases.md b/docs/useCases.md index 4ac475b8..b55157b8 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -872,7 +872,7 @@ The `DatasetPreviewSubset`returned instance contains a property called `totalDat #### Get Dataset Versions Summaries -Returns an array of [DatasetVersionSummaryInfo](../src/datasets/domain/models/DatasetVersionSummaryInfo.ts) that contains information about what changed in every specific version. +Returns the total count of versions and an array of [DatasetVersionSummaryInfo](../src/datasets/domain/models/DatasetVersionSummaryInfo.ts) that contains information about what changed in every specific version. ##### Example call: @@ -885,7 +885,7 @@ const datasetId = 'doi:10.77777/FK2/AAAAAA' getDatasetVersionsSummaries .execute(datasetId) - .then((datasetVersionsSummaries: DatasetVersionSummaryInfo[]) => { + .then((datasetVersionsSummaries: DatasetVersionSummarySubset) => { /* ... */ }) @@ -2002,7 +2002,7 @@ The `fileId` parameter can be a string, for persistent identifiers, or a number, #### Get File Version Summaries -Get the file versions summaries, return a list of summaries for each version +Get the file versions summaries, return a total count of versions and a list of summaries for each version ##### Example call: @@ -2013,7 +2013,7 @@ import { getFileVersionSummaries } from '@iqss/dataverse-client-javascript' const fileId = 1 -getFileVersionSummaries.execute(fileId).then((fileVersionSummaries: fileVersionSummaryInfo[]) => { +getFileVersionSummaries.execute(fileId).then((fileVersionSummaries: fileVersionSummarySubset) => { /* ... */ }) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 14c796d2..ff8da4f7 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -42,7 +42,8 @@ import { createCollectionViaApi, deleteCollectionViaApi, publishCollectionViaApi, - ROOT_COLLECTION_ALIAS + ROOT_COLLECTION_ALIAS, + setStorageDriverViaApi } from '../../testHelpers/collections/collectionHelper' import { calculateBlobChecksum, @@ -1291,6 +1292,7 @@ describe('DatasetsRepository', () => { beforeAll(async () => { await createCollectionViaApi(testDatasetVersionsCollectionAlias) await publishCollectionViaApi(testDatasetVersionsCollectionAlias) + await setStorageDriverViaApi(testDatasetVersionsCollectionAlias, 'LocalStack') }) afterAll(async () => { diff --git a/test/unit/datasets/DatasetsRepository.test.ts b/test/unit/datasets/DatasetsRepository.test.ts index 3003d27a..b87e05de 100644 --- a/test/unit/datasets/DatasetsRepository.test.ts +++ b/test/unit/datasets/DatasetsRepository.test.ts @@ -12,7 +12,11 @@ import { createUpdateDatasetRequestPayload } from '../../testHelpers/datasets/datasetHelper' import { TestConstants } from '../../testHelpers/TestConstants' -import { DatasetNotNumberedVersion, DatasetPreviewSubset } from '../../../src/datasets' +import { + DatasetNotNumberedVersion, + DatasetPreviewSubset, + DatasetVersionSummarySubset +} from '../../../src/datasets' import { createDatasetUserPermissionsModel } from '../../testHelpers/datasets/datasetUserPermissionsHelper' import { createDatasetLockModel, @@ -1050,13 +1054,16 @@ describe('DatasetsRepository', () => { }) describe('getDatasetVersionSummaries', () => { - const testDatasetVersionSummaries = createDatasetVersionSummaryModel() + const testDatasetVersionSummariesSubset: DatasetVersionSummarySubset = { + summaries: [createDatasetVersionSummaryModel()], + totalCount: 1 + } const testDatasetVersionSummariesResponse = { data: { status: 'OK', - data: [testDatasetVersionSummaries], - totalCount: 1 + data: testDatasetVersionSummariesSubset.summaries, + totalCount: testDatasetVersionSummariesSubset.totalCount } } @@ -1069,28 +1076,37 @@ describe('DatasetsRepository', () => { // API Key auth let actual = await sut.getDatasetVersionsSummaries(testDatasetModel.id) - expect(axios.get).toHaveBeenCalledWith( + const expectedRequestParams = new URLSearchParams() + + const expectedRequestConfigApiKey = { + params: expectedRequestParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + + expect(axios.get).toHaveBeenNthCalledWith( + 1, expectedApiEndpoint, - TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + expectedRequestConfigApiKey ) - expect(actual).toStrictEqual({ - summaries: [testDatasetVersionSummaries], - totalCount: 1 - }) + expect(actual).toStrictEqual(testDatasetVersionSummariesSubset) // Session cookie auth ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) actual = await sut.getDatasetVersionsSummaries(testDatasetModel.id) - expect(axios.get).toHaveBeenCalledWith( + const expectedRequestConfigSessionCookie = { + params: expectedRequestParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE.headers, + withCredentials: true + } + + expect(axios.get).toHaveBeenNthCalledWith( + 2, expectedApiEndpoint, - TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE + expectedRequestConfigSessionCookie ) - expect(actual).toStrictEqual({ - summaries: [testDatasetVersionSummaries], - totalCount: 1 - }) + expect(actual).toStrictEqual(testDatasetVersionSummariesSubset) }) test('should return error result on error response', async () => { @@ -1099,10 +1115,14 @@ describe('DatasetsRepository', () => { let error = undefined as unknown as ReadError await sut.getDatasetVersionsSummaries(testDatasetModel.id).catch((e) => (error = e)) - expect(axios.get).toHaveBeenCalledWith( - expectedApiEndpoint, - TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY - ) + const expectedRequestParams = new URLSearchParams() + + const expectedRequestConfigApiKey = { + params: expectedRequestParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + + expect(axios.get).toHaveBeenCalledWith(expectedApiEndpoint, expectedRequestConfigApiKey) expect(error).toBeInstanceOf(ReadError) }) }) @@ -1116,28 +1136,37 @@ describe('DatasetsRepository', () => { // API Key auth let actual = await sut.getDatasetVersionsSummaries(TestConstants.TEST_DUMMY_PERSISTENT_ID) - expect(axios.get).toHaveBeenCalledWith( + const expectedRequestParams = new URLSearchParams() + + const expectedRequestConfigApiKey = { + params: expectedRequestParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + + expect(axios.get).toHaveBeenNthCalledWith( + 1, expectedApiEndpoint, - TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + expectedRequestConfigApiKey ) - expect(actual).toStrictEqual({ - summaries: [testDatasetVersionSummaries], - totalCount: 1 - }) + expect(actual).toStrictEqual(testDatasetVersionSummariesSubset) // Session cookie auth ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) actual = await sut.getDatasetVersionsSummaries(TestConstants.TEST_DUMMY_PERSISTENT_ID) - expect(axios.get).toHaveBeenCalledWith( + const expectedRequestConfigSessionCookie = { + params: expectedRequestParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE.headers, + withCredentials: true + } + + expect(axios.get).toHaveBeenNthCalledWith( + 2, expectedApiEndpoint, - TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE + expectedRequestConfigSessionCookie ) - expect(actual).toStrictEqual({ - summaries: [testDatasetVersionSummaries], - totalCount: 1 - }) + expect(actual).toStrictEqual(testDatasetVersionSummariesSubset) }) test('should return error result on error response', async () => { @@ -1148,10 +1177,14 @@ describe('DatasetsRepository', () => { .getDatasetVersionsSummaries(TestConstants.TEST_DUMMY_PERSISTENT_ID) .catch((e) => (error = e)) - expect(axios.get).toHaveBeenCalledWith( - expectedApiEndpoint, - TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY - ) + const expectedRequestParams = new URLSearchParams() + + const expectedRequestConfigApiKey = { + params: expectedRequestParams, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + + expect(axios.get).toHaveBeenCalledWith(expectedApiEndpoint, expectedRequestConfigApiKey) expect(error).toBeInstanceOf(ReadError) }) }) diff --git a/test/unit/files/GetFileVersionSummaries.test.ts b/test/unit/files/GetFileVersionSummaries.test.ts index 709ac19e..f2f29ab9 100644 --- a/test/unit/files/GetFileVersionSummaries.test.ts +++ b/test/unit/files/GetFileVersionSummaries.test.ts @@ -7,7 +7,6 @@ describe('execute', () => { test('should return file on repository success when passing numeric id', async () => { const filesRepositoryStub: IFilesRepository = {} as IFilesRepository const fileVersionSummariesSubset: FileVersionSummarySubset = { - totalCount: 1, summaries: [ { datasetVersion: '1.0', From 061ad8ef5717c3f69a8b9ba92c7868fe341c0e0b Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 29 Oct 2025 16:28:08 -0400 Subject: [PATCH 041/110] fix: remove a console.log --- test/integration/datasets/DatasetsRepository.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index ff8da4f7..5513314f 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1306,7 +1306,6 @@ describe('DatasetsRepository', () => { ) const actual = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - console.log('actual summaries', actual) expect(actual.summaries.length).toBeGreaterThan(0) expect(actual.totalCount).toBeGreaterThan(0) From b6d38254c3559b2916d14417f0a142fa6535430f Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 31 Oct 2025 15:52:34 -0400 Subject: [PATCH 042/110] chore: remove console.log --- test/integration/datasets/DatasetsRepository.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index ab469334..342e3bb7 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1525,7 +1525,6 @@ describe('DatasetsRepository', () => { } const summaries = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - console.log('summaries', summaries) const firstPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 0) From 25bb338b9d974200ef1d5761fad603789ae44b3e Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 31 Oct 2025 16:07:15 -0400 Subject: [PATCH 043/110] chore: remove unused line --- test/integration/datasets/DatasetsRepository.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 342e3bb7..8c2b8712 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -1524,8 +1524,6 @@ describe('DatasetsRepository', () => { await waitForNoLocks(testDatasetIds.numericId, 10) } - const summaries = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId) - const firstPage = await sut.getDatasetVersionsSummaries(testDatasetIds.numericId, 5, 0) expect(firstPage.summaries.length).toBe(5) From a6284c6db3d3cc4f8348d08246ae9536f5b7b717 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:19:15 +0000 Subject: [PATCH 044/110] Initial plan From d6fb9024518d867068e082d2614ba3d10298eabd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:22:51 +0000 Subject: [PATCH 045/110] docs: change absolute GitHub blob links to relative links in README.md Co-authored-by: ekraffmiller <675224+ekraffmiller@users.noreply.github.com> --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 44b2266d..69ee09e0 100644 --- a/README.md +++ b/README.md @@ -36,18 +36,18 @@ getDataset.execute(datasetIdentifier, datasetVersion).then((dataset: Dataset) => /* ... */ ``` -For detailed information about available use cases see [Use Cases Docs](https://github.com/IQSS/dataverse-client-javascript/blob/main/docs/useCases.md). +For detailed information about available use cases see [Use Cases Docs](docs/useCases.md). -For detailed information about usage see [Usage Docs](https://github.com/IQSS/dataverse-client-javascript/blob/main/docs/usage.md). +For detailed information about usage see [Usage Docs](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. +See [CHANGELOG.md](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. +Want to add a new use case or improve an existing one? Please check the [Contributing](CONTRIBUTING.md) section. ## License -This project is open source and available under the [MIT License](https://github.com/IQSS/dataverse-client-javascript/blob/main/LICENSE). +This project is open source and available under the [MIT License](LICENSE). From 341f655a483a15ccd7a35539b8ea3af8913f6429 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 2 Dec 2025 15:32:47 -0500 Subject: [PATCH 046/110] fix NotificationsRepository.test.ts: use persistentId to test the correct notification. --- .../NotificationsRepository.test.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index f1378a83..37560880 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -37,29 +37,24 @@ describe('NotificationsRepository', () => { await publishDatasetViaApi(testDatasetIds.numericId) await waitForNoLocks(testDatasetIds.numericId, 10) - const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser() + const notificationSubset: NotificationSubset = await sut.getAllNotificationsByUser(true) expect(Array.isArray(notificationSubset.notifications)).toBe(true) expect(notificationSubset.notifications.length).toBeGreaterThan(0) const publishedNotification = notificationSubset.notifications.find( - (n) => n.type === NotificationType.PUBLISHEDDS + (n) => + n.datasetPersistentIdentifier === testDatasetIds.persistentId && + n.type === NotificationType.PUBLISHEDDS ) as Notification 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 "${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}" has been published` - ) - expect(publishedNotification?.messageText).toContain( - `Your dataset named ${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}` + expect(publishedNotification?.datasetDisplayName).toContain( + `${TestConstants.TEST_NEW_DATASET_DTO.metadataBlockValues[0].fields.title}` ) }) From 10460ef3fad981389670553b828670e0b9c6fa27 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 4 Dec 2025 15:37:35 -0500 Subject: [PATCH 047/110] update .env: change DATAVERSE_IMAGE_TAG to unstable --- test/environment/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/environment/.env b/test/environment/.env index 7d1dda97..fad1c7af 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -2,5 +2,5 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=11852-notifs-api-pagination-unread +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 8b77e8f90c1b5b13188c71d1a3702084ea7fcd33 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 4 Dec 2025 15:43:59 -0500 Subject: [PATCH 048/110] update .env: change DATAVERSE_IMAGE_TAG to 11852-notifs-api-pagination-unread --- test/environment/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/environment/.env b/test/environment/.env index fad1c7af..7d1dda97 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -2,5 +2,5 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_TAG=11852-notifs-api-pagination-unread DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 29e036a55e42c3069c42975b4b7255848ce58bb9 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosca Villanueva Date: Thu, 4 Dec 2025 16:00:04 -0500 Subject: [PATCH 049/110] update .env: change DATAVERSE_IMAGE_REGISTRY to docker.io and reset DATAVERSE_IMAGE_TAG to unstable --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index 7d1dda97..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=11852-notifs-api-pagination-unread +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m From 2a1a04c6d97690941a1b75c4a4d8fbe3686db797 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 4 Dec 2025 17:01:04 -0500 Subject: [PATCH 050/110] fix CollectionsRepository.test.ts: use persistentId to test the correct notification. --- .../collections/CollectionsRepository.test.ts | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index ce080777..c341beb7 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -380,9 +380,14 @@ describe('CollectionsRepository', () => { const testTextFile1Name = 'test-file-1.txt' const testSubCollectionAlias = 'collectionsRepositoryTestSubCollection' - + const testCollectionItemsAlias = 'collectionsRepositoryTestCollectionItems' beforeAll(async () => { - await createCollectionViaApi(testSubCollectionAlias, testCollectionAlias).catch(() => { + await createCollectionViaApi(testCollectionItemsAlias, ROOT_COLLECTION_ALIAS).catch(() => { + throw new Error( + `Tests beforeAll(): Error while creating collection ${testCollectionItemsAlias}` + ) + }) + await createCollectionViaApi(testSubCollectionAlias, testCollectionItemsAlias).catch(() => { throw new Error( `Tests beforeAll(): Error while creating subcollection ${testSubCollectionAlias}` ) @@ -421,7 +426,7 @@ describe('CollectionsRepository', () => { // Give enough time to Solr for indexing await new Promise((resolve) => setTimeout(resolve, 5000)) - let actual = await sut.getCollectionItems(testCollectionAlias) + let actual = await sut.getCollectionItems(testCollectionItemsAlias) const actualFilePreview = actual.items[1] as FilePreview const actualDatasetPreview = actual.items[0] as DatasetPreview const actualCollectionPreview = actual.items[2] as CollectionPreview @@ -539,12 +544,12 @@ describe('CollectionsRepository', () => { expect(actualCollectionPreview.alias).toBe(testSubCollectionAlias) expect(actualCollectionPreview.description).toBe('We do all the science.') expect(actualCollectionPreview.imageUrl).toBe(undefined) - expect(actualCollectionPreview.parentAlias).toBe(testCollectionAlias) + expect(actualCollectionPreview.parentAlias).toBe(testCollectionItemsAlias) expect(actualCollectionPreview.parentName).toBe(expectedCollectionsName) expect(actualCollectionPreview.publicationStatuses).toContain(PublicationStatus.Unpublished) expect(actualCollectionPreview.releaseOrCreateDate).not.toBeUndefined() expect(actualCollectionPreview.affiliation).toBe('Scientific Research University') - expect(actualCollectionPreview.parentAlias).toBe('collectionsRepositoryTestCollection') + expect(actualCollectionPreview.parentAlias).toBe('collectionsRepositoryTestCollectionItems') expect(actualCollectionPreview.parentName).toBe(expectedCollectionsName) expect(actualCollectionPreview.type).toBe(CollectionItemType.COLLECTION) @@ -553,7 +558,7 @@ describe('CollectionsRepository', () => { expect(actual.facets).toEqual(expectedFacetsAll) // Test limit and offset - actual = await sut.getCollectionItems(testCollectionAlias, 1, 1) + actual = await sut.getCollectionItems(testCollectionItemsAlias, 1, 1) expect((actual.items[0] as FilePreview).name).toBe(expectedFileName) expect(actual.items.length).toBe(1) expect(actual.totalItemCount).toBe(3) @@ -563,7 +568,7 @@ describe('CollectionsRepository', () => { 'test-fi' ) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaForFile @@ -575,7 +580,7 @@ describe('CollectionsRepository', () => { 'Dataset created using' ) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaForDataset @@ -587,7 +592,7 @@ describe('CollectionsRepository', () => { const collectionSearchCriteriaForDatasetAndCollection = new CollectionSearchCriteria().withSearchText('the') actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaForDatasetAndCollection @@ -598,7 +603,7 @@ describe('CollectionsRepository', () => { // Test search text, limit and offset actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, 1, 1, collectionSearchCriteriaForDatasetAndCollection @@ -611,7 +616,7 @@ describe('CollectionsRepository', () => { const collectionSearchCriteriaForCollectionType = new CollectionSearchCriteria().withItemTypes([CollectionItemType.COLLECTION]) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaForCollectionType @@ -626,7 +631,7 @@ describe('CollectionsRepository', () => { CollectionItemType.DATASET ]) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaForDatasetType @@ -641,7 +646,7 @@ describe('CollectionsRepository', () => { CollectionItemType.FILE ]) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaForFileType @@ -657,7 +662,7 @@ describe('CollectionsRepository', () => { CollectionItemType.COLLECTION ]) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaForMultiTypes @@ -674,7 +679,7 @@ describe('CollectionsRepository', () => { .withOrder(OrderType.ASC) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaNameAscending @@ -691,7 +696,7 @@ describe('CollectionsRepository', () => { .withOrder(OrderType.DESC) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaNameDescending @@ -708,7 +713,7 @@ describe('CollectionsRepository', () => { .withOrder(OrderType.ASC) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaDateAscending @@ -725,7 +730,7 @@ describe('CollectionsRepository', () => { .withOrder(OrderType.DESC) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaDateDescending @@ -741,7 +746,7 @@ describe('CollectionsRepository', () => { new CollectionSearchCriteria().withFilterQueries(['dvCategory:Laboratory']) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaFilterQueryCollection @@ -758,7 +763,7 @@ describe('CollectionsRepository', () => { ]) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaFilterQueryDataset @@ -773,7 +778,7 @@ describe('CollectionsRepository', () => { new CollectionSearchCriteria().withFilterQueries(['fileAccess:Public']) actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, collectionSearchCriteriaFilterQuerieCollAndFile @@ -786,7 +791,7 @@ describe('CollectionsRepository', () => { // Test with showTypeCounts param in true actual = await sut.getCollectionItems( - testCollectionAlias, + testCollectionItemsAlias, undefined, undefined, undefined, From 83d52ae0d82e60bea35c48cadbee3626293c97f3 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 5 Dec 2025 14:28:05 -0500 Subject: [PATCH 051/110] fix CollectionsRepository.test.ts: remove test of datasetCitation from filePreview (not needed) --- test/integration/collections/CollectionsRepository.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index c341beb7..0426e47e 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -501,7 +501,6 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.checksum?.type).toBe('MD5') expect(actualFilePreview.checksum?.value).toBe(expectedFileMd5) - expect(actualFilePreview.datasetCitation).toContain(expectedDatasetCitationFragment) expect(actualFilePreview.datasetId).toBe(testDatasetIds.numericId) expect(actualFilePreview.datasetName).toBe(expectedDatasetDescription) expect(actualFilePreview.datasetPersistentId).toBe(testDatasetIds.persistentId) @@ -1002,7 +1001,6 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.checksum?.type).toBe('MD5') expect(actualFilePreview.checksum?.value).toBe(expectedFileMd5) - expect(actualFilePreview.datasetCitation).toContain(expectedDatasetCitationFragment) expect(actualFilePreview.datasetId).toBe(testDatasetIds.numericId) expect(actualFilePreview.datasetName).toBe(expectedDatasetDescription) expect(actualFilePreview.datasetPersistentId).toBe(testDatasetIds.persistentId) From 160d58bc0df3686e8e431f4bd3dbc0c9f6fd7acf Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 5 Dec 2025 14:37:09 -0500 Subject: [PATCH 052/110] fix CollectionsRepository.test.ts: remove additional test of datasetCitation from filePreview --- test/integration/collections/CollectionsRepository.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 0426e47e..58eef058 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -1802,7 +1802,6 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.checksum?.type).toBe('MD5') expect(actualFilePreview.checksum?.value).toBeDefined() - expect(actualFilePreview.datasetCitation).toContain(expectedDatasetCitationFragment) expect(actualFilePreview.datasetId).toBe(testDatasetIds.numericId) expect(actualFilePreview.datasetName).toBe(expectedDatasetDescription) expect(actualFilePreview.datasetPersistentId).toBe(testDatasetIds.persistentId) From 6eba2d8cf87344341a7befd42ba03727ad396c88 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Fri, 5 Dec 2025 17:32:07 -0500 Subject: [PATCH 053/110] fix CollectionsRepository.test.ts: comment out tests that are affected by displayOrder bug --- .../collections/CollectionsRepository.test.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 58eef058..f42906d1 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -68,7 +68,8 @@ describe('CollectionsRepository', () => { const testCollectionAlias = 'collectionsRepositoryTestCollection' const sut: CollectionsRepository = new CollectionsRepository() let testCollectionId: number - const currentYear = new Date().getFullYear() + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // const currentYear = new Date().getFullYear() beforeAll(async () => { // create builtin user and pass API key to APiConfig @@ -432,7 +433,8 @@ describe('CollectionsRepository', () => { const actualCollectionPreview = actual.items[2] as CollectionPreview const expectedFileMd5 = '68b22040025784da775f55cfcb6dee2e' - const expectedDatasetCitationFragment = `Admin, Dataverse; Owner, Dataverse, ${currentYear}, "Dataset created using the createDataset use case"` + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // const expectedDatasetCitationFragment = `Admin, Dataverse; Owner, Dataverse, ${currentYear}, "Dataset created using the createDataset use case"` const expectedDatasetDescription = 'Dataset created using the createDataset use case' const expectedFileName = 'test-file-1.txt' const expectedCollectionsName = 'Scientific Research' @@ -501,6 +503,8 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.checksum?.type).toBe('MD5') expect(actualFilePreview.checksum?.value).toBe(expectedFileMd5) + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // expect(actualFilePreview.datasetCitation).toContain(expectedDatasetCitationFragment) expect(actualFilePreview.datasetId).toBe(testDatasetIds.numericId) expect(actualFilePreview.datasetName).toBe(expectedDatasetDescription) expect(actualFilePreview.datasetPersistentId).toBe(testDatasetIds.persistentId) @@ -521,7 +525,8 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.canDownloadFile).toBe(true) expect(actualDatasetPreview.title).toBe(expectedDatasetDescription) - expect(actualDatasetPreview.citation).toContain(expectedDatasetCitationFragment) + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // expect(actualDatasetPreview.citation).toContain(expectedDatasetCitationFragment) expect(actualDatasetPreview.description).toBe('This is the description of the dataset.') expect(actualDatasetPreview.persistentId).not.toBeUndefined() expect(actualDatasetPreview.persistentId).not.toBeUndefined() @@ -994,13 +999,16 @@ describe('CollectionsRepository', () => { const expectedFileMd5 = '77c7f03a7d7772907b43f0b322cef723' - const expectedDatasetCitationFragment = `Admin, Dataverse; Owner, Dataverse, ${currentYear}, "Dataset created using the createDataset use case` + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // const expectedDatasetCitationFragment = `Admin, Dataverse; Owner, Dataverse, ${currentYear}, "Dataset created using the createDataset use case` const expectedDatasetDescription = 'Dataset created using the createDataset use case' const expectedFileName = 'test-file-4.tab' const expectedCollectionsName = 'Scientific Research' expect(actualFilePreview.checksum?.type).toBe('MD5') expect(actualFilePreview.checksum?.value).toBe(expectedFileMd5) + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // expect(actualFilePreview.datasetCitation).toContain(expectedDatasetCitationFragment) expect(actualFilePreview.datasetId).toBe(testDatasetIds.numericId) expect(actualFilePreview.datasetName).toBe(expectedDatasetDescription) expect(actualFilePreview.datasetPersistentId).toBe(testDatasetIds.persistentId) @@ -1023,7 +1031,8 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.variables).toBe(3) expect(actualDatasetPreview.title).toBe(expectedDatasetDescription) - expect(actualDatasetPreview.citation).toContain(expectedDatasetCitationFragment) + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // expect(actualDatasetPreview.citation).toContain(expectedDatasetCitationFragment) expect(actualDatasetPreview.description).toBe('This is the description of the dataset.') expect(actualDatasetPreview.persistentId).not.toBeUndefined() expect(actualDatasetPreview.persistentId).not.toBeUndefined() @@ -1781,7 +1790,8 @@ describe('CollectionsRepository', () => { ) as CollectionPreview const expectedFileMd5 = '799b5c8c5fdcfbd56c3943f7a6c35326' - const expectedDatasetCitationFragment = `Admin, Dataverse; Owner, Dataverse, ${currentYear}, "Dataset created using the createDataset use case"` + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // const expectedDatasetCitationFragment = `Admin, Dataverse; Owner, Dataverse, ${currentYear}, "Dataset created using the createDataset use case"` const expectedDatasetDescription = 'Dataset created using the createDataset use case' const expectedFileName = 'test-file-2.txt' const expectedCollectionsName = 'Test Collection' @@ -1802,6 +1812,8 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.checksum?.type).toBe('MD5') expect(actualFilePreview.checksum?.value).toBeDefined() + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // expect(actualFilePreview.datasetCitation).toContain(expectedDatasetCitationFragment) expect(actualFilePreview.datasetId).toBe(testDatasetIds.numericId) expect(actualFilePreview.datasetName).toBe(expectedDatasetDescription) expect(actualFilePreview.datasetPersistentId).toBe(testDatasetIds.persistentId) @@ -1821,7 +1833,8 @@ describe('CollectionsRepository', () => { expect(actualFilePreview.canDownloadFile).toBe(true) expect(actualDatasetPreview.title).toBe(expectedDatasetDescription) - expect(actualDatasetPreview.citation).toContain(expectedDatasetCitationFragment) + // TODO: uncomment this test when https://github.com/IQSS/dataverse/issues/12027 is fixed + // expect(actualDatasetPreview.citation).toContain(expectedDatasetCitationFragment) expect(actualDatasetPreview.description).toBe('This is the description of the dataset.') expect(actualDatasetPreview.persistentId).not.toBeUndefined() expect(actualDatasetPreview.persistentId).not.toBeUndefined() From 36b189f54e9dfa14947223a33df3aa6ed04343be Mon Sep 17 00:00:00 2001 From: Cheng Shi <91049239+ChengShi-1@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:44:08 -0500 Subject: [PATCH 054/110] get storage driver use case --- CHANGELOG.md | 1 + docs/useCases.md | 25 +++++++++ src/datasets/domain/models/StorageDriver.ts | 8 +++ .../repositories/IDatasetsRepository.ts | 2 + .../useCases/GetDatasetStorageDriver.ts | 21 ++++++++ src/datasets/index.ts | 5 +- .../infra/repositories/DatasetsRepository.ts | 12 +++++ .../datasets/DatasetsRepository.test.ts | 24 +++++++++ .../datasets/GetDatasetStorageDriver.test.ts | 51 +++++++++++++++++++ 9 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/datasets/domain/models/StorageDriver.ts create mode 100644 src/datasets/domain/useCases/GetDatasetStorageDriver.ts create mode 100644 test/unit/datasets/GetDatasetStorageDriver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 56926bf8..2fcef063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added +- Datasets: Added `getDatasetStorageDriver` use case and repository method to support Dataverse endpoint `GET /datasets/{identifier}/storageDriver`, for retrieving dataset storage driver configuration with properties: name, type, label, directUpload, directDownload, and uploadOutOfBand. - Datasets: Added `updateDatasetLicense` use case and repository method to support Dataverse endpoint `PUT /datasets/{id}/license`, for updating dataset license or custom terms - New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). - New Use Case: [Create a Dataset Template](./docs/useCases.md#create-a-dataset-template) under Collections. diff --git a/docs/useCases.md b/docs/useCases.md index 3254e4a5..0e6e0e5f 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -41,6 +41,7 @@ The different use cases currently available in the package are classified below, - [Get Dataset Linked Collections](#get-dataset-linked-collections) - [Get Dataset Available Categories](#get-dataset-available-categories) - [Get Dataset Templates](#get-dataset-templates) + - [Get Dataset Storage Driver](#get-dataset-storage-driver) - [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) @@ -1351,6 +1352,30 @@ getDatasetTemplates.execute(collectionIdOrAlias).then((datasetTemplates: Dataset _See [use case](../src/datasets/domain/useCases/GetDatasetTemplates.ts)_ definition. +#### Get Dataset Storage Driver + +Returns a [StorageDriver](../src/datasets/domain/models/StorageDriver.ts) instance with storage driver configuration for a dataset, including properties like name, type, label, and upload/download capabilities. + +##### Example call: + +```typescript +import { getDatasetStorageDriver } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const datasetId = 'doi:10.77777/FK2/AAAAAA' + +getDatasetStorageDriver.execute(datasetId).then((storageDriver: StorageDriver) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/datasets/domain/useCases/GetDatasetStorageDriver.ts)_ implementation\_. + +The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. + #### Add a Dataset Type Adds a dataset types that can be used at dataset creation. diff --git a/src/datasets/domain/models/StorageDriver.ts b/src/datasets/domain/models/StorageDriver.ts new file mode 100644 index 00000000..9c04b400 --- /dev/null +++ b/src/datasets/domain/models/StorageDriver.ts @@ -0,0 +1,8 @@ +export interface StorageDriver { + name: string + type: string + label: string + directUpload: boolean + directDownload: boolean + uploadOutOfBand: boolean +} diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 8a52f8f9..48e29de6 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -17,6 +17,7 @@ import { DatasetType } from '../models/DatasetType' import { TermsOfAccess } from '../models/Dataset' import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest' import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO' +import { StorageDriver } from '../models/StorageDriver' export interface IDatasetsRepository { getDataset( @@ -102,4 +103,5 @@ export interface IDatasetsRepository { datasetId: number | string, payload: DatasetLicenseUpdateRequest ): Promise + getDatasetStorageDriver(datasetId: number | string): Promise } diff --git a/src/datasets/domain/useCases/GetDatasetStorageDriver.ts b/src/datasets/domain/useCases/GetDatasetStorageDriver.ts new file mode 100644 index 00000000..361d1db9 --- /dev/null +++ b/src/datasets/domain/useCases/GetDatasetStorageDriver.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IDatasetsRepository } from '../repositories/IDatasetsRepository' +import { StorageDriver } from '../models/StorageDriver' + +export class GetDatasetStorageDriver implements UseCase { + private datasetsRepository: IDatasetsRepository + + constructor(datasetsRepository: IDatasetsRepository) { + this.datasetsRepository = datasetsRepository + } + + /** + * Returns the storage driver information for a given 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 this.datasetsRepository.getDatasetStorageDriver(datasetId) + } +} diff --git a/src/datasets/index.ts b/src/datasets/index.ts index b8edb5b3..ffb00af4 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -34,6 +34,7 @@ import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCi import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' import { UpdateTermsOfAccess } from './domain/useCases/UpdateTermsOfAccess' import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense' +import { GetDatasetStorageDriver } from './domain/useCases/GetDatasetStorageDriver' const datasetsRepository = new DatasetsRepository() @@ -84,6 +85,7 @@ const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(da const getDatasetTemplates = new GetDatasetTemplates(datasetsRepository) const updateTermsOfAccess = new UpdateTermsOfAccess(datasetsRepository) const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository) +const getDatasetStorageDriver = new GetDatasetStorageDriver(datasetsRepository) export { getDataset, @@ -115,7 +117,8 @@ export { linkDatasetTypeWithMetadataBlocks, setAvailableLicensesForDatasetType, deleteDatasetType, - updateDatasetLicense + updateDatasetLicense, + getDatasetStorageDriver } 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 849cf658..c50b30e1 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -33,6 +33,7 @@ import { TermsOfAccess } from '../../domain/models/Dataset' import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAccessTransformers' import { DatasetLicenseUpdateRequest } from '../../domain/dtos/DatasetLicenseUpdateRequest' import { DatasetTypeDTO } from '../../domain/dtos/DatasetTypeDTO' +import { StorageDriver } from '../../domain/models/StorageDriver' export interface GetAllDatasetPreviewsQueryParams { per_page?: number @@ -515,4 +516,15 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi throw error }) } + + public async getDatasetStorageDriver(datasetId: number | string): Promise { + return this.doGet( + this.buildApiEndpoint(this.datasetsResourceName, 'storageDriver', datasetId), + true + ) + .then((response) => response.data.data as StorageDriver) + .catch((error) => { + throw error + }) + } } diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 5e3fa4b1..0f472b1f 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -2244,4 +2244,28 @@ describe('DatasetsRepository', () => { await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) }) }) + + describe('getDatasetStorageDriver', () => { + let testDatasetIds: CreatedDatasetIdentifiers + + beforeAll(async () => { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + }) + + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + }) + + test('should return storage driver info for dataset', async () => { + const storageDriver = await sut.getDatasetStorageDriver(testDatasetIds.numericId) + expect(storageDriver).toHaveProperty('name') + expect(storageDriver).toHaveProperty('type') + expect(storageDriver).toHaveProperty('label') + expect(typeof storageDriver.directUpload).toBe('boolean') + expect(typeof storageDriver.directDownload).toBe('boolean') + expect(typeof storageDriver.uploadOutOfBand).toBe('boolean') + }) + }) }) diff --git a/test/unit/datasets/GetDatasetStorageDriver.test.ts b/test/unit/datasets/GetDatasetStorageDriver.test.ts new file mode 100644 index 00000000..bd55d164 --- /dev/null +++ b/test/unit/datasets/GetDatasetStorageDriver.test.ts @@ -0,0 +1,51 @@ +import { GetDatasetStorageDriver } from '../../../src/datasets/domain/useCases/GetDatasetStorageDriver' +import { IDatasetsRepository } from '../../../src/datasets/domain/repositories/IDatasetsRepository' +import { StorageDriver } from '../../../src/datasets/domain/models/StorageDriver' +import { ReadError } from '../../../src/core/domain/repositories/ReadError' + +describe('GetDatasetStorageDriver (unit)', () => { + const testStorageDriver: StorageDriver = { + name: 'local', + type: 'filesystem', + label: 'Local Storage', + directUpload: true, + directDownload: true, + uploadOutOfBand: false + } + + test('should return storage driver on repository success', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.getDatasetStorageDriver = jest.fn().mockResolvedValue(testStorageDriver) + const sut = new GetDatasetStorageDriver(datasetsRepositoryStub) + + const actual = await sut.execute(1) + + expect(actual).toEqual(testStorageDriver) + expect(actual.name).toBe('local') + expect(actual.type).toBe('filesystem') + expect(actual.label).toBe('Local Storage') + expect(actual.directUpload).toBe(true) + expect(actual.directDownload).toBe(true) + expect(actual.uploadOutOfBand).toBe(false) + }) + + test('should return storage driver when using persistent id', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.getDatasetStorageDriver = jest.fn().mockResolvedValue(testStorageDriver) + const sut = new GetDatasetStorageDriver(datasetsRepositoryStub) + + const actual = await sut.execute('doi:10.77777/FK2/AAAAAA') + + expect(actual).toEqual(testStorageDriver) + }) + + test('should return error result on repository error', async () => { + const datasetsRepositoryStub: IDatasetsRepository = {} as IDatasetsRepository + datasetsRepositoryStub.getDatasetStorageDriver = jest + .fn() + .mockRejectedValue(new ReadError('[404] Dataset not found')) + const sut = new GetDatasetStorageDriver(datasetsRepositoryStub) + + await expect(sut.execute(1)).rejects.toThrow(ReadError) + }) +}) From e20c2cccb7025b28b46ebed0532854b1447570ba Mon Sep 17 00:00:00 2001 From: Cheng Shi <91049239+ChengShi-1@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:34:17 -0500 Subject: [PATCH 055/110] Export storagedriver --- docs/useCases.md | 2 +- src/datasets/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/useCases.md b/docs/useCases.md index 0e6e0e5f..ed6c72ed 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -1372,7 +1372,7 @@ getDatasetStorageDriver.execute(datasetId).then((storageDriver: StorageDriver) = /* ... */ ``` -_See [use case](../src/datasets/domain/useCases/GetDatasetStorageDriver.ts)_ implementation\_. +_See [use case](../src/datasets/domain/useCases/GetDatasetStorageDriver.ts) implementation_. The `datasetId` parameter can be a string, for persistent identifiers, or a number, for numeric identifiers. diff --git a/src/datasets/index.ts b/src/datasets/index.ts index ffb00af4..84b22e75 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -157,3 +157,4 @@ export { export { DatasetLinkedCollection } from './domain/models/DatasetLinkedCollection' export { DatasetType } from './domain/models/DatasetType' export { DatasetTypeDTO } from './domain/dtos/DatasetTypeDTO' +export { StorageDriver } from './domain/models/StorageDriver' From 243eda11a403ca1bed5374c478949a33f5e2a3f0 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 7 Jan 2026 15:40:27 -0500 Subject: [PATCH 056/110] feat: restructure template --- docs/useCases.md | 53 ++++---- .../repositories/ICollectionsRepository.ts | 5 - src/collections/index.ts | 5 +- .../repositories/CollectionsRepository.ts | 15 --- .../repositories/IDatasetsRepository.ts | 2 - src/datasets/index.ts | 3 - .../infra/repositories/DatasetsRepository.ts | 16 --- src/index.ts | 1 + .../domain/dtos/CreateDatasetTemplateDTO.ts | 0 .../domain/models/DatasetTemplate.ts | 2 +- .../repositories/ITemplatesRepository.ts | 10 ++ .../domain/useCases/CreateDatasetTemplate.ts | 12 +- .../domain/useCases/GetDatasetTemplates.ts | 10 +- src/template/index.ts | 20 ++++ .../infra/repositories/TemplatesRepository.ts | 37 ++++++ .../transformers/DatasetTemplatePayload.ts | 2 +- .../datasetTemplateTransformers.ts | 2 +- .../createDatasetTemplate.test.ts | 5 +- .../collections/CollectionsRepository.test.ts | 62 +--------- .../datasets/DatasetsRepository.test.ts | 39 ------ .../template/TemplateRepository.test.ts | 113 ++++++++++++++++++ .../datasets/datasetTemplatesHelper.ts | 2 +- .../unit/collections/createDatasetTemplate.ts | 46 ------- .../template/createDatasetTemplate.test.ts | 44 +++++++ 24 files changed, 275 insertions(+), 231 deletions(-) rename src/{collections => template}/domain/dtos/CreateDatasetTemplateDTO.ts (100%) rename src/{datasets => template}/domain/models/DatasetTemplate.ts (87%) create mode 100644 src/template/domain/repositories/ITemplatesRepository.ts rename src/{collections => template}/domain/useCases/CreateDatasetTemplate.ts (66%) rename src/{datasets => template}/domain/useCases/GetDatasetTemplates.ts (73%) create mode 100644 src/template/index.ts create mode 100644 src/template/infra/repositories/TemplatesRepository.ts rename src/{datasets => template}/infra/repositories/transformers/DatasetTemplatePayload.ts (93%) rename src/{datasets => template}/infra/repositories/transformers/datasetTemplateTransformers.ts (95%) rename test/functional/{collections => template}/createDatasetTemplate.test.ts (89%) create mode 100644 test/integration/template/TemplateRepository.test.ts delete mode 100644 test/unit/collections/createDatasetTemplate.ts create mode 100644 test/unit/template/createDatasetTemplate.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 3254e4a5..6bf629db 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -25,6 +25,10 @@ The different use cases currently available in the package are classified below, - [Update Collection Featured Items](#update-collection-featured-items) - [Delete Collection Featured Items](#delete-collection-featured-items) - [Delete a Collection Featured Item](#delete-a-collection-featured-item) +- [Templates](#Templates) + - [Templates read use cases](#templates-read-use-cases) + - [Get Dataset Templates](#get-dataset-templates) + - [Templates write use cases](#templates-write-use-cases) - [Create a Dataset Template](#create-a-dataset-template) - [Datasets](#Datasets) - [Datasets read use cases](#datasets-read-use-cases) @@ -40,7 +44,6 @@ 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) - [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) @@ -569,6 +572,30 @@ deleteCollectionFeaturedItem.execute(featuredItemId) _See [use case](../src/collections/domain/useCases/DeleteCollectionFeaturedItem.ts)_ definition. +## Templates + +### Templates Read Use Cases + +#### Get Dataset Templates + +Returns a [DatasetTemplate](../src/template/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/template/domain/useCases/GetDatasetTemplates.ts)_ definition. + +### Templates Write Use Cases + #### Create a Dataset Template Creates a dataset template for a given Dataverse collection id or alias. @@ -577,10 +604,10 @@ Creates a dataset template for a given Dataverse collection id or alias. ```typescript import { createDatasetTemplate } from '@iqss/dataverse-client-javascript' -import { TemplateCreateDTO } from '@iqss/dataverse-client-javascript' +import { CreateDatasetTemplateDTO } from '@iqss/dataverse-client-javascript' const collectionAlias = ':root' -const template: TemplateCreateDTO = { +const template: CreateDatasetTemplateDTO = { name: 'Dataverse template', isDefault: true, fields: [ @@ -602,7 +629,7 @@ const template: TemplateCreateDTO = { await createDatasetTemplate.execute(template, collectionAlias) ``` -_See [use case](../src/collections/domain/useCases/CreateDatasetTemplate.ts) implementation_. +_See [use case](../src/template/domain/useCases/CreateDatasetTemplate.ts) implementation_. ## Datasets @@ -1333,24 +1360,6 @@ _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. - #### Add a Dataset Type Adds a dataset types that can be used at dataset creation. diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index cae28415..bc8960c8 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -12,7 +12,6 @@ import { CollectionItemType } from '../../../collections/domain/models/Collectio import { CollectionLinks } from '../models/CollectionLinks' import { CollectionSummary } from '../models/CollectionSummary' import { LinkingObjectType } from '../useCases/GetCollectionsForLinking' -import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -69,8 +68,4 @@ export interface ICollectionsRepository { searchTerm: string, alreadyLinked: boolean ): Promise - createDatasetTemplate( - collectionIdOrAlias: number | string, - template: CreateDatasetTemplateDTO - ): Promise } diff --git a/src/collections/index.ts b/src/collections/index.ts index df7b6af5..59e2e50b 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -16,7 +16,6 @@ import { LinkCollection } from './domain/useCases/LinkCollection' import { UnlinkCollection } from './domain/useCases/UnlinkCollection' import { GetCollectionLinks } from './domain/useCases/GetCollectionLinks' import { GetCollectionsForLinking } from './domain/useCases/GetCollectionsForLinking' -import { CreateDatasetTemplate } from './domain/useCases/CreateDatasetTemplate' const collectionsRepository = new CollectionsRepository() @@ -37,7 +36,6 @@ const linkCollection = new LinkCollection(collectionsRepository) const unlinkCollection = new UnlinkCollection(collectionsRepository) const getCollectionLinks = new GetCollectionLinks(collectionsRepository) const getCollectionsForLinking = new GetCollectionsForLinking(collectionsRepository) -const createDatasetTemplate = new CreateDatasetTemplate(collectionsRepository) export { getCollection, @@ -56,8 +54,7 @@ export { linkCollection, unlinkCollection, getCollectionLinks, - getCollectionsForLinking, - createDatasetTemplate + getCollectionsForLinking } 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 53ebfff3..e0e459b0 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -40,7 +40,6 @@ 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' -import { CreateDatasetTemplateDTO } from '../../domain/dtos/CreateDatasetTemplateDTO' export interface NewCollectionRequestPayload { alias: string @@ -529,18 +528,4 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } - - public async createDatasetTemplate( - collectionIdOrAlias: number | string, - template: CreateDatasetTemplateDTO - ): Promise { - return this.doPost( - `/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, - template - ) - .then(() => undefined) - .catch((error) => { - throw error - }) - } } diff --git a/src/datasets/domain/repositories/IDatasetsRepository.ts b/src/datasets/domain/repositories/IDatasetsRepository.ts index 8a52f8f9..c0b0e670 100644 --- a/src/datasets/domain/repositories/IDatasetsRepository.ts +++ b/src/datasets/domain/repositories/IDatasetsRepository.ts @@ -12,7 +12,6 @@ import { DatasetVersionSummarySubset } from '../models/DatasetVersionSummaryInfo import { DatasetLinkedCollection } from '../models/DatasetLinkedCollection' import { CitationFormat } from '../models/CitationFormat' import { FormattedCitation } from '../models/FormattedCitation' -import { DatasetTemplate } from '../models/DatasetTemplate' import { DatasetType } from '../models/DatasetType' import { TermsOfAccess } from '../models/Dataset' import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest' @@ -84,7 +83,6 @@ export interface IDatasetsRepository { format: CitationFormat, includeDeaccessioned?: boolean ): Promise - getDatasetTemplates(collectionIdOrAlias: number | string): Promise getDatasetAvailableDatasetTypes(): Promise getDatasetAvailableDatasetType(datasetTypeId: number | string): Promise addDatasetType(datasetType: DatasetTypeDTO): Promise diff --git a/src/datasets/index.ts b/src/datasets/index.ts index b8edb5b3..fd267057 100644 --- a/src/datasets/index.ts +++ b/src/datasets/index.ts @@ -31,7 +31,6 @@ import { LinkDatasetTypeWithMetadataBlocks } from './domain/useCases/LinkDataset import { SetAvailableLicensesForDatasetType } from './domain/useCases/SetAvailableLicensesForDatasetType' import { DeleteDatasetType } from './domain/useCases/DeleteDatasetType' import { GetDatasetCitationInOtherFormats } from './domain/useCases/GetDatasetCitationInOtherFormats' -import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' import { UpdateTermsOfAccess } from './domain/useCases/UpdateTermsOfAccess' import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense' @@ -81,7 +80,6 @@ const setAvailableLicensesForDatasetType = new SetAvailableLicensesForDatasetTyp ) const deleteDatasetType = new DeleteDatasetType(datasetsRepository) const getDatasetCitationInOtherFormats = new GetDatasetCitationInOtherFormats(datasetsRepository) -const getDatasetTemplates = new GetDatasetTemplates(datasetsRepository) const updateTermsOfAccess = new UpdateTermsOfAccess(datasetsRepository) const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository) @@ -107,7 +105,6 @@ export { getDatasetLinkedCollections, getDatasetAvailableCategories, getDatasetCitationInOtherFormats, - getDatasetTemplates, updateTermsOfAccess, getDatasetAvailableDatasetTypes, getDatasetAvailableDatasetType, diff --git a/src/datasets/infra/repositories/DatasetsRepository.ts b/src/datasets/infra/repositories/DatasetsRepository.ts index 849cf658..c3b05c88 100644 --- a/src/datasets/infra/repositories/DatasetsRepository.ts +++ b/src/datasets/infra/repositories/DatasetsRepository.ts @@ -1,4 +1,3 @@ -import { AxiosResponse } from 'axios' import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { IDatasetsRepository } from '../../domain/repositories/IDatasetsRepository' import { Dataset, VersionUpdateType } from '../../domain/models/Dataset' @@ -25,9 +24,6 @@ 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' import { DatasetType } from '../../domain/models/DatasetType' import { TermsOfAccess } from '../../domain/models/Dataset' import { transformTermsOfAccessToUpdatePayload } from './transformers/termsOfAccessTransformers' @@ -402,18 +398,6 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi }) } - 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 - }) - } - public async getDatasetAvailableDatasetTypes(): Promise { return this.doGet(this.buildApiEndpoint(this.datasetsResourceName, 'datasetTypes')) .then((response) => response.data.data) diff --git a/src/index.ts b/src/index.ts index 9e64baa6..e7f4475a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,3 +12,4 @@ export * from './notifications' export * from './search' export * from './licenses' export * from './externalTools' +export * from './template' diff --git a/src/collections/domain/dtos/CreateDatasetTemplateDTO.ts b/src/template/domain/dtos/CreateDatasetTemplateDTO.ts similarity index 100% rename from src/collections/domain/dtos/CreateDatasetTemplateDTO.ts rename to src/template/domain/dtos/CreateDatasetTemplateDTO.ts diff --git a/src/datasets/domain/models/DatasetTemplate.ts b/src/template/domain/models/DatasetTemplate.ts similarity index 87% rename from src/datasets/domain/models/DatasetTemplate.ts rename to src/template/domain/models/DatasetTemplate.ts index 9be71f23..afe5d8d3 100644 --- a/src/datasets/domain/models/DatasetTemplate.ts +++ b/src/template/domain/models/DatasetTemplate.ts @@ -1,4 +1,4 @@ -import { DatasetMetadataBlock, TermsOfUse } from './Dataset' +import { DatasetMetadataBlock, TermsOfUse } from '../../../datasets/domain/models/Dataset' import { License } from '../../../licenses/domain/models/License' export interface DatasetTemplate { diff --git a/src/template/domain/repositories/ITemplatesRepository.ts b/src/template/domain/repositories/ITemplatesRepository.ts new file mode 100644 index 00000000..b05e9893 --- /dev/null +++ b/src/template/domain/repositories/ITemplatesRepository.ts @@ -0,0 +1,10 @@ +import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO' +import { DatasetTemplate } from '../models/DatasetTemplate' + +export interface ITemplatesRepository { + createDatasetTemplate( + collectionIdOrAlias: number | string, + template: CreateDatasetTemplateDTO + ): Promise + getDatasetTemplates(collectionIdOrAlias: number | string): Promise +} diff --git a/src/collections/domain/useCases/CreateDatasetTemplate.ts b/src/template/domain/useCases/CreateDatasetTemplate.ts similarity index 66% rename from src/collections/domain/useCases/CreateDatasetTemplate.ts rename to src/template/domain/useCases/CreateDatasetTemplate.ts index ffb443f6..9d9b1f77 100644 --- a/src/collections/domain/useCases/CreateDatasetTemplate.ts +++ b/src/template/domain/useCases/CreateDatasetTemplate.ts @@ -1,13 +1,13 @@ -import { ROOT_COLLECTION_ID } from '../models/Collection' +import { ROOT_COLLECTION_ID } from '../../../collections/domain/models/Collection' import { UseCase } from '../../../core/domain/useCases/UseCase' -import { ICollectionsRepository } from '../repositories/ICollectionsRepository' import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO' +import { ITemplatesRepository } from '../repositories/ITemplatesRepository' export class CreateDatasetTemplate implements UseCase { - private collectionsRepository: ICollectionsRepository + private templatesRepository: ITemplatesRepository - constructor(collectionsRepository: ICollectionsRepository) { - this.collectionsRepository = collectionsRepository + constructor(templatesRepository: ITemplatesRepository) { + this.templatesRepository = templatesRepository } /** @@ -22,6 +22,6 @@ export class CreateDatasetTemplate implements UseCase { template: CreateDatasetTemplateDTO, collectionIdOrAlias: number | string = ROOT_COLLECTION_ID ): Promise { - return await this.collectionsRepository.createDatasetTemplate(collectionIdOrAlias, template) + return await this.templatesRepository.createDatasetTemplate(collectionIdOrAlias, template) } } diff --git a/src/datasets/domain/useCases/GetDatasetTemplates.ts b/src/template/domain/useCases/GetDatasetTemplates.ts similarity index 73% rename from src/datasets/domain/useCases/GetDatasetTemplates.ts rename to src/template/domain/useCases/GetDatasetTemplates.ts index 6878e625..f73bf342 100644 --- a/src/datasets/domain/useCases/GetDatasetTemplates.ts +++ b/src/template/domain/useCases/GetDatasetTemplates.ts @@ -1,13 +1,13 @@ 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' +import { ITemplatesRepository } from '../repositories/ITemplatesRepository' export class GetDatasetTemplates implements UseCase { - private datasetsRepository: IDatasetsRepository + private templatesRepository: ITemplatesRepository - constructor(datasetsRepository: IDatasetsRepository) { - this.datasetsRepository = datasetsRepository + constructor(templatesRepository: ITemplatesRepository) { + this.templatesRepository = templatesRepository } /** @@ -20,6 +20,6 @@ export class GetDatasetTemplates implements UseCase { async execute( collectionIdOrAlias: number | string = ROOT_COLLECTION_ID ): Promise { - return await this.datasetsRepository.getDatasetTemplates(collectionIdOrAlias) + return await this.templatesRepository.getDatasetTemplates(collectionIdOrAlias) } } diff --git a/src/template/index.ts b/src/template/index.ts new file mode 100644 index 00000000..1bebaeef --- /dev/null +++ b/src/template/index.ts @@ -0,0 +1,20 @@ +import { TemplatesRepository } from './infra/repositories/TemplatesRepository' +import { CreateDatasetTemplate } from './domain/useCases/CreateDatasetTemplate' +import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' + +const templatesRepository = new TemplatesRepository() + +const createDatasetTemplate = new CreateDatasetTemplate(templatesRepository) +const getDatasetTemplates = new GetDatasetTemplates(templatesRepository) + +export { createDatasetTemplate, getDatasetTemplates } +export { + CreateDatasetTemplateDTO, + TemplateFieldDTO, + TemplateFieldValueDTO, + TemplateFieldValuePrimitiveDTO, + TemplateFieldValueCompoundDTO, + TemplateFieldValueControlledVocabularyDTO, + TemplateInstructionDTO +} from './domain/dtos/CreateDatasetTemplateDTO' +export { DatasetTemplate, DatasetTemplateInstruction } from './domain/models/DatasetTemplate' diff --git a/src/template/infra/repositories/TemplatesRepository.ts b/src/template/infra/repositories/TemplatesRepository.ts new file mode 100644 index 00000000..dc613e4c --- /dev/null +++ b/src/template/infra/repositories/TemplatesRepository.ts @@ -0,0 +1,37 @@ +import { AxiosResponse } from 'axios' +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { CreateDatasetTemplateDTO } from '../../domain/dtos/CreateDatasetTemplateDTO' +import { DatasetTemplate } from '../../domain/models/DatasetTemplate' +import { ITemplatesRepository } from '../../domain/repositories/ITemplatesRepository' +import { DatasetTemplatePayload } from './transformers/DatasetTemplatePayload' +import { transformDatasetTemplatePayloadToDatasetTemplate } from './transformers/datasetTemplateTransformers' + +export class TemplatesRepository extends ApiRepository implements ITemplatesRepository { + private readonly collectionsResourceName: string = 'dataverses' + + public async createDatasetTemplate( + collectionIdOrAlias: number | string, + template: CreateDatasetTemplateDTO + ): Promise { + return this.doPost( + `/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, + template + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async getDatasetTemplates( + collectionIdOrAlias: number | string + ): Promise { + return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, true) + .then((response: AxiosResponse<{ data: DatasetTemplatePayload[] }>) => + transformDatasetTemplatePayloadToDatasetTemplate(response.data.data) + ) + .catch((error) => { + throw error + }) + } +} diff --git a/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts b/src/template/infra/repositories/transformers/DatasetTemplatePayload.ts similarity index 93% rename from src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts rename to src/template/infra/repositories/transformers/DatasetTemplatePayload.ts index e43e96eb..9cd9792f 100644 --- a/src/datasets/infra/repositories/transformers/DatasetTemplatePayload.ts +++ b/src/template/infra/repositories/transformers/DatasetTemplatePayload.ts @@ -1,5 +1,5 @@ import { LicensePayload } from '../../../../licenses/domain/repositories/transformers/LicensePayload' -import { MetadataFieldPayload } from './DatasetPayload' +import { MetadataFieldPayload } from '../../../../datasets/infra/repositories/transformers/DatasetPayload' export interface DatasetTemplatePayload { id: number diff --git a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts b/src/template/infra/repositories/transformers/datasetTemplateTransformers.ts similarity index 95% rename from src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts rename to src/template/infra/repositories/transformers/datasetTemplateTransformers.ts index 32486199..d48496f8 100644 --- a/src/datasets/infra/repositories/transformers/datasetTemplateTransformers.ts +++ b/src/template/infra/repositories/transformers/datasetTemplateTransformers.ts @@ -1,7 +1,7 @@ import { transformPayloadLicenseToLicense } from '../../../../licenses/domain/repositories/transformers/licenseTransformers' import { DatasetTemplate } from '../../../domain/models/DatasetTemplate' import { DatasetTemplatePayload } from './DatasetTemplatePayload' -import { transformPayloadToDatasetMetadataBlocks } from './datasetTransformers' +import { transformPayloadToDatasetMetadataBlocks } from '../../../../datasets/infra/repositories/transformers/datasetTransformers' export const transformDatasetTemplatePayloadToDatasetTemplate = ( collectionDatasetTemplatePayload: DatasetTemplatePayload[] diff --git a/test/functional/collections/createDatasetTemplate.test.ts b/test/functional/template/createDatasetTemplate.test.ts similarity index 89% rename from test/functional/collections/createDatasetTemplate.test.ts rename to test/functional/template/createDatasetTemplate.test.ts index 84309e52..86fb373b 100644 --- a/test/functional/collections/createDatasetTemplate.test.ts +++ b/test/functional/template/createDatasetTemplate.test.ts @@ -1,9 +1,8 @@ import { ApiConfig } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' import { TestConstants } from '../../testHelpers/TestConstants' -import { getDatasetTemplates } from '../../../src/datasets' -import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO' -import { createDatasetTemplate } from '../../../src/collections' +import { createDatasetTemplate, getDatasetTemplates } from '../../../src/template' +import { CreateDatasetTemplateDTO } from '../../../src/template/domain/dtos/CreateDatasetTemplateDTO' import { MetadataFieldTypeClass } from '../../../src/metadataBlocks/domain/models/MetadataBlock' import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index f42906d1..94b62311 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -16,9 +16,7 @@ import { getDatasetFiles, restrictFile, deleteFile, - linkDataset, - createDatasetTemplate, - MetadataFieldTypeClass + linkDataset } from '../../../src' import { ApiConfig } from '../../../src' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -60,9 +58,6 @@ import { DvObjectFeaturedItemDTO, FeaturedItemsDTO } from '../../../src/collections/domain/dtos/FeaturedItemsDTO' -import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO' -import { getDatasetTemplates } from '../../../src/datasets' -import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' describe('CollectionsRepository', () => { const testCollectionAlias = 'collectionsRepositoryTestCollection' @@ -2163,59 +2158,4 @@ describe('CollectionsRepository', () => { await expect(sut.getCollectionLinks(invalidCollectionId)).rejects.toThrow(expectedError) }) }) - - describe('createDatasetTemplate', () => { - const templateDto: CreateDatasetTemplateDTO = { - name: 'CollectionsRepository template', - isDefault: true, - fields: [ - { - typeName: 'author', - typeClass: MetadataFieldTypeClass.Compound, - multiple: true, - value: [ - { - authorName: { - typeName: 'authorName', - typeClass: MetadataFieldTypeClass.Primitive, - value: 'Belicheck, Bill' - }, - authorAffiliation: { - typeName: 'authorIdentifierScheme', - typeClass: MetadataFieldTypeClass.Primitive, - value: 'ORCID' - } - } - ] - } - ], - instructions: [ - { - instructionField: 'author', - instructionText: 'The author data' - } - ] - } - test('should create a template in :root with provided JSON', async () => { - await createDatasetTemplate.execute(templateDto) - const templates = await getDatasetTemplates.execute(':root') - - expect(templates[templates.length - 1].name).toBe(templateDto.name) - expect(templates[templates.length - 1].isDefault).toBe(templateDto.isDefault) - expect(templates[templates.length - 1].instructions.length).toBe( - templateDto.instructions?.length ?? 0 - ) - - deleteDatasetTemplateViaApi(templates[templates.length - 1].id) - }) - - test('should return error when creating a template with invalidCollectionAlias', async () => { - const expectedError = new WriteError( - `[404] Can't find dataverse with identifier='invalidCollectionAlias'` - ) - await expect( - createDatasetTemplate.execute(templateDto, 'invalidCollectionAlias') - ).rejects.toThrow(expectedError) - }) - }) }) diff --git a/test/integration/datasets/DatasetsRepository.test.ts b/test/integration/datasets/DatasetsRepository.test.ts index 5e3fa4b1..0ffd48bb 100644 --- a/test/integration/datasets/DatasetsRepository.test.ts +++ b/test/integration/datasets/DatasetsRepository.test.ts @@ -62,10 +62,6 @@ 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: { @@ -1818,41 +1814,6 @@ describe('DatasetsRepository', () => { }) }) - 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) - }) - }) - describe('getDatasetAvailableDatasetTypes', () => { test('should return available dataset types', async () => { const actualDatasetTypes: DatasetType[] = await getDatasetAvailableDatasetTypes.execute() diff --git a/test/integration/template/TemplateRepository.test.ts b/test/integration/template/TemplateRepository.test.ts new file mode 100644 index 00000000..83432eb8 --- /dev/null +++ b/test/integration/template/TemplateRepository.test.ts @@ -0,0 +1,113 @@ +import { ApiConfig, MetadataFieldTypeClass, WriteError } from '../../../src' +import { createDatasetTemplate, getDatasetTemplates } from '../../../src/template' +import { CreateDatasetTemplateDTO } from '../../../src/template/domain/dtos/CreateDatasetTemplateDTO' +import { TemplatesRepository } from '../../../src/template/infra/repositories/TemplatesRepository' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { + createCollectionViaApi, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' +import { + createDatasetTemplateViaApi, + deleteDatasetTemplateViaApi +} from '../../testHelpers/datasets/datasetTemplatesHelper' + +describe('TemplatesRepository', () => { + const sut: TemplatesRepository = new TemplatesRepository() + const testCollectionAlias = 'testGetDatasetTemplates' + + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + await createCollectionViaApi(testCollectionAlias) + }) + + afterAll(async () => { + await deleteCollectionViaApi(testCollectionAlias) + }) + + describe('createDatasetTemplate', () => { + const templateDto: CreateDatasetTemplateDTO = { + name: 'CollectionsRepository template', + isDefault: true, + fields: [ + { + typeName: 'author', + typeClass: MetadataFieldTypeClass.Compound, + multiple: true, + value: [ + { + authorName: { + typeName: 'authorName', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'Belicheck, Bill' + }, + authorAffiliation: { + typeName: 'authorIdentifierScheme', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'ORCID' + } + } + ] + } + ], + instructions: [ + { + instructionField: 'author', + instructionText: 'The author data' + } + ] + } + + test('should create a template in :root with provided JSON', async () => { + await createDatasetTemplate.execute(templateDto) + const templates = await getDatasetTemplates.execute(':root') + + expect(templates[templates.length - 1].name).toBe(templateDto.name) + expect(templates[templates.length - 1].isDefault).toBe(templateDto.isDefault) + expect(templates[templates.length - 1].instructions.length).toBe( + templateDto.instructions?.length ?? 0 + ) + + deleteDatasetTemplateViaApi(templates[templates.length - 1].id) + }) + + test('should return error when creating a template with invalidCollectionAlias', async () => { + const expectedError = new WriteError( + `[404] Can't find dataverse with identifier='invalidCollectionAlias'` + ) + await expect( + createDatasetTemplate.execute(templateDto, 'invalidCollectionAlias') + ).rejects.toThrow(expectedError) + }) + }) + + describe('getDatasetTemplates', () => { + test('should return the right number of dataset templates', async () => { + const actual = await sut.getDatasetTemplates(testCollectionAlias) + + expect(actual.length).toBe(1) + }) + + 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 index 1cc87300..0ec2e22b 100644 --- a/test/testHelpers/datasets/datasetTemplatesHelper.ts +++ b/test/testHelpers/datasets/datasetTemplatesHelper.ts @@ -1,6 +1,6 @@ import axios from 'axios' import { TestConstants } from '../TestConstants' -import { DatasetTemplatePayload } from '../../../src/datasets/infra/repositories/transformers/DatasetTemplatePayload' +import { DatasetTemplatePayload } from '../../../src/template/infra/repositories/transformers/DatasetTemplatePayload' const DATASET_TEMPLATE_DTO = { name: 'Dataset Template', diff --git a/test/unit/collections/createDatasetTemplate.ts b/test/unit/collections/createDatasetTemplate.ts deleted file mode 100644 index 0004b7a7..00000000 --- a/test/unit/collections/createDatasetTemplate.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CreateDatasetTemplate } from '../../../src/collections/domain/useCases/CreateDatasetTemplate' -import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' -import { CreateDatasetTemplateDTO } from '../../../src/collections/domain/dtos/CreateDatasetTemplateDTO' -import { WriteError } from '../../../src' - -describe('execute', () => { - const testTemplateDTO = { name: 't' } as CreateDatasetTemplateDTO - const testCollectionId = 1 - - test('should return undefined when repository call is successful', async () => { - const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository - collectionRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(testCollectionId) - const sut = new CreateDatasetTemplate(collectionRepositoryStub) - - const actual = await sut.execute(testTemplateDTO) - - expect(collectionRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith( - ':root', - testTemplateDTO - ) - expect(actual).toEqual(testCollectionId) - }) - - test('should call repository with provided collection id/alias', async () => { - const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository - collectionRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(testCollectionId) - - const sut = new CreateDatasetTemplate(collectionRepositoryStub) - const actual = await sut.execute(testTemplateDTO, 'alias123') - - expect(collectionRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith( - 'alias123', - testTemplateDTO - ) - - expect(actual).toEqual(testCollectionId) - }) - - test('should return error result on repository error', async () => { - const collectionRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository - collectionRepositoryStub.createDatasetTemplate = jest.fn().mockRejectedValue(new WriteError()) - const testCreateTemplate = new CreateDatasetTemplate(collectionRepositoryStub) - - await expect(testCreateTemplate.execute(testTemplateDTO)).rejects.toThrow(WriteError) - }) -}) diff --git a/test/unit/template/createDatasetTemplate.test.ts b/test/unit/template/createDatasetTemplate.test.ts new file mode 100644 index 00000000..c541cb96 --- /dev/null +++ b/test/unit/template/createDatasetTemplate.test.ts @@ -0,0 +1,44 @@ +import { CreateDatasetTemplate } from '../../../src/template/domain/useCases/CreateDatasetTemplate' +import { ITemplatesRepository } from '../../../src/template/domain/repositories/ITemplatesRepository' +import { CreateDatasetTemplateDTO } from '../../../src/template/domain/dtos/CreateDatasetTemplateDTO' +import { WriteError } from '../../../src' + +describe('execute', () => { + const testTemplateDTO = { name: 't' } as CreateDatasetTemplateDTO + test('should return undefined when repository call is successful', async () => { + const templatesRepositoryStub: ITemplatesRepository = {} as ITemplatesRepository + templatesRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(undefined) + const sut = new CreateDatasetTemplate(templatesRepositoryStub) + + const actual = await sut.execute(testTemplateDTO) + + expect(templatesRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith( + ':root', + testTemplateDTO + ) + expect(actual).toBeUndefined() + }) + + test('should call repository with provided collection id/alias', async () => { + const templatesRepositoryStub: ITemplatesRepository = {} as ITemplatesRepository + templatesRepositoryStub.createDatasetTemplate = jest.fn().mockResolvedValue(undefined) + + const sut = new CreateDatasetTemplate(templatesRepositoryStub) + const actual = await sut.execute(testTemplateDTO, 'alias123') + + expect(templatesRepositoryStub.createDatasetTemplate).toHaveBeenCalledWith( + 'alias123', + testTemplateDTO + ) + + expect(actual).toBeUndefined() + }) + + test('should return error result on repository error', async () => { + const templatesRepositoryStub: ITemplatesRepository = {} as ITemplatesRepository + templatesRepositoryStub.createDatasetTemplate = jest.fn().mockRejectedValue(new WriteError()) + const testCreateTemplate = new CreateDatasetTemplate(templatesRepositoryStub) + + await expect(testCreateTemplate.execute(testTemplateDTO)).rejects.toThrow(WriteError) + }) +}) From 778f30cd09ac9572adc55789c395c4c5184c474d Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 7 Jan 2026 16:37:30 -0500 Subject: [PATCH 057/110] feat: get and delete a single template by id --- CHANGELOG.md | 8 +- docs/useCases.md | 36 ++++ .../repositories/ITemplatesRepository.ts | 2 + .../domain/useCases/DeleteTemplate.ts | 19 +++ src/template/domain/useCases/GetTemplate.ts | 21 +++ src/template/index.ts | 6 +- .../infra/repositories/TemplatesRepository.ts | 25 ++- .../datasetTemplateTransformers.ts | 103 ++++++----- .../template/TemplateRepository.test.ts | 160 ++++++++++++++++-- test/unit/template/deleteTemplate.test.ts | 26 +++ test/unit/template/getTemplate.test.ts | 28 +++ 11 files changed, 372 insertions(+), 62 deletions(-) create mode 100644 src/template/domain/useCases/DeleteTemplate.ts create mode 100644 src/template/domain/useCases/GetTemplate.ts create mode 100644 test/unit/template/deleteTemplate.test.ts create mode 100644 test/unit/template/getTemplate.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 01908608..61a9e8c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,14 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added - Datasets: Added `updateDatasetLicense` use case and repository method to support Dataverse endpoint `PUT /datasets/{id}/license`, for updating dataset license or custom terms + - New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). -- New Use Case: [Create a Dataset Template](./docs/useCases.md#create-a-dataset-template) under Collections. + +- New Use Case: [Create a Dataset Template](./docs/useCases.md#create-a-dataset-template) under Templates. + +- New Use Case: [Get a Template](./docs/useCases.md#get-a-template) under Templates. + +- New Use Case: [Delete a Template](./docs/useCases.md#delete-a-template) under Templates. - New Use Case: [Update Terms of Access](./docs/useCases.md#update-terms-of-access). diff --git a/docs/useCases.md b/docs/useCases.md index 6bf629db..3301d025 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -27,9 +27,11 @@ The different use cases currently available in the package are classified below, - [Delete a Collection Featured Item](#delete-a-collection-featured-item) - [Templates](#Templates) - [Templates read use cases](#templates-read-use-cases) + - [Get a Template](#get-a-template) - [Get Dataset Templates](#get-dataset-templates) - [Templates write use cases](#templates-write-use-cases) - [Create a Dataset Template](#create-a-dataset-template) + - [Delete a Template](#delete-a-template) - [Datasets](#Datasets) - [Datasets read use cases](#datasets-read-use-cases) - [Get a Dataset](#get-a-dataset) @@ -576,6 +578,24 @@ _See [use case](../src/collections/domain/useCases/DeleteCollectionFeaturedItem. ### Templates Read Use Cases +#### Get a Template + +Returns a [DatasetTemplate](../src/template/domain/models/DatasetTemplate.ts) by its template id. + +##### Example call: + +```typescript +import { getTemplate } from '@iqss/dataverse-client-javascript' + +const templateId = 12345 + +getTemplate.execute(templateId).then((template: DatasetTemplate) => { + /* ... */ +}) +``` + +_See [use case](../src/template/domain/useCases/GetTemplate.ts)_ definition. + #### Get Dataset Templates Returns a [DatasetTemplate](../src/template/domain/models/DatasetTemplate.ts) array containing the dataset templates of the requested collection, given the collection identifier or alias. @@ -631,6 +651,22 @@ await createDatasetTemplate.execute(template, collectionAlias) _See [use case](../src/template/domain/useCases/CreateDatasetTemplate.ts) implementation_. +#### Delete a Template + +Deletes a dataset template by its template id. + +##### Example call: + +```typescript +import { deleteTemplate } from '@iqss/dataverse-client-javascript' + +const templateId = 12345 + +await deleteTemplate.execute(templateId) +``` + +_See [use case](../src/template/domain/useCases/DeleteTemplate.ts)_ definition. + ## Datasets ### Datasets Read Use Cases diff --git a/src/template/domain/repositories/ITemplatesRepository.ts b/src/template/domain/repositories/ITemplatesRepository.ts index b05e9893..7cac1aa4 100644 --- a/src/template/domain/repositories/ITemplatesRepository.ts +++ b/src/template/domain/repositories/ITemplatesRepository.ts @@ -6,5 +6,7 @@ export interface ITemplatesRepository { collectionIdOrAlias: number | string, template: CreateDatasetTemplateDTO ): Promise + getTemplate(templateId: number): Promise getDatasetTemplates(collectionIdOrAlias: number | string): Promise + deleteTemplate(templateId: number): Promise } diff --git a/src/template/domain/useCases/DeleteTemplate.ts b/src/template/domain/useCases/DeleteTemplate.ts new file mode 100644 index 00000000..ef0aec84 --- /dev/null +++ b/src/template/domain/useCases/DeleteTemplate.ts @@ -0,0 +1,19 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { ITemplatesRepository } from '../repositories/ITemplatesRepository' + +export class DeleteTemplate implements UseCase { + private templatesRepository: ITemplatesRepository + + constructor(templatesRepository: ITemplatesRepository) { + this.templatesRepository = templatesRepository + } + + /** + * Deletes a dataset template by its template id. + * + * @param {number} templateId - Dataset template id. + */ + async execute(templateId: number): Promise { + return await this.templatesRepository.deleteTemplate(templateId) + } +} diff --git a/src/template/domain/useCases/GetTemplate.ts b/src/template/domain/useCases/GetTemplate.ts new file mode 100644 index 00000000..27db14ba --- /dev/null +++ b/src/template/domain/useCases/GetTemplate.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { DatasetTemplate } from '../models/DatasetTemplate' +import { ITemplatesRepository } from '../repositories/ITemplatesRepository' + +export class GetTemplate implements UseCase { + private templatesRepository: ITemplatesRepository + + constructor(templatesRepository: ITemplatesRepository) { + this.templatesRepository = templatesRepository + } + + /** + * Returns a dataset template by its template id. + * + * @param {number} templateId - Dataset template id. + * @returns {Promise} + */ + async execute(templateId: number): Promise { + return await this.templatesRepository.getTemplate(templateId) + } +} diff --git a/src/template/index.ts b/src/template/index.ts index 1bebaeef..bb1f765e 100644 --- a/src/template/index.ts +++ b/src/template/index.ts @@ -1,13 +1,17 @@ import { TemplatesRepository } from './infra/repositories/TemplatesRepository' import { CreateDatasetTemplate } from './domain/useCases/CreateDatasetTemplate' +import { DeleteTemplate } from './domain/useCases/DeleteTemplate' import { GetDatasetTemplates } from './domain/useCases/GetDatasetTemplates' +import { GetTemplate } from './domain/useCases/GetTemplate' const templatesRepository = new TemplatesRepository() const createDatasetTemplate = new CreateDatasetTemplate(templatesRepository) +const deleteTemplate = new DeleteTemplate(templatesRepository) const getDatasetTemplates = new GetDatasetTemplates(templatesRepository) +const getTemplate = new GetTemplate(templatesRepository) -export { createDatasetTemplate, getDatasetTemplates } +export { createDatasetTemplate, deleteTemplate, getDatasetTemplates, getTemplate } export { CreateDatasetTemplateDTO, TemplateFieldDTO, diff --git a/src/template/infra/repositories/TemplatesRepository.ts b/src/template/infra/repositories/TemplatesRepository.ts index dc613e4c..2a8412fd 100644 --- a/src/template/infra/repositories/TemplatesRepository.ts +++ b/src/template/infra/repositories/TemplatesRepository.ts @@ -4,7 +4,10 @@ import { CreateDatasetTemplateDTO } from '../../domain/dtos/CreateDatasetTemplat import { DatasetTemplate } from '../../domain/models/DatasetTemplate' import { ITemplatesRepository } from '../../domain/repositories/ITemplatesRepository' import { DatasetTemplatePayload } from './transformers/DatasetTemplatePayload' -import { transformDatasetTemplatePayloadToDatasetTemplate } from './transformers/datasetTemplateTransformers' +import { + transformTemplatePayloadToTemplate, + transformTemplatePayloadsToTemplates +} from './transformers/datasetTemplateTransformers' export class TemplatesRepository extends ApiRepository implements ITemplatesRepository { private readonly collectionsResourceName: string = 'dataverses' @@ -23,15 +26,33 @@ export class TemplatesRepository extends ApiRepository implements ITemplatesRepo }) } + public async getTemplate(templateId: number): Promise { + return this.doGet(`/dataverses/${templateId}/template`, true) + .then((response: AxiosResponse<{ data: DatasetTemplatePayload }>) => + transformTemplatePayloadToTemplate(response.data.data) + ) + .catch((error) => { + throw error + }) + } + public async getDatasetTemplates( collectionIdOrAlias: number | string ): Promise { return this.doGet(`/${this.collectionsResourceName}/${collectionIdOrAlias}/templates`, true) .then((response: AxiosResponse<{ data: DatasetTemplatePayload[] }>) => - transformDatasetTemplatePayloadToDatasetTemplate(response.data.data) + transformTemplatePayloadsToTemplates(response.data.data) ) .catch((error) => { throw error }) } + + public async deleteTemplate(templateId: number): Promise { + return this.doDelete(`/dataverses/${templateId}/template`) + .then(() => undefined) + .catch((error) => { + throw error + }) + } } diff --git a/src/template/infra/repositories/transformers/datasetTemplateTransformers.ts b/src/template/infra/repositories/transformers/datasetTemplateTransformers.ts index d48496f8..59fe8179 100644 --- a/src/template/infra/repositories/transformers/datasetTemplateTransformers.ts +++ b/src/template/infra/repositories/transformers/datasetTemplateTransformers.ts @@ -3,55 +3,66 @@ import { DatasetTemplate } from '../../../domain/models/DatasetTemplate' import { DatasetTemplatePayload } from './DatasetTemplatePayload' import { transformPayloadToDatasetMetadataBlocks } from '../../../../datasets/infra/repositories/transformers/datasetTransformers' -export const transformDatasetTemplatePayloadToDatasetTemplate = ( - collectionDatasetTemplatePayload: DatasetTemplatePayload[] -): DatasetTemplate[] => { - return collectionDatasetTemplatePayload.map((payload) => { - const datasetTemplate: DatasetTemplate = { - id: payload.id, - name: payload.name, - collectionAlias: payload.dataverseAlias, - isDefault: payload.isDefault, - usageCount: payload.usageCount, - createTime: payload.createTime, - createDate: payload.createDate, - datasetMetadataBlocks: transformPayloadToDatasetMetadataBlocks(payload.datasetFields, false), - 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 - } +export const transformTemplatePayloadToTemplate = ( + collectionDatasetTemplatePayload: DatasetTemplatePayload +): DatasetTemplate => { + const datasetTemplate: DatasetTemplate = { + id: collectionDatasetTemplatePayload.id, + name: collectionDatasetTemplatePayload.name, + collectionAlias: collectionDatasetTemplatePayload.dataverseAlias, + isDefault: collectionDatasetTemplatePayload.isDefault, + usageCount: collectionDatasetTemplatePayload.usageCount, + createTime: collectionDatasetTemplatePayload.createTime, + createDate: collectionDatasetTemplatePayload.createDate, + datasetMetadataBlocks: transformPayloadToDatasetMetadataBlocks( + collectionDatasetTemplatePayload.datasetFields, + false + ), + instructions: collectionDatasetTemplatePayload.instructions.map((instruction) => ({ + instructionField: instruction.instructionField, + instructionText: instruction.instructionText + })), + termsOfUse: { + termsOfAccess: { + fileAccessRequest: collectionDatasetTemplatePayload.termsOfUseAndAccess.fileAccessRequest, + termsOfAccessForRestrictedFiles: + collectionDatasetTemplatePayload.termsOfUseAndAccess.termsOfAccess, + dataAccessPlace: collectionDatasetTemplatePayload.termsOfUseAndAccess.dataAccessPlace, + originalArchive: collectionDatasetTemplatePayload.termsOfUseAndAccess.originalArchive, + availabilityStatus: collectionDatasetTemplatePayload.termsOfUseAndAccess.availabilityStatus, + contactForAccess: collectionDatasetTemplatePayload.termsOfUseAndAccess.contactForAccess, + sizeOfCollection: collectionDatasetTemplatePayload.termsOfUseAndAccess.sizeOfCollection, + studyCompletion: collectionDatasetTemplatePayload.termsOfUseAndAccess.studyCompletion } } + } - if (payload.termsOfUseAndAccess.license) { - datasetTemplate.license = transformPayloadLicenseToLicense( - payload.termsOfUseAndAccess.license - ) - } else { - datasetTemplate.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 - } + if (collectionDatasetTemplatePayload.termsOfUseAndAccess.license) { + datasetTemplate.license = transformPayloadLicenseToLicense( + collectionDatasetTemplatePayload.termsOfUseAndAccess.license + ) + } else { + datasetTemplate.termsOfUse.customTerms = { + termsOfUse: collectionDatasetTemplatePayload.termsOfUseAndAccess.termsOfUse as string, + confidentialityDeclaration: collectionDatasetTemplatePayload.termsOfUseAndAccess + .confidentialityDeclaration as string, + specialPermissions: collectionDatasetTemplatePayload.termsOfUseAndAccess + .specialPermissions as string, + restrictions: collectionDatasetTemplatePayload.termsOfUseAndAccess.restrictions as string, + citationRequirements: collectionDatasetTemplatePayload.termsOfUseAndAccess + .citationRequirements as string, + depositorRequirements: collectionDatasetTemplatePayload.termsOfUseAndAccess + .depositorRequirements as string, + conditions: collectionDatasetTemplatePayload.termsOfUseAndAccess.conditions as string, + disclaimer: collectionDatasetTemplatePayload.termsOfUseAndAccess.disclaimer as string } + } - return datasetTemplate - }) + return datasetTemplate +} + +export const transformTemplatePayloadsToTemplates = ( + datasetTemplatePayloads: DatasetTemplatePayload[] +): DatasetTemplate[] => { + return datasetTemplatePayloads.map((payload) => transformTemplatePayloadToTemplate(payload)) } diff --git a/test/integration/template/TemplateRepository.test.ts b/test/integration/template/TemplateRepository.test.ts index 83432eb8..196193f0 100644 --- a/test/integration/template/TemplateRepository.test.ts +++ b/test/integration/template/TemplateRepository.test.ts @@ -8,10 +8,7 @@ import { createCollectionViaApi, deleteCollectionViaApi } from '../../testHelpers/collections/collectionHelper' -import { - createDatasetTemplateViaApi, - deleteDatasetTemplateViaApi -} from '../../testHelpers/datasets/datasetTemplatesHelper' +import { deleteDatasetTemplateViaApi } from '../../testHelpers/datasets/datasetTemplatesHelper' describe('TemplatesRepository', () => { const sut: TemplatesRepository = new TemplatesRepository() @@ -33,7 +30,7 @@ describe('TemplatesRepository', () => { describe('createDatasetTemplate', () => { const templateDto: CreateDatasetTemplateDTO = { name: 'CollectionsRepository template', - isDefault: true, + isDefault: false, fields: [ { typeName: 'author', @@ -73,7 +70,7 @@ describe('TemplatesRepository', () => { templateDto.instructions?.length ?? 0 ) - deleteDatasetTemplateViaApi(templates[templates.length - 1].id) + await deleteDatasetTemplateViaApi(templates[templates.length - 1].id) }) test('should return error when creating a template with invalidCollectionAlias', async () => { @@ -87,27 +84,166 @@ describe('TemplatesRepository', () => { }) describe('getDatasetTemplates', () => { - test('should return the right number of dataset templates', async () => { + test('should return empty dataset templates', async () => { const actual = await sut.getDatasetTemplates(testCollectionAlias) - expect(actual.length).toBe(1) + expect(actual.length).toBe(0) }) test('should return dataset templates for a collection', async () => { - const templateCreated = await createDatasetTemplateViaApi(testCollectionAlias) + await createDatasetTemplate.execute( + { + name: 'Template for GetDatasetTemplates', + isDefault: false, + fields: [ + { + typeName: 'author', + typeClass: MetadataFieldTypeClass.Compound, + multiple: true, + value: [ + { + authorName: { + typeName: 'authorName', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'Belicheck, Bill' + }, + authorAffiliation: { + typeName: 'authorIdentifierScheme', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'ORCID' + } + } + ] + } + ], + instructions: [ + { + instructionField: 'author', + instructionText: 'The author data' + } + ] + }, + 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].name).toBe('Template for GetDatasetTemplates') + expect(actual[0].isDefault).toBe(false) 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) + expect(actual[0].instructions.length).toBe(1) await deleteDatasetTemplateViaApi(actual[0].id) }) }) + + describe('getTemplate', () => { + test('should return a dataset template by id', async () => { + await createDatasetTemplate.execute( + { + name: 'Template for GetTemplate', + isDefault: false, + fields: [ + { + typeName: 'author', + typeClass: MetadataFieldTypeClass.Compound, + multiple: true, + value: [ + { + authorName: { + typeName: 'authorName', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'Belicheck, Bill' + }, + authorAffiliation: { + typeName: 'authorIdentifierScheme', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'ORCID' + } + } + ] + } + ], + instructions: [ + { + instructionField: 'author', + instructionText: 'The author data' + } + ] + }, + testCollectionAlias + ) + const templates = await getDatasetTemplates.execute(testCollectionAlias) + const templateId = templates[templates.length - 1].id + const templateExpectedIsDefault = templates[templates.length - 1].isDefault + + const actual = await sut.getTemplate(templateId) + + expect(actual.name).toBe('Template for GetTemplate') + expect(actual.isDefault).toBe(templateExpectedIsDefault) + expect(actual.datasetMetadataBlocks.length).toBe(1) + expect(actual.datasetMetadataBlocks[0].name).toBe('citation') + expect(actual.datasetMetadataBlocks[0].fields.author.length).toBe(1) + expect(actual.instructions.length).toBe(1) + + await deleteDatasetTemplateViaApi(templateId) + }) + + test('should return error when template does not exist', async () => { + await expect(sut.getTemplate(999999)).rejects.toThrow() + }) + }) + + describe('deleteTemplate', () => { + test('should delete a dataset template by id', async () => { + await createDatasetTemplate.execute( + { + name: 'Template for DeleteTemplate', + isDefault: false, + fields: [ + { + typeName: 'author', + typeClass: MetadataFieldTypeClass.Compound, + multiple: true, + value: [ + { + authorName: { + typeName: 'authorName', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'Belicheck, Bill' + }, + authorAffiliation: { + typeName: 'authorIdentifierScheme', + typeClass: MetadataFieldTypeClass.Primitive, + value: 'ORCID' + } + } + ] + } + ], + instructions: [ + { + instructionField: 'author', + instructionText: 'The author data' + } + ] + }, + testCollectionAlias + ) + const templates = await getDatasetTemplates.execute(testCollectionAlias) + const templateId = templates[templates.length - 1].id + + await sut.deleteTemplate(templateId) + + await expect(sut.getTemplate(templateId)).rejects.toThrow() + }) + + test('should return error when deleting a template that does not exist', async () => { + await expect(sut.deleteTemplate(999999)).rejects.toThrow() + }) + }) }) diff --git a/test/unit/template/deleteTemplate.test.ts b/test/unit/template/deleteTemplate.test.ts new file mode 100644 index 00000000..f1f71ac4 --- /dev/null +++ b/test/unit/template/deleteTemplate.test.ts @@ -0,0 +1,26 @@ +import { DeleteTemplate } from '../../../src/template/domain/useCases/DeleteTemplate' +import { ITemplatesRepository } from '../../../src/template/domain/repositories/ITemplatesRepository' +import { WriteError } from '../../../src' + +describe('execute', () => { + const templateId = 123 + + test('should delete a dataset template', async () => { + const templatesRepositoryStub: ITemplatesRepository = {} as ITemplatesRepository + templatesRepositoryStub.deleteTemplate = jest.fn().mockResolvedValue(undefined) + const sut = new DeleteTemplate(templatesRepositoryStub) + + const actual = await sut.execute(templateId) + + expect(templatesRepositoryStub.deleteTemplate).toHaveBeenCalledWith(templateId) + expect(actual).toBeUndefined() + }) + + test('should return error result on repository error', async () => { + const templatesRepositoryStub: ITemplatesRepository = {} as ITemplatesRepository + templatesRepositoryStub.deleteTemplate = jest.fn().mockRejectedValue(new WriteError()) + const sut = new DeleteTemplate(templatesRepositoryStub) + + await expect(sut.execute(templateId)).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/template/getTemplate.test.ts b/test/unit/template/getTemplate.test.ts new file mode 100644 index 00000000..0e480e21 --- /dev/null +++ b/test/unit/template/getTemplate.test.ts @@ -0,0 +1,28 @@ +import { GetTemplate } from '../../../src/template/domain/useCases/GetTemplate' +import { ITemplatesRepository } from '../../../src/template/domain/repositories/ITemplatesRepository' +import { DatasetTemplate } from '../../../src/template/domain/models/DatasetTemplate' +import { ReadError } from '../../../src' + +describe('execute', () => { + const templateId = 123 + const template = { id: templateId } as DatasetTemplate + + test('should return a dataset template', async () => { + const templatesRepositoryStub: ITemplatesRepository = {} as ITemplatesRepository + templatesRepositoryStub.getTemplate = jest.fn().mockResolvedValue(template) + const sut = new GetTemplate(templatesRepositoryStub) + + const actual = await sut.execute(templateId) + + expect(templatesRepositoryStub.getTemplate).toHaveBeenCalledWith(templateId) + expect(actual).toBe(template) + }) + + test('should return error result on repository error', async () => { + const templatesRepositoryStub: ITemplatesRepository = {} as ITemplatesRepository + templatesRepositoryStub.getTemplate = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetTemplate(templatesRepositoryStub) + + await expect(sut.execute(templateId)).rejects.toThrow(ReadError) + }) +}) From 0d51b50c6114c8ea3727882f3da150d11d813ed6 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 7 Jan 2026 19:01:26 -0500 Subject: [PATCH 058/110] chore: sync the naming to template fron DatasetTemplate --- CHANGELOG.md | 2 +- docs/useCases.md | 28 +++++++++---------- src/index.ts | 2 +- .../domain/dtos/CreateDatasetTemplateDTO.ts | 0 .../domain/models/Template.ts} | 6 ++-- .../repositories/ITemplatesRepository.ts | 6 ++-- .../domain/useCases/CreateTemplate.ts} | 4 +-- .../domain/useCases/DeleteTemplate.ts | 4 +-- .../domain/useCases/GetDatasetTemplates.ts | 12 ++++---- .../domain/useCases/GetTemplate.ts | 12 ++++---- src/{template => templates}/index.ts | 8 +++--- .../infra/repositories/TemplatesRepository.ts | 16 +++++------ .../transformers/TemplatePayload.ts} | 2 +- .../transformers/templateTransformers.ts} | 20 ++++++------- .../createDatasetTemplate.test.ts | 6 ++-- .../TemplateRepository.test.ts | 26 ++++++++--------- .../datasets/datasetTemplatesHelper.ts | 4 +-- .../createDatasetTemplate.test.ts | 12 ++++---- .../deleteTemplate.test.ts | 6 ++-- .../getTemplate.test.ts | 10 +++---- 20 files changed, 91 insertions(+), 95 deletions(-) rename src/{template => templates}/domain/dtos/CreateDatasetTemplateDTO.ts (100%) rename src/{template/domain/models/DatasetTemplate.ts => templates/domain/models/Template.ts} (82%) rename src/{template => templates}/domain/repositories/ITemplatesRepository.ts (71%) rename src/{template/domain/useCases/CreateDatasetTemplate.ts => templates/domain/useCases/CreateTemplate.ts} (89%) rename src/{template => templates}/domain/useCases/DeleteTemplate.ts (82%) rename src/{template => templates}/domain/useCases/GetDatasetTemplates.ts (64%) rename src/{template => templates}/domain/useCases/GetTemplate.ts (54%) rename src/{template => templates}/index.ts (70%) rename src/{template => templates}/infra/repositories/TemplatesRepository.ts (73%) rename src/{template/infra/repositories/transformers/DatasetTemplatePayload.ts => templates/infra/repositories/transformers/TemplatePayload.ts} (97%) rename src/{template/infra/repositories/transformers/datasetTemplateTransformers.ts => templates/infra/repositories/transformers/templateTransformers.ts} (86%) rename test/functional/{template => templates}/createDatasetTemplate.test.ts (89%) rename test/integration/{template => templates}/TemplateRepository.test.ts (89%) rename test/unit/{template => templates}/createDatasetTemplate.test.ts (73%) rename test/unit/{template => templates}/deleteTemplate.test.ts (78%) rename test/unit/{template => templates}/getTemplate.test.ts (69%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61a9e8c0..324a28e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - New Use Case: [Get Collections For Linking Use Case](./docs/useCases.md#get-collections-for-linking). -- New Use Case: [Create a Dataset Template](./docs/useCases.md#create-a-dataset-template) under Templates. +- New Use Case: [Create a Template](./docs/useCases.md#create-a-template) under Templates. - New Use Case: [Get a Template](./docs/useCases.md#get-a-template) under Templates. diff --git a/docs/useCases.md b/docs/useCases.md index 3301d025..48291b7a 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -30,7 +30,7 @@ The different use cases currently available in the package are classified below, - [Get a Template](#get-a-template) - [Get Dataset Templates](#get-dataset-templates) - [Templates write use cases](#templates-write-use-cases) - - [Create a Dataset Template](#create-a-dataset-template) + - [Create a Template](#create-a-template) - [Delete a Template](#delete-a-template) - [Datasets](#Datasets) - [Datasets read use cases](#datasets-read-use-cases) @@ -580,7 +580,7 @@ _See [use case](../src/collections/domain/useCases/DeleteCollectionFeaturedItem. #### Get a Template -Returns a [DatasetTemplate](../src/template/domain/models/DatasetTemplate.ts) by its template id. +Returns a [Template](../src/templates/domain/models/Template.ts) by its template id. ##### Example call: @@ -589,16 +589,16 @@ import { getTemplate } from '@iqss/dataverse-client-javascript' const templateId = 12345 -getTemplate.execute(templateId).then((template: DatasetTemplate) => { +getTemplate.execute(templateId).then((template: Template) => { /* ... */ }) ``` -_See [use case](../src/template/domain/useCases/GetTemplate.ts)_ definition. +_See [use case](../src/templates/domain/useCases/GetTemplate.ts)_ definition. #### Get Dataset Templates -Returns a [DatasetTemplate](../src/template/domain/models/DatasetTemplate.ts) array containing the dataset templates of the requested collection, given the collection identifier or alias. +Returns a [Template](../src/templates/domain/models/Template.ts) array containing the templates of the requested collection, given the collection identifier or alias. ##### Example call: @@ -607,23 +607,23 @@ import { getDatasetTemplates } from '@iqss/dataverse-client-javascript' const collectionIdOrAlias = 12345 -getDatasetTemplates.execute(collectionIdOrAlias).then((datasetTemplates: DatasetTemplate[]) => { +getDatasetTemplates.execute(collectionIdOrAlias).then((datasetTemplates: Template[]) => { /* ... */ }) ``` -_See [use case](../src/template/domain/useCases/GetDatasetTemplates.ts)_ definition. +_See [use case](../src/templates/domain/useCases/GetDatasetTemplates.ts)_ definition. ### Templates Write Use Cases -#### Create a Dataset Template +#### Create a Template -Creates a dataset template for a given Dataverse collection id or alias. +Creates a template for a given Dataverse collection id or alias. ##### Example call: ```typescript -import { createDatasetTemplate } from '@iqss/dataverse-client-javascript' +import { createTemplate } from '@iqss/dataverse-client-javascript' import { CreateDatasetTemplateDTO } from '@iqss/dataverse-client-javascript' const collectionAlias = ':root' @@ -646,14 +646,14 @@ const template: CreateDatasetTemplateDTO = { instructions: [{ instructionField: 'author', instructionText: 'The author data' }] } -await createDatasetTemplate.execute(template, collectionAlias) +await createTemplate.execute(template, collectionAlias) ``` -_See [use case](../src/template/domain/useCases/CreateDatasetTemplate.ts) implementation_. +_See [use case](../src/templates/domain/useCases/CreateTemplate.ts) implementation_. #### Delete a Template -Deletes a dataset template by its template id. +Deletes a template by its template id. ##### Example call: @@ -665,7 +665,7 @@ const templateId = 12345 await deleteTemplate.execute(templateId) ``` -_See [use case](../src/template/domain/useCases/DeleteTemplate.ts)_ definition. +_See [use case](../src/templates/domain/useCases/DeleteTemplate.ts)_ definition. ## Datasets diff --git a/src/index.ts b/src/index.ts index e7f4475a..578f1924 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,4 +12,4 @@ export * from './notifications' export * from './search' export * from './licenses' export * from './externalTools' -export * from './template' +export * from './templates' diff --git a/src/template/domain/dtos/CreateDatasetTemplateDTO.ts b/src/templates/domain/dtos/CreateDatasetTemplateDTO.ts similarity index 100% rename from src/template/domain/dtos/CreateDatasetTemplateDTO.ts rename to src/templates/domain/dtos/CreateDatasetTemplateDTO.ts diff --git a/src/template/domain/models/DatasetTemplate.ts b/src/templates/domain/models/Template.ts similarity index 82% rename from src/template/domain/models/DatasetTemplate.ts rename to src/templates/domain/models/Template.ts index afe5d8d3..b5b93b62 100644 --- a/src/template/domain/models/DatasetTemplate.ts +++ b/src/templates/domain/models/Template.ts @@ -1,7 +1,7 @@ import { DatasetMetadataBlock, TermsOfUse } from '../../../datasets/domain/models/Dataset' import { License } from '../../../licenses/domain/models/License' -export interface DatasetTemplate { +export interface Template { id: number name: string collectionAlias: string @@ -11,13 +11,13 @@ export interface DatasetTemplate { createDate: string // 👇 From Edit Template Metadata datasetMetadataBlocks: DatasetMetadataBlock[] - instructions: DatasetTemplateInstruction[] + instructions: TemplateInstruction[] // 👇 From Edit Template Terms termsOfUse: TermsOfUse license?: License // This license property is going to be present if not custom terms are added in the UI } -export interface DatasetTemplateInstruction { +export interface TemplateInstruction { instructionField: string instructionText: string } diff --git a/src/template/domain/repositories/ITemplatesRepository.ts b/src/templates/domain/repositories/ITemplatesRepository.ts similarity index 71% rename from src/template/domain/repositories/ITemplatesRepository.ts rename to src/templates/domain/repositories/ITemplatesRepository.ts index 7cac1aa4..a79e4cc9 100644 --- a/src/template/domain/repositories/ITemplatesRepository.ts +++ b/src/templates/domain/repositories/ITemplatesRepository.ts @@ -1,12 +1,12 @@ import { CreateDatasetTemplateDTO } from '../dtos/CreateDatasetTemplateDTO' -import { DatasetTemplate } from '../models/DatasetTemplate' +import { Template } from '../models/Template' export interface ITemplatesRepository { createDatasetTemplate( collectionIdOrAlias: number | string, template: CreateDatasetTemplateDTO ): Promise - getTemplate(templateId: number): Promise - getDatasetTemplates(collectionIdOrAlias: number | string): Promise + getTemplate(templateId: number): Promise