From cfe34ea764047b839a7e3f8b65e40a85076a4587 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 3 Oct 2025 18:08:08 -0400 Subject: [PATCH 1/8] 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 b0929de83347676f5884bf6d50ad1bc3077ef809 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 14 Oct 2025 10:44:48 -0400 Subject: [PATCH 2/8] 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 3/8] 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 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 4/8] 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 5/8] 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 6/8] 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 b6d38254c3559b2916d14417f0a142fa6535430f Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 31 Oct 2025 15:52:34 -0400 Subject: [PATCH 7/8] 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 8/8] 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)