From 967a016393fbc535f6f066a6561ed602e2ab1329 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 6 Oct 2025 14:52:14 -0400 Subject: [PATCH 01/24] 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 02/24] 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 03/24] 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 7e86b7608c576c04b03c23424718af3222c14127 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 21 Oct 2025 18:06:59 -0400 Subject: [PATCH 04/24] 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 8d571f35f5458812e7607de743d1f43cec716ff6 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Wed, 22 Oct 2025 20:43:45 -0400 Subject: [PATCH 05/24] 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 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 17/24] 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 18/24] 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 19/24] 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 20/24] 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