From 0c49ec545a6aa3773387e213b4362de5a84b8bf7 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 11 Jul 2025 14:51:29 -0400 Subject: [PATCH 1/8] feat: update notification usecases --- docs/useCases.md | 47 +++++++++++ src/index.ts | 1 + .../domain/models/Notification.ts | 7 ++ .../repositories/INotificationsRepository.ts | 6 ++ .../useCases/DeleteNotificationByUser.ts | 16 ++++ .../useCases/GetAllNotificationsByUser.ts | 16 ++++ src/notifications/index.ts | 12 +++ .../repositories/NotificationsRepository.ts | 25 ++++++ .../DeleteNotificationByUser.test.ts | 30 +++++++ .../GetAllNotificationsByUser.test.ts | 33 ++++++++ .../NotificationsRepository.test.ts | 78 +++++++++++++++++++ .../DeleteNotificationByUser.test.ts | 46 +++++++++++ .../GetAllNotificationsByUser.test.ts | 44 +++++++++++ 13 files changed, 361 insertions(+) create mode 100644 src/notifications/domain/models/Notification.ts create mode 100644 src/notifications/domain/repositories/INotificationsRepository.ts create mode 100644 src/notifications/domain/useCases/DeleteNotificationByUser.ts create mode 100644 src/notifications/domain/useCases/GetAllNotificationsByUser.ts create mode 100644 src/notifications/index.ts create mode 100644 src/notifications/infra/repositories/NotificationsRepository.ts create mode 100644 test/functional/notifications/DeleteNotificationByUser.test.ts create mode 100644 test/functional/notifications/GetAllNotificationsByUser.test.ts create mode 100644 test/integration/notifications/NotificationsRepository.test.ts create mode 100644 test/unit/notifications/DeleteNotificationByUser.test.ts create mode 100644 test/unit/notifications/GetAllNotificationsByUser.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 2ccee9fc..fa4f507e 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -85,6 +85,9 @@ The different use cases currently available in the package are classified below, - [Get Application Terms of Use](#get-application-terms-of-use) - [Contact](#Contact) - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) +- [Notifications](#Notifications) + - [Get All Notifications by User](#get-all-notifications-by-user) + - [Delete Notification by User](#delete-notification-by-user) ## Collections @@ -1991,3 +1994,47 @@ In ContactDTO, it takes the following information: - **subject**: the email subject line. - **body**: the email body to send. - **fromEmail**: the email to list in the reply-to field. + +## Notifications + +#### Get All Notifications by User + +Returns a [Notification](../src/notifications/domain/models/Notification.ts) array containing all notifications for the current authenticated user. + +##### Example call: + +```typescript +import { getAllNotificationsByUser } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getAllNotificationsByUser.execute().then((notifications: Notification[]) => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/GetAllNotificationsByUser.ts) implementation_. + +#### Delete Notification by User + +Deletes a specific notification for the current authenticated user by its ID. + +##### Example call: + +```typescript +import { deleteNotificationByUser } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const notificationId = 123 + +deleteNotificationByUser.execute(notificationId: number).then(() => { + /* ... */ +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/DeleteNotificationByUser.ts) implementation_. diff --git a/src/index.ts b/src/index.ts index 89a79af6..7669a4c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,3 +8,4 @@ export * from './collections' export * from './metadataBlocks' export * from './files' export * from './contactInfo' +export * from './notifications' diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts new file mode 100644 index 00000000..f99a2170 --- /dev/null +++ b/src/notifications/domain/models/Notification.ts @@ -0,0 +1,7 @@ +export interface Notification { + id: number + type: string + subjectText: string + messageText: string + sentTimestamp: string +} diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts new file mode 100644 index 00000000..c528122b --- /dev/null +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -0,0 +1,6 @@ +import { Notification } from '../models/Notification' + +export interface INotificationsRepository { + getAllNotificationsByUser(): Promise + deleteNotificationByUser(notificationId: number): Promise +} diff --git a/src/notifications/domain/useCases/DeleteNotificationByUser.ts b/src/notifications/domain/useCases/DeleteNotificationByUser.ts new file mode 100644 index 00000000..2c98d43b --- /dev/null +++ b/src/notifications/domain/useCases/DeleteNotificationByUser.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +/** + * Use case for deleting a specific notification for the current user. + * + * @param notificationId - The ID of the notification to delete. + * @returns {Promise} - A promise that resolves when the notification is deleted. + */ +export class DeleteNotificationByUser implements UseCase { + constructor(private readonly notificationsRepository: INotificationsRepository) {} + + async execute(notificationId: number): Promise { + return this.notificationsRepository.deleteNotificationByUser(notificationId) + } +} diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts new file mode 100644 index 00000000..1eb70ac8 --- /dev/null +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { Notification } from '../models/Notification' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +export class GetAllNotificationsByUser implements UseCase { + constructor(private readonly notificationsRepository: INotificationsRepository) {} + + /** + * Use case for retrieving all notifications for the current user. + * + * @returns {Promise} - A promise that resolves to an array of Notification instances. + */ + async execute(): Promise { + return (await this.notificationsRepository.getAllNotificationsByUser()) as Notification[] + } +} diff --git a/src/notifications/index.ts b/src/notifications/index.ts new file mode 100644 index 00000000..a246c783 --- /dev/null +++ b/src/notifications/index.ts @@ -0,0 +1,12 @@ +import { NotificationsRepository } from './infra/repositories/NotificationsRepository' +import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' +import { DeleteNotificationByUser } from './domain/useCases/DeleteNotificationByUser' + +const notificationsRepository = new NotificationsRepository() + +const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) +const deleteNotificationByUser = new DeleteNotificationByUser(notificationsRepository) + +export { getAllNotificationsByUser, deleteNotificationByUser } + +export { Notification } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts new file mode 100644 index 00000000..341f8526 --- /dev/null +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -0,0 +1,25 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { INotificationsRepository } from '../../domain/repositories/INotificationsRepository' +import { Notification } from '../../domain/models/Notification' + +export class NotificationsRepository extends ApiRepository implements INotificationsRepository { + private readonly notificationsResourceName: string = 'notifications' + + public async getAllNotificationsByUser(): Promise { + return this.doGet(this.buildApiEndpoint(this.notificationsResourceName, 'all'), true) + .then((response) => response.data.data.notifications as Notification[]) + .catch((error) => { + throw error + }) + } + + public async deleteNotificationByUser(notificationId: number): Promise { + return this.doDelete( + this.buildApiEndpoint(this.notificationsResourceName, notificationId.toString()) + ) + .then(() => {}) + .catch((error) => { + throw error + }) + } +} diff --git a/test/functional/notifications/DeleteNotificationByUser.test.ts b/test/functional/notifications/DeleteNotificationByUser.test.ts new file mode 100644 index 00000000..eceb81d4 --- /dev/null +++ b/test/functional/notifications/DeleteNotificationByUser.test.ts @@ -0,0 +1,30 @@ +import { + ApiConfig, + deleteNotificationByUser, + getAllNotificationsByUser, + WriteError +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should successfully delete a notification for authenticated user', async () => { + const notificationId = 1 + await deleteNotificationByUser.execute(notificationId) + + const notifications = await getAllNotificationsByUser.execute() + expect(notifications.length).toBe(0) + }) + + test('should throw an error when the notification id does not exist', async () => { + await expect(deleteNotificationByUser.execute(123)).rejects.toThrow(WriteError) + }) +}) diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts new file mode 100644 index 00000000..2202ff20 --- /dev/null +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -0,0 +1,33 @@ +import { ApiConfig, getAllNotificationsByUser, Notification } from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should successfully return notifications for authenticated user', async () => { + const notifications: Notification[] = await getAllNotificationsByUser.execute() + + expect(notifications).not.toBeNull() + expect(Array.isArray(notifications)).toBe(true) + }) + + test('should have correct notification properties if notifications exist', async () => { + const notifications = await getAllNotificationsByUser.execute() + if (notifications.length === 0) { + return + } + const first = notifications[0] + expect(first).toHaveProperty('id') + expect(first).toHaveProperty('type') + expect(first).toHaveProperty('subjectText') + expect(first).toHaveProperty('messageText') + expect(first).toHaveProperty('sentTimestamp') + }) +}) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts new file mode 100644 index 00000000..873650e2 --- /dev/null +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -0,0 +1,78 @@ +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { NotificationsRepository } from '../../../src/notifications/infra/repositories/NotificationsRepository' +import { Notification } from '../../../src/notifications/domain/models/Notification' +import { createDataset } from '../../../src/datasets' +import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' +import { WriteError } from '../../../src' + +describe('NotificationsRepository', () => { + const sut: NotificationsRepository = new NotificationsRepository() + + beforeEach(() => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return notifications after creating and publishing a dataset', async () => { + // Create a dataset and publish it so that a notification of Dataset published is created + const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + + await publishDatasetViaApi(testDatasetIds.numericId) + await waitForNoLocks(testDatasetIds.numericId, 10) + + const notifications: Notification[] = await sut.getAllNotificationsByUser() + + expect(Array.isArray(notifications)).toBe(true) + expect(notifications.length).toBe(2) + + const publishedNotification = notifications.find((n) => n.type === 'PUBLISHEDDS') + + expect(publishedNotification).toBeDefined() + + expect(publishedNotification).toHaveProperty('id') + expect(publishedNotification).toHaveProperty('type') + expect(publishedNotification).toHaveProperty('subjectText') + expect(publishedNotification).toHaveProperty('messageText') + expect(publishedNotification).toHaveProperty('sentTimestamp') + + expect(publishedNotification?.subjectText).toContain( + 'Dataset created using the createDataset use case' + ) + expect(publishedNotification?.messageText).toContain( + 'Your dataset named Dataset created using the createDataset use case' + ) + }) + + test('should delete a notification by ID', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser() + + const notificationToDelete = notifications[0] + + await sut.deleteNotificationByUser(notificationToDelete.id) + + const notificationsAfterDelete: Notification[] = await sut.getAllNotificationsByUser() + const deletedNotification = notificationsAfterDelete.find( + (n) => n.id === notificationToDelete.id + ) + expect(deletedNotification).toBeUndefined() + }) + + test('should throw error when trying to delete notification with wrong ID', async () => { + const nonExistentMetadataBlockName = 99999 + + const expectedError = new WriteError( + `[404] Notification ${nonExistentMetadataBlockName} not found.` + ) + + await expect(sut.deleteNotificationByUser(nonExistentMetadataBlockName)).rejects.toThrow( + expectedError + ) + }) +}) diff --git a/test/unit/notifications/DeleteNotificationByUser.test.ts b/test/unit/notifications/DeleteNotificationByUser.test.ts new file mode 100644 index 00000000..e8f064c3 --- /dev/null +++ b/test/unit/notifications/DeleteNotificationByUser.test.ts @@ -0,0 +1,46 @@ +import { DeleteNotificationByUser } from '../../../src/notifications/domain/useCases/DeleteNotificationByUser' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { Notification } from '../../../src/notifications/domain/models/Notification' + +const mockNotifications: Notification[] = [ + { + id: 1, + type: 'PUBLISHEDDS', + subjectText: 'Test notification', + messageText: 'Test message', + sentTimestamp: '2025-01-01T00:00:00Z' + }, + { + id: 2, + type: 'ASSIGNROLE', + subjectText: 'Role assignment', + messageText: 'Role assigned', + sentTimestamp: '2025-01-01T00:00:00Z' + } +] + +describe('execute', () => { + test('should delete notification from repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) + notificationsRepositoryStub.deleteNotificationByUser = jest + .fn() + .mockResolvedValue(mockNotifications) + const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + + await sut.execute(123) + + expect(notificationsRepositoryStub.deleteNotificationByUser).toHaveBeenCalledWith(123) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) + notificationsRepositoryStub.deleteNotificationByUser = jest + .fn() + .mockRejectedValue(new Error('Repository error')) + const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + + await expect(sut.execute(123)).rejects.toThrow('Repository error') + }) +}) diff --git a/test/unit/notifications/GetAllNotificationsByUser.test.ts b/test/unit/notifications/GetAllNotificationsByUser.test.ts new file mode 100644 index 00000000..5779f303 --- /dev/null +++ b/test/unit/notifications/GetAllNotificationsByUser.test.ts @@ -0,0 +1,44 @@ +import { GetAllNotificationsByUser } from '../../../src/notifications/domain/useCases/GetAllNotificationsByUser' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { Notification } from '../../../src/notifications/domain/models/Notification' + +const mockNotifications: Notification[] = [ + { + id: 1, + type: 'PUBLISHEDDS', + subjectText: 'Test notification', + messageText: 'Test message', + sentTimestamp: '2025-01-01T00:00:00Z' + }, + { + id: 2, + type: 'ASSIGNROLE', + subjectText: 'Role assignment', + messageText: 'Role assigned', + sentTimestamp: '2025-01-01T00:00:00Z' + } +] + +describe('execute', () => { + test('should return notifications from repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest + .fn() + .mockResolvedValue(mockNotifications) + const sut = new GetAllNotificationsByUser(notificationsRepositoryStub) + + const result = await sut.execute() + + expect(result).toEqual(mockNotifications) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getAllNotificationsByUser = jest + .fn() + .mockRejectedValue(new Error('Repository error')) + const sut = new GetAllNotificationsByUser(notificationsRepositoryStub) + + await expect(sut.execute()).rejects.toThrow('Repository error') + }) +}) From 3c12fbe7887e7bc89d7d9dfa6df78a04bb41e40b Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 11 Jul 2025 15:31:55 -0400 Subject: [PATCH 2/8] fix: testcases --- .../notifications/DeleteNotificationByUser.test.ts | 8 +++++--- .../notifications/GetAllNotificationsByUser.test.ts | 13 ++++--------- .../notifications/NotificationsRepository.test.ts | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/test/functional/notifications/DeleteNotificationByUser.test.ts b/test/functional/notifications/DeleteNotificationByUser.test.ts index eceb81d4..93ff971b 100644 --- a/test/functional/notifications/DeleteNotificationByUser.test.ts +++ b/test/functional/notifications/DeleteNotificationByUser.test.ts @@ -17,11 +17,13 @@ describe('execute', () => { }) test('should successfully delete a notification for authenticated user', async () => { - const notificationId = 1 + const notifications = await getAllNotificationsByUser.execute() + const notificationId = notifications[notifications.length - 1].id + await deleteNotificationByUser.execute(notificationId) - const notifications = await getAllNotificationsByUser.execute() - expect(notifications.length).toBe(0) + const notificationsAfterDelete = await getAllNotificationsByUser.execute() + expect(notificationsAfterDelete.length).toBe(notifications.length - 1) }) test('should throw an error when the notification id does not exist', async () => { diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 2202ff20..59d950d9 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -20,14 +20,9 @@ describe('execute', () => { test('should have correct notification properties if notifications exist', async () => { const notifications = await getAllNotificationsByUser.execute() - if (notifications.length === 0) { - return - } - const first = notifications[0] - expect(first).toHaveProperty('id') - expect(first).toHaveProperty('type') - expect(first).toHaveProperty('subjectText') - expect(first).toHaveProperty('messageText') - expect(first).toHaveProperty('sentTimestamp') + + expect(notifications[0]).toHaveProperty('id') + expect(notifications[0]).toHaveProperty('type') + expect(notifications[0]).toHaveProperty('sentTimestamp') }) }) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 873650e2..b5b026ae 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -30,7 +30,7 @@ describe('NotificationsRepository', () => { const notifications: Notification[] = await sut.getAllNotificationsByUser() expect(Array.isArray(notifications)).toBe(true) - expect(notifications.length).toBe(2) + expect(notifications.length).toBeGreaterThan(0) const publishedNotification = notifications.find((n) => n.type === 'PUBLISHEDDS') From 252e08c09499db93abe0c4b247bf8a698bbe0487 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 17 Jul 2025 15:40:11 -0400 Subject: [PATCH 3/8] fix: naming of delete notification --- docs/useCases.md | 10 +++++----- .../repositories/INotificationsRepository.ts | 2 +- ...NotificationByUser.ts => DeleteNotification.ts} | 4 ++-- src/notifications/index.ts | 6 +++--- .../infra/repositories/NotificationsRepository.ts | 2 +- ...onByUser.test.ts => DeleteNotification.test.ts} | 11 +++-------- .../notifications/NotificationsRepository.test.ts | 10 ++++------ ...onByUser.test.ts => DeleteNotification.test.ts} | 14 ++++++-------- 8 files changed, 25 insertions(+), 34 deletions(-) rename src/notifications/domain/useCases/{DeleteNotificationByUser.ts => DeleteNotification.ts} (78%) rename test/functional/notifications/{DeleteNotificationByUser.test.ts => DeleteNotification.test.ts} (76%) rename test/unit/notifications/{DeleteNotificationByUser.test.ts => DeleteNotification.test.ts} (70%) diff --git a/docs/useCases.md b/docs/useCases.md index fa4f507e..9d7cf314 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -87,7 +87,7 @@ The different use cases currently available in the package are classified below, - [Send Feedback to Object Contacts](#send-feedback-to-object-contacts) - [Notifications](#Notifications) - [Get All Notifications by User](#get-all-notifications-by-user) - - [Delete Notification by User](#delete-notification-by-user) + - [Delete Notification](#delete-notification) ## Collections @@ -2017,24 +2017,24 @@ getAllNotificationsByUser.execute().then((notifications: Notification[]) => { _See [use case](../src/notifications/domain/useCases/GetAllNotificationsByUser.ts) implementation_. -#### Delete Notification by User +#### Delete Notification Deletes a specific notification for the current authenticated user by its ID. ##### Example call: ```typescript -import { deleteNotificationByUser } from '@iqss/dataverse-client-javascript' +import { deleteNotification } from '@iqss/dataverse-client-javascript' /* ... */ const notificationId = 123 -deleteNotificationByUser.execute(notificationId: number).then(() => { +deleteNotification.execute(notificationId: number).then(() => { /* ... */ }) /* ... */ ``` -_See [use case](../src/notifications/domain/useCases/DeleteNotificationByUser.ts) implementation_. +_See [use case](../src/notifications/domain/useCases/DeleteNotification.ts) implementation_. diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index c528122b..dd40d071 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -2,5 +2,5 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { getAllNotificationsByUser(): Promise - deleteNotificationByUser(notificationId: number): Promise + deleteNotification(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/DeleteNotificationByUser.ts b/src/notifications/domain/useCases/DeleteNotification.ts similarity index 78% rename from src/notifications/domain/useCases/DeleteNotificationByUser.ts rename to src/notifications/domain/useCases/DeleteNotification.ts index 2c98d43b..ed57fc0b 100644 --- a/src/notifications/domain/useCases/DeleteNotificationByUser.ts +++ b/src/notifications/domain/useCases/DeleteNotification.ts @@ -7,10 +7,10 @@ import { INotificationsRepository } from '../repositories/INotificationsReposito * @param notificationId - The ID of the notification to delete. * @returns {Promise} - A promise that resolves when the notification is deleted. */ -export class DeleteNotificationByUser implements UseCase { +export class DeleteNotification implements UseCase { constructor(private readonly notificationsRepository: INotificationsRepository) {} async execute(notificationId: number): Promise { - return this.notificationsRepository.deleteNotificationByUser(notificationId) + return this.notificationsRepository.deleteNotification(notificationId) } } diff --git a/src/notifications/index.ts b/src/notifications/index.ts index a246c783..72ae6e82 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -1,12 +1,12 @@ import { NotificationsRepository } from './infra/repositories/NotificationsRepository' import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' -import { DeleteNotificationByUser } from './domain/useCases/DeleteNotificationByUser' +import { DeleteNotification } from './domain/useCases/DeleteNotification' const notificationsRepository = new NotificationsRepository() const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) -const deleteNotificationByUser = new DeleteNotificationByUser(notificationsRepository) +const deleteNotification = new DeleteNotification(notificationsRepository) -export { getAllNotificationsByUser, deleteNotificationByUser } +export { getAllNotificationsByUser, deleteNotification } export { Notification } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 341f8526..7cd608b9 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -13,7 +13,7 @@ export class NotificationsRepository extends ApiRepository implements INotificat }) } - public async deleteNotificationByUser(notificationId: number): Promise { + public async deleteNotification(notificationId: number): Promise { return this.doDelete( this.buildApiEndpoint(this.notificationsResourceName, notificationId.toString()) ) diff --git a/test/functional/notifications/DeleteNotificationByUser.test.ts b/test/functional/notifications/DeleteNotification.test.ts similarity index 76% rename from test/functional/notifications/DeleteNotificationByUser.test.ts rename to test/functional/notifications/DeleteNotification.test.ts index 93ff971b..093fa637 100644 --- a/test/functional/notifications/DeleteNotificationByUser.test.ts +++ b/test/functional/notifications/DeleteNotification.test.ts @@ -1,9 +1,4 @@ -import { - ApiConfig, - deleteNotificationByUser, - getAllNotificationsByUser, - WriteError -} from '../../../src' +import { ApiConfig, deleteNotification, getAllNotificationsByUser, WriteError } from '../../../src' import { TestConstants } from '../../testHelpers/TestConstants' import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' @@ -20,13 +15,13 @@ describe('execute', () => { const notifications = await getAllNotificationsByUser.execute() const notificationId = notifications[notifications.length - 1].id - await deleteNotificationByUser.execute(notificationId) + await deleteNotification.execute(notificationId) const notificationsAfterDelete = await getAllNotificationsByUser.execute() expect(notificationsAfterDelete.length).toBe(notifications.length - 1) }) test('should throw an error when the notification id does not exist', async () => { - await expect(deleteNotificationByUser.execute(123)).rejects.toThrow(WriteError) + await expect(deleteNotification.execute(123)).rejects.toThrow(WriteError) }) }) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index b5b026ae..df619136 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -55,7 +55,7 @@ describe('NotificationsRepository', () => { const notificationToDelete = notifications[0] - await sut.deleteNotificationByUser(notificationToDelete.id) + await sut.deleteNotification(notificationToDelete.id) const notificationsAfterDelete: Notification[] = await sut.getAllNotificationsByUser() const deletedNotification = notificationsAfterDelete.find( @@ -65,14 +65,12 @@ describe('NotificationsRepository', () => { }) test('should throw error when trying to delete notification with wrong ID', async () => { - const nonExistentMetadataBlockName = 99999 + const nonExistentNotificationId = 99999 const expectedError = new WriteError( - `[404] Notification ${nonExistentMetadataBlockName} not found.` + `[404] Notification ${nonExistentNotificationId} not found.` ) - await expect(sut.deleteNotificationByUser(nonExistentMetadataBlockName)).rejects.toThrow( - expectedError - ) + await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) }) diff --git a/test/unit/notifications/DeleteNotificationByUser.test.ts b/test/unit/notifications/DeleteNotification.test.ts similarity index 70% rename from test/unit/notifications/DeleteNotificationByUser.test.ts rename to test/unit/notifications/DeleteNotification.test.ts index e8f064c3..4ad62444 100644 --- a/test/unit/notifications/DeleteNotificationByUser.test.ts +++ b/test/unit/notifications/DeleteNotification.test.ts @@ -1,4 +1,4 @@ -import { DeleteNotificationByUser } from '../../../src/notifications/domain/useCases/DeleteNotificationByUser' +import { DeleteNotification } from '../../../src/notifications/domain/useCases/DeleteNotification' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' import { Notification } from '../../../src/notifications/domain/models/Notification' @@ -23,23 +23,21 @@ describe('execute', () => { test('should delete notification from repository', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) - notificationsRepositoryStub.deleteNotificationByUser = jest - .fn() - .mockResolvedValue(mockNotifications) - const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + notificationsRepositoryStub.deleteNotification = jest.fn().mockResolvedValue(mockNotifications) + const sut = new DeleteNotification(notificationsRepositoryStub) await sut.execute(123) - expect(notificationsRepositoryStub.deleteNotificationByUser).toHaveBeenCalledWith(123) + expect(notificationsRepositoryStub.deleteNotification).toHaveBeenCalledWith(123) }) test('should throw error when repository throws error', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository notificationsRepositoryStub.getAllNotificationsByUser = jest.fn().mockResolvedValue([]) - notificationsRepositoryStub.deleteNotificationByUser = jest + notificationsRepositoryStub.deleteNotification = jest .fn() .mockRejectedValue(new Error('Repository error')) - const sut = new DeleteNotificationByUser(notificationsRepositoryStub) + const sut = new DeleteNotification(notificationsRepositoryStub) await expect(sut.execute(123)).rejects.toThrow('Repository error') }) From e93174e59e50486f65c5d0e91bd95e0cf52e35bd Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 15 Aug 2025 15:13:28 -0400 Subject: [PATCH 4/8] feat: update get notification parameter --- .../domain/models/Notification.ts | 71 ++++++++++++++++++- .../repositories/INotificationsRepository.ts | 2 +- .../useCases/GetAllNotificationsByUser.ts | 7 +- src/notifications/index.ts | 2 +- .../repositories/NotificationsRepository.ts | 11 ++- test/environment/.env | 4 +- .../GetAllNotificationsByUser.test.ts | 16 +++++ .../NotificationsRepository.test.ts | 50 +++++++++++-- .../notifications/DeleteNotification.test.ts | 15 ++-- .../GetAllNotificationsByUser.test.ts | 15 ++-- 10 files changed, 167 insertions(+), 26 deletions(-) diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index f99a2170..26f41d0d 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -1,7 +1,72 @@ +export enum NotificationType { + ASSIGNROLE = 'ASSIGNROLE', + REVOKEROLE = 'REVOKEROLE', + CREATEDV = 'CREATEDV', + CREATEDS = 'CREATEDS', + CREATEACC = 'CREATEACC', + SUBMITTEDDS = 'SUBMITTEDDS', + RETURNEDDS = 'RETURNEDDS', + PUBLISHEDDS = 'PUBLISHEDDS', + REQUESTFILEACCESS = 'REQUESTFILEACCESS', + GRANTFILEACCESS = 'GRANTFILEACCESS', + REJECTFILEACCESS = 'REJECTFILEACCESS', + FILESYSTEMIMPORT = 'FILESYSTEMIMPORT', + CHECKSUMIMPORT = 'CHECKSUMIMPORT', + CHECKSUMFAIL = 'CHECKSUMFAIL', + CONFIRMEMAIL = 'CONFIRMEMAIL', + APIGENERATED = 'APIGENERATED', + INGESTCOMPLETED = 'INGESTCOMPLETED', + INGESTCOMPLETEDWITHERRORS = 'INGESTCOMPLETEDWITHERRORS', + PUBLISHFAILED_PIDREG = 'PUBLISHFAILED_PIDREG', + WORKFLOW_SUCCESS = 'WORKFLOW_SUCCESS', + WORKFLOW_FAILURE = 'WORKFLOW_FAILURE', + STATUSUPDATED = 'STATUSUPDATED', + DATASETCREATED = 'DATASETCREATED', + DATASETMENTIONED = 'DATASETMENTIONED', + GLOBUSUPLOADCOMPLETED = 'GLOBUSUPLOADCOMPLETED', + GLOBUSUPLOADCOMPLETEDWITHERRORS = 'GLOBUSUPLOADCOMPLETEDWITHERRORS', + GLOBUSDOWNLOADCOMPLETED = 'GLOBUSDOWNLOADCOMPLETED', + GLOBUSDOWNLOADCOMPLETEDWITHERRORS = 'GLOBUSDOWNLOADCOMPLETEDWITHERRORS', + REQUESTEDFILEACCESS = 'REQUESTEDFILEACCESS', + GLOBUSUPLOADREMOTEFAILURE = 'GLOBUSUPLOADREMOTEFAILURE', + GLOBUSUPLOADLOCALFAILURE = 'GLOBUSUPLOADLOCALFAILURE', + PIDRECONCILED = 'PIDRECONCILED' +} + +export interface RoleAssignment { + id: number + assignee: string + definitionPointId: number + roleId: number + roleName: string + _roleAlias: string +} + export interface Notification { id: number - type: string - subjectText: string - messageText: string + type: NotificationType + subjectText?: string + messageText?: string sentTimestamp: string + displayAsRead?: boolean + installationBrandName?: string + userGuidesBaseUrl?: string + userGuidesVersion?: string + userGuidesSectionPath?: string + roleAssignments?: RoleAssignment[] + dataverseAlias?: string + dataverseDisplayName?: string + datasetPersistentIdentifier?: string + datasetDisplayName?: string + ownerPersistentIdentifier?: string + ownerAlias?: string + ownerDisplayName?: string + requestorFirstName?: string + requestorLastName?: string + requestorEmail?: string + dataFileId?: number + dataFileDisplayName?: string + currentCurationStatus?: string + additionalInfo?: string + objectDeleted?: boolean } diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index dd40d071..c414f8b8 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -1,6 +1,6 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { - getAllNotificationsByUser(): Promise + getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise deleteNotification(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts index 1eb70ac8..43555ccc 100644 --- a/src/notifications/domain/useCases/GetAllNotificationsByUser.ts +++ b/src/notifications/domain/useCases/GetAllNotificationsByUser.ts @@ -8,9 +8,12 @@ export class GetAllNotificationsByUser implements UseCase { /** * Use case for retrieving all notifications for the current user. * + * @param inAppNotificationFormat - Optional parameter to retrieve fields needed for in-app notifications * @returns {Promise} - A promise that resolves to an array of Notification instances. */ - async execute(): Promise { - return (await this.notificationsRepository.getAllNotificationsByUser()) as Notification[] + async execute(inAppNotificationFormat?: boolean): Promise { + return (await this.notificationsRepository.getAllNotificationsByUser( + inAppNotificationFormat + )) as Notification[] } } diff --git a/src/notifications/index.ts b/src/notifications/index.ts index 72ae6e82..8e97bfcc 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -9,4 +9,4 @@ const deleteNotification = new DeleteNotification(notificationsRepository) export { getAllNotificationsByUser, deleteNotification } -export { Notification } from './domain/models/Notification' +export { Notification, NotificationType, RoleAssignment } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 7cd608b9..b298d040 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -5,8 +5,15 @@ import { Notification } from '../../domain/models/Notification' export class NotificationsRepository extends ApiRepository implements INotificationsRepository { private readonly notificationsResourceName: string = 'notifications' - public async getAllNotificationsByUser(): Promise { - return this.doGet(this.buildApiEndpoint(this.notificationsResourceName, 'all'), true) + public async getAllNotificationsByUser( + inAppNotificationFormat?: boolean + ): Promise { + const queryParams = inAppNotificationFormat ? { inAppNotificationFormat: 'true' } : undefined + return this.doGet( + this.buildApiEndpoint(this.notificationsResourceName, 'all'), + true, + queryParams + ) .then((response) => response.data.data.notifications as Notification[]) .catch((error) => { throw error diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..d7d1957e 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=11648-notifications-api-extension DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 59d950d9..11f9a33c 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -25,4 +25,20 @@ describe('execute', () => { expect(notifications[0]).toHaveProperty('type') expect(notifications[0]).toHaveProperty('sentTimestamp') }) + + test('should have correct in-app notification properties when inAppNotificationFormat is true', async () => { + const notifications = await getAllNotificationsByUser.execute(true) + + expect(notifications[0]).toHaveProperty('id') + expect(notifications[0]).toHaveProperty('type') + expect(notifications[0]).toHaveProperty('sentTimestamp') + expect(notifications[0]).toHaveProperty('displayAsRead') + expect(notifications[0]).toHaveProperty('roleAssignments') + expect(notifications[0].roleAssignments).toBeDefined() + expect(notifications[0].roleAssignments?.length).toBeGreaterThan(0) + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleId', 1) + expect(notifications[0].roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) + }) }) diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index df619136..d29dbb49 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -4,13 +4,21 @@ import { } from '../../../src/core/infra/repositories/ApiConfig' import { TestConstants } from '../../testHelpers/TestConstants' import { NotificationsRepository } from '../../../src/notifications/infra/repositories/NotificationsRepository' -import { Notification } from '../../../src/notifications/domain/models/Notification' -import { createDataset } from '../../../src/datasets' -import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' +import { + Notification, + NotificationType +} from '../../../src/notifications/domain/models/Notification' +import { createDataset, CreatedDatasetIdentifiers } from '../../../src/datasets' +import { + deletePublishedDatasetViaApi, + publishDatasetViaApi, + waitForNoLocks +} from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' describe('NotificationsRepository', () => { const sut: NotificationsRepository = new NotificationsRepository() + let testDatasetIds: CreatedDatasetIdentifiers beforeEach(() => { ApiConfig.init( @@ -20,9 +28,13 @@ describe('NotificationsRepository', () => { ) }) + afterAll(async () => { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + }) + test('should return notifications after creating and publishing a dataset', async () => { // Create a dataset and publish it so that a notification of Dataset published is created - const testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) await publishDatasetViaApi(testDatasetIds.numericId) await waitForNoLocks(testDatasetIds.numericId, 10) @@ -32,7 +44,9 @@ describe('NotificationsRepository', () => { expect(Array.isArray(notifications)).toBe(true) expect(notifications.length).toBeGreaterThan(0) - const publishedNotification = notifications.find((n) => n.type === 'PUBLISHEDDS') + const publishedNotification = notifications.find( + (n) => n.type === NotificationType.PUBLISHEDDS + ) as Notification expect(publishedNotification).toBeDefined() @@ -73,4 +87,30 @@ describe('NotificationsRepository', () => { await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) + + test('should return notifications with basic properties when inAppNotificationFormat is true', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + + const notification = notifications[0] + expect(notification).toHaveProperty('id') + expect(notification).toHaveProperty('type') + expect(notification.type).toBe(NotificationType.ASSIGNROLE) + expect(notification).toHaveProperty('sentTimestamp') + expect(notification).toHaveProperty('displayAsRead') + expect(notification).toHaveProperty('dataverseDisplayName') + + expect(notification).toHaveProperty('roleAssignments') + expect(notification.roleAssignments).toBeDefined() + expect(notification.roleAssignments?.length).toBeGreaterThan(0) + expect(notification.roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') + expect(notification.roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') + expect(notification.roleAssignments?.[0]).toHaveProperty('roleId', 1) + expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) + }) + + test('should return array when inAppNotificationFormat is false', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(false) + + expect(Array.isArray(notifications)).toBe(true) + }) }) diff --git a/test/unit/notifications/DeleteNotification.test.ts b/test/unit/notifications/DeleteNotification.test.ts index 4ad62444..d568b975 100644 --- a/test/unit/notifications/DeleteNotification.test.ts +++ b/test/unit/notifications/DeleteNotification.test.ts @@ -1,21 +1,26 @@ import { DeleteNotification } from '../../../src/notifications/domain/useCases/DeleteNotification' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' -import { Notification } from '../../../src/notifications/domain/models/Notification' +import { + Notification, + NotificationType +} from '../../../src/notifications/domain/models/Notification' const mockNotifications: Notification[] = [ { id: 1, - type: 'PUBLISHEDDS', + type: NotificationType.PUBLISHEDDS, subjectText: 'Test notification', messageText: 'Test message', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false }, { id: 2, - type: 'ASSIGNROLE', + type: NotificationType.ASSIGNROLE, subjectText: 'Role assignment', messageText: 'Role assigned', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false } ] diff --git a/test/unit/notifications/GetAllNotificationsByUser.test.ts b/test/unit/notifications/GetAllNotificationsByUser.test.ts index 5779f303..7df67bca 100644 --- a/test/unit/notifications/GetAllNotificationsByUser.test.ts +++ b/test/unit/notifications/GetAllNotificationsByUser.test.ts @@ -1,21 +1,26 @@ import { GetAllNotificationsByUser } from '../../../src/notifications/domain/useCases/GetAllNotificationsByUser' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' -import { Notification } from '../../../src/notifications/domain/models/Notification' +import { + Notification, + NotificationType +} from '../../../src/notifications/domain/models/Notification' const mockNotifications: Notification[] = [ { id: 1, - type: 'PUBLISHEDDS', + type: NotificationType.PUBLISHEDDS, subjectText: 'Test notification', messageText: 'Test message', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false }, { id: 2, - type: 'ASSIGNROLE', + type: NotificationType.ASSIGNROLE, subjectText: 'Role assignment', messageText: 'Role assigned', - sentTimestamp: '2025-01-01T00:00:00Z' + sentTimestamp: '2025-01-01T00:00:00Z', + displayAsRead: false } ] From 541e892199533ba870b38565c9064a393d19d226 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 18 Aug 2025 12:06:00 -0400 Subject: [PATCH 5/8] feat: display as read --- docs/useCases.md | 44 +++++++++++++++ .../domain/models/Notification.ts | 2 +- .../repositories/INotificationsRepository.ts | 2 + .../domain/useCases/GetUnreadCount.ts | 14 +++++ .../domain/useCases/MarkAsRead.ts | 14 +++++ src/notifications/index.ts | 6 +- .../repositories/NotificationsRepository.ts | 16 +++++- .../GetAllNotificationsByUser.test.ts | 7 --- .../notifications/GetUnreadCount.test.ts | 10 ++++ .../notifications/MarkAsRead.test.ts | 1 + .../NotificationsRepository.test.ts | 55 +++++++++++++------ .../unit/notifications/GetUnreadCount.test.ts | 25 +++++++++ test/unit/notifications/MarkAsRead.test.ts | 24 ++++++++ 13 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 src/notifications/domain/useCases/GetUnreadCount.ts create mode 100644 src/notifications/domain/useCases/MarkAsRead.ts create mode 100644 test/functional/notifications/GetUnreadCount.test.ts create mode 100644 test/functional/notifications/MarkAsRead.test.ts create mode 100644 test/unit/notifications/GetUnreadCount.test.ts create mode 100644 test/unit/notifications/MarkAsRead.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index fec2a009..faaadb16 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -91,6 +91,8 @@ The different use cases currently available in the package are classified below, - [Notifications](#Notifications) - [Get All Notifications by User](#get-all-notifications-by-user) - [Delete Notification](#delete-notification) + - [Get Unread Count](#get-unread-count) + - [Mark As Read](#mark-as-read) - [Search](#Search) - [Get Search Services](#get-search-services) @@ -2143,6 +2145,48 @@ deleteNotification.execute(notificationId: number).then(() => { _See [use case](../src/notifications/domain/useCases/DeleteNotification.ts) implementation_. +#### Get Unread Count + +Returns the number of unread notifications for the current authenticated user. + +##### Example call: + +```typescript +import { getUnreadCount } from '@iqss/dataverse-client-javascript' + +/* ... */ + +getUnreadCount.execute().then((count: number) => { + console.log(`You have ${count} unread notifications`) +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/GetUnreadCount.ts) implementation_. + +#### Mark As Read + +Marks a specific notification as read for the current authenticated user. This operation is idempotent - marking an already-read notification as read will not cause an error. + +##### Example call: + +```typescript +import { markAsRead } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const notificationId = 123 + +markAsRead.execute(notificationId).then(() => { + console.log('Notification marked as read') +}) + +/* ... */ +``` + +_See [use case](../src/notifications/domain/useCases/MarkAsRead.ts) implementation_. + ## Search #### Get Search Services diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index 26f41d0d..04243636 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -48,7 +48,7 @@ export interface Notification { subjectText?: string messageText?: string sentTimestamp: string - displayAsRead?: boolean + displayAsRead: boolean installationBrandName?: string userGuidesBaseUrl?: string userGuidesVersion?: string diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index c414f8b8..d2e51467 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -3,4 +3,6 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise deleteNotification(notificationId: number): Promise + getUnreadCount(): Promise + markAsRead(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/GetUnreadCount.ts b/src/notifications/domain/useCases/GetUnreadCount.ts new file mode 100644 index 00000000..a7940ebb --- /dev/null +++ b/src/notifications/domain/useCases/GetUnreadCount.ts @@ -0,0 +1,14 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +export class GetUnreadCount implements UseCase { + private notificationsRepository: INotificationsRepository + + constructor(notificationsRepository: INotificationsRepository) { + this.notificationsRepository = notificationsRepository + } + + async execute(): Promise { + return await this.notificationsRepository.getUnreadCount() + } +} diff --git a/src/notifications/domain/useCases/MarkAsRead.ts b/src/notifications/domain/useCases/MarkAsRead.ts new file mode 100644 index 00000000..bb7f849d --- /dev/null +++ b/src/notifications/domain/useCases/MarkAsRead.ts @@ -0,0 +1,14 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { INotificationsRepository } from '../repositories/INotificationsRepository' + +export class MarkAsRead implements UseCase { + private notificationsRepository: INotificationsRepository + + constructor(notificationsRepository: INotificationsRepository) { + this.notificationsRepository = notificationsRepository + } + + async execute(notificationId: number): Promise { + return await this.notificationsRepository.markAsRead(notificationId) + } +} diff --git a/src/notifications/index.ts b/src/notifications/index.ts index 8e97bfcc..1c819dbc 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -1,12 +1,16 @@ import { NotificationsRepository } from './infra/repositories/NotificationsRepository' import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' import { DeleteNotification } from './domain/useCases/DeleteNotification' +import { GetUnreadCount } from './domain/useCases/GetUnreadCount' +import { MarkAsRead } from './domain/useCases/MarkAsRead' const notificationsRepository = new NotificationsRepository() const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) const deleteNotification = new DeleteNotification(notificationsRepository) +const getUnreadCount = new GetUnreadCount(notificationsRepository) +const markAsRead = new MarkAsRead(notificationsRepository) -export { getAllNotificationsByUser, deleteNotification } +export { getAllNotificationsByUser, deleteNotification, getUnreadCount, markAsRead } export { Notification, NotificationType, RoleAssignment } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index b298d040..8d677721 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -24,9 +24,23 @@ export class NotificationsRepository extends ApiRepository implements INotificat return this.doDelete( this.buildApiEndpoint(this.notificationsResourceName, notificationId.toString()) ) - .then(() => {}) + .then(() => undefined) .catch((error) => { throw error }) } + + public async getUnreadCount(): Promise { + return this.doGet( + this.buildApiEndpoint(this.notificationsResourceName, 'unreadCount'), + true + ).then((response) => response.data.data.unreadCount as number) + } + + public async markAsRead(notificationId: number): Promise { + return this.doPut( + this.buildApiEndpoint(this.notificationsResourceName, 'markAsRead', notificationId), + {} + ).then(() => undefined) + } } diff --git a/test/functional/notifications/GetAllNotificationsByUser.test.ts b/test/functional/notifications/GetAllNotificationsByUser.test.ts index 11f9a33c..7ccd7ec1 100644 --- a/test/functional/notifications/GetAllNotificationsByUser.test.ts +++ b/test/functional/notifications/GetAllNotificationsByUser.test.ts @@ -33,12 +33,5 @@ describe('execute', () => { expect(notifications[0]).toHaveProperty('type') expect(notifications[0]).toHaveProperty('sentTimestamp') expect(notifications[0]).toHaveProperty('displayAsRead') - expect(notifications[0]).toHaveProperty('roleAssignments') - expect(notifications[0].roleAssignments).toBeDefined() - expect(notifications[0].roleAssignments?.length).toBeGreaterThan(0) - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('roleId', 1) - expect(notifications[0].roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) }) }) diff --git a/test/functional/notifications/GetUnreadCount.test.ts b/test/functional/notifications/GetUnreadCount.test.ts new file mode 100644 index 00000000..cecb97c2 --- /dev/null +++ b/test/functional/notifications/GetUnreadCount.test.ts @@ -0,0 +1,10 @@ +import { getUnreadCount } from '../../../src/notifications' + +describe('GetUnreadCount', () => { + test('should return unread count', async () => { + const unreadCount = await getUnreadCount.execute() + + expect(typeof unreadCount).toBe('number') + expect(unreadCount).toBeGreaterThanOrEqual(0) + }) +}) diff --git a/test/functional/notifications/MarkAsRead.test.ts b/test/functional/notifications/MarkAsRead.test.ts new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/test/functional/notifications/MarkAsRead.test.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index d29dbb49..809d201f 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -9,11 +9,7 @@ import { NotificationType } from '../../../src/notifications/domain/models/Notification' import { createDataset, CreatedDatasetIdentifiers } from '../../../src/datasets' -import { - deletePublishedDatasetViaApi, - publishDatasetViaApi, - waitForNoLocks -} from '../../testHelpers/datasets/datasetHelper' +import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' describe('NotificationsRepository', () => { @@ -28,10 +24,6 @@ describe('NotificationsRepository', () => { ) }) - afterAll(async () => { - await deletePublishedDatasetViaApi(testDatasetIds.persistentId) - }) - test('should return notifications after creating and publishing a dataset', async () => { // Create a dataset and publish it so that a notification of Dataset published is created testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) @@ -81,9 +73,7 @@ describe('NotificationsRepository', () => { test('should throw error when trying to delete notification with wrong ID', async () => { const nonExistentNotificationId = 99999 - const expectedError = new WriteError( - `[404] Notification ${nonExistentNotificationId} not found.` - ) + const expectedError = new WriteError() await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) @@ -102,10 +92,10 @@ describe('NotificationsRepository', () => { expect(notification).toHaveProperty('roleAssignments') expect(notification.roleAssignments).toBeDefined() expect(notification.roleAssignments?.length).toBeGreaterThan(0) - expect(notification.roleAssignments?.[0]).toHaveProperty('roleName', 'Admin') - expect(notification.roleAssignments?.[0]).toHaveProperty('assignee', '@dataverseAdmin') - expect(notification.roleAssignments?.[0]).toHaveProperty('roleId', 1) - expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId', 1) + expect(notification.roleAssignments?.[0]).toHaveProperty('roleName') + expect(notification.roleAssignments?.[0]).toHaveProperty('assignee') + expect(notification.roleAssignments?.[0]).toHaveProperty('roleId') + expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) test('should return array when inAppNotificationFormat is false', async () => { @@ -113,4 +103,37 @@ describe('NotificationsRepository', () => { expect(Array.isArray(notifications)).toBe(true) }) + + test('should return unread count', async () => { + const unreadCount = await sut.getUnreadCount() + + console.log('unreadCount', unreadCount) + expect(typeof unreadCount).toBe('number') + expect(unreadCount).toBeGreaterThanOrEqual(0) + }) + + test('should mark notification as read successfully', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser() + + expect(notifications.length).toBeGreaterThan(0) + + const unreadNotification = notifications[0] + + await expect(sut.markAsRead(unreadNotification.id)).resolves.toBeUndefined() + + const updatedNotifications: Notification[] = await sut.getAllNotificationsByUser() + const updatedNotification = updatedNotifications.find((n) => n.id === unreadNotification.id) + + expect(updatedNotification?.displayAsRead).toBe(true) + }) + + test('should throw error when marking non-existent notification as read', async () => { + const nonExistentNotificationId = 99999 + + const expectedError = new WriteError( + `[404] Notification ${nonExistentNotificationId} not found.` + ) + + await expect(sut.markAsRead(nonExistentNotificationId)).rejects.toThrow(expectedError) + }) }) diff --git a/test/unit/notifications/GetUnreadCount.test.ts b/test/unit/notifications/GetUnreadCount.test.ts new file mode 100644 index 00000000..af049c56 --- /dev/null +++ b/test/unit/notifications/GetUnreadCount.test.ts @@ -0,0 +1,25 @@ +import { GetUnreadCount } from '../../../src/notifications/domain/useCases/GetUnreadCount' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { ReadError } from '../../../src' + +describe('GetUnreadCount', () => { + test('should return unread count from repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + + notificationsRepositoryStub.getUnreadCount = jest.fn().mockResolvedValue(5) + const sut = new GetUnreadCount(notificationsRepositoryStub) + + const result = await sut.execute() + + expect(notificationsRepositoryStub.getUnreadCount).toHaveBeenCalledWith() + expect(result).toBe(5) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.getUnreadCount = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetUnreadCount(notificationsRepositoryStub) + + await expect(sut.execute()).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/notifications/MarkAsRead.test.ts b/test/unit/notifications/MarkAsRead.test.ts new file mode 100644 index 00000000..41616a11 --- /dev/null +++ b/test/unit/notifications/MarkAsRead.test.ts @@ -0,0 +1,24 @@ +import { MarkAsRead } from '../../../src/notifications/domain/useCases/MarkAsRead' +import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' +import { ReadError } from '../../../src' + +describe('MarkAsRead', () => { + test('should mark notification as read in repository', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + + notificationsRepositoryStub.markAsRead = jest.fn().mockResolvedValue(undefined) + const sut = new MarkAsRead(notificationsRepositoryStub) + + await sut.execute(123) + + expect(notificationsRepositoryStub.markAsRead).toHaveBeenCalledWith(123) + }) + + test('should throw error when repository throws error', async () => { + const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository + notificationsRepositoryStub.markAsRead = jest.fn().mockRejectedValue(new ReadError()) + const sut = new MarkAsRead(notificationsRepositoryStub) + + await expect(sut.execute(123)).rejects.toThrow(ReadError) + }) +}) From de95d37e63c4f0f20f51ebbe31df3620a8564b5d Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 18 Aug 2025 12:20:50 -0400 Subject: [PATCH 6/8] fix on tests --- .../domain/useCases/GetUnreadCount.ts | 5 +++ .../domain/useCases/MarkAsRead.ts | 6 ++++ .../notifications/GetUnreadCount.test.ts | 10 ------ .../notifications/MarkAsRead.test.ts | 1 - .../NotificationsRepository.test.ts | 35 ++++++++++++------- 5 files changed, 34 insertions(+), 23 deletions(-) delete mode 100644 test/functional/notifications/GetUnreadCount.test.ts delete mode 100644 test/functional/notifications/MarkAsRead.test.ts diff --git a/src/notifications/domain/useCases/GetUnreadCount.ts b/src/notifications/domain/useCases/GetUnreadCount.ts index a7940ebb..fff13899 100644 --- a/src/notifications/domain/useCases/GetUnreadCount.ts +++ b/src/notifications/domain/useCases/GetUnreadCount.ts @@ -8,6 +8,11 @@ export class GetUnreadCount implements UseCase { this.notificationsRepository = notificationsRepository } + /** + * Use case for retrieving the number of unread notifications for the current user. + * + * @returns {Promise} - A promise that resolves to the number of unread notifications. + */ async execute(): Promise { return await this.notificationsRepository.getUnreadCount() } diff --git a/src/notifications/domain/useCases/MarkAsRead.ts b/src/notifications/domain/useCases/MarkAsRead.ts index bb7f849d..2789713e 100644 --- a/src/notifications/domain/useCases/MarkAsRead.ts +++ b/src/notifications/domain/useCases/MarkAsRead.ts @@ -8,6 +8,12 @@ export class MarkAsRead implements UseCase { this.notificationsRepository = notificationsRepository } + /** + * Use case for marking a notification as read. + * + * @param notificationId - The ID of the notification to mark as read. + * @returns {Promise} - A promise that resolves when the notification is marked as read. + */ async execute(notificationId: number): Promise { return await this.notificationsRepository.markAsRead(notificationId) } diff --git a/test/functional/notifications/GetUnreadCount.test.ts b/test/functional/notifications/GetUnreadCount.test.ts deleted file mode 100644 index cecb97c2..00000000 --- a/test/functional/notifications/GetUnreadCount.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getUnreadCount } from '../../../src/notifications' - -describe('GetUnreadCount', () => { - test('should return unread count', async () => { - const unreadCount = await getUnreadCount.execute() - - expect(typeof unreadCount).toBe('number') - expect(unreadCount).toBeGreaterThanOrEqual(0) - }) -}) diff --git a/test/functional/notifications/MarkAsRead.test.ts b/test/functional/notifications/MarkAsRead.test.ts deleted file mode 100644 index 0519ecba..00000000 --- a/test/functional/notifications/MarkAsRead.test.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 809d201f..2b7e7e13 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -73,7 +73,9 @@ describe('NotificationsRepository', () => { test('should throw error when trying to delete notification with wrong ID', async () => { const nonExistentNotificationId = 99999 - const expectedError = new WriteError() + const expectedError = new WriteError( + `[404] Notification ${nonExistentNotificationId} not found.` + ) await expect(sut.deleteNotification(nonExistentNotificationId)).rejects.toThrow(expectedError) }) @@ -84,18 +86,28 @@ describe('NotificationsRepository', () => { const notification = notifications[0] expect(notification).toHaveProperty('id') expect(notification).toHaveProperty('type') - expect(notification.type).toBe(NotificationType.ASSIGNROLE) expect(notification).toHaveProperty('sentTimestamp') expect(notification).toHaveProperty('displayAsRead') - expect(notification).toHaveProperty('dataverseDisplayName') - - expect(notification).toHaveProperty('roleAssignments') - expect(notification.roleAssignments).toBeDefined() - expect(notification.roleAssignments?.length).toBeGreaterThan(0) - expect(notification.roleAssignments?.[0]).toHaveProperty('roleName') - expect(notification.roleAssignments?.[0]).toHaveProperty('assignee') - expect(notification.roleAssignments?.[0]).toHaveProperty('roleId') - expect(notification.roleAssignments?.[0]).toHaveProperty('definitionPointId') + }) + + test('should find notification with ASSIGNROLE type', async () => { + const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + + // Find a notification with ASSIGNROLE type + const assignRoleNotification = notifications.find((n) => n.type === NotificationType.ASSIGNROLE) + + expect(assignRoleNotification).toBeDefined() + expect(assignRoleNotification?.type).toBe(NotificationType.ASSIGNROLE) + expect(assignRoleNotification?.sentTimestamp).toBeDefined() + expect(assignRoleNotification?.displayAsRead).toBeDefined() + expect(assignRoleNotification?.dataverseDisplayName).toBeDefined() + + expect(assignRoleNotification?.roleAssignments).toBeDefined() + expect(assignRoleNotification?.roleAssignments?.length).toBeGreaterThan(0) + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('roleName') + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('assignee') + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('roleId') + expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) test('should return array when inAppNotificationFormat is false', async () => { @@ -107,7 +119,6 @@ describe('NotificationsRepository', () => { test('should return unread count', async () => { const unreadCount = await sut.getUnreadCount() - console.log('unreadCount', unreadCount) expect(typeof unreadCount).toBe('number') expect(unreadCount).toBeGreaterThanOrEqual(0) }) From e087a57dffa803cf737f25c677cc005e8b8c21e4 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 18 Aug 2025 21:24:44 -0400 Subject: [PATCH 7/8] update env. variables --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index d7d1957e..e7b54bde 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=11648-notifications-api-extension +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m From b61f8cf8b081f6a753f165ade93ca4b3e7e3107f Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 22 Aug 2025 09:52:03 -0400 Subject: [PATCH 8/8] fix: update naming and test --- docs/useCases.md | 12 ++--- .../domain/models/Notification.ts | 4 +- .../repositories/INotificationsRepository.ts | 4 +- ...ount.ts => GetUnreadNotificationsCount.ts} | 4 +- ...arkAsRead.ts => MarkNotificationAsRead.ts} | 4 +- src/notifications/index.ts | 15 ++++-- .../repositories/NotificationsRepository.ts | 17 +++++-- .../infra/transformers/NotificationPayload.ts | 30 +++++++++++ .../NotificationsRepository.test.ts | 50 ++++++++++++++++--- .../unit/notifications/GetUnreadCount.test.ts | 16 +++--- test/unit/notifications/MarkAsRead.test.ts | 16 +++--- 11 files changed, 129 insertions(+), 43 deletions(-) rename src/notifications/domain/useCases/{GetUnreadCount.ts => GetUnreadNotificationsCount.ts} (80%) rename src/notifications/domain/useCases/{MarkAsRead.ts => MarkNotificationAsRead.ts} (81%) create mode 100644 src/notifications/infra/transformers/NotificationPayload.ts diff --git a/docs/useCases.md b/docs/useCases.md index faaadb16..bb3a2951 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -2152,18 +2152,18 @@ Returns the number of unread notifications for the current authenticated user. ##### Example call: ```typescript -import { getUnreadCount } from '@iqss/dataverse-client-javascript' +import { getUnreadNotificationsCount } from '@iqss/dataverse-client-javascript' /* ... */ -getUnreadCount.execute().then((count: number) => { +getUnreadNotificationsCount.execute().then((count: number) => { console.log(`You have ${count} unread notifications`) }) /* ... */ ``` -_See [use case](../src/notifications/domain/useCases/GetUnreadCount.ts) implementation_. +_See [use case](../src/notifications/domain/useCases/GetUnreadNotificationsCount.ts) implementation_. #### Mark As Read @@ -2172,20 +2172,20 @@ Marks a specific notification as read for the current authenticated user. This o ##### Example call: ```typescript -import { markAsRead } from '@iqss/dataverse-client-javascript' +import { markNotificationAsRead } from '@iqss/dataverse-client-javascript' /* ... */ const notificationId = 123 -markAsRead.execute(notificationId).then(() => { +markNotificationAsRead.execute(notificationId).then(() => { console.log('Notification marked as read') }) /* ... */ ``` -_See [use case](../src/notifications/domain/useCases/MarkAsRead.ts) implementation_. +_See [use case](../src/notifications/domain/useCases/MarkNotificationAsRead.ts) implementation_. ## Search diff --git a/src/notifications/domain/models/Notification.ts b/src/notifications/domain/models/Notification.ts index 04243636..001d0933 100644 --- a/src/notifications/domain/models/Notification.ts +++ b/src/notifications/domain/models/Notification.ts @@ -54,8 +54,8 @@ export interface Notification { userGuidesVersion?: string userGuidesSectionPath?: string roleAssignments?: RoleAssignment[] - dataverseAlias?: string - dataverseDisplayName?: string + collectionAlias?: string + collectionDisplayName?: string datasetPersistentIdentifier?: string datasetDisplayName?: string ownerPersistentIdentifier?: string diff --git a/src/notifications/domain/repositories/INotificationsRepository.ts b/src/notifications/domain/repositories/INotificationsRepository.ts index d2e51467..9392c543 100644 --- a/src/notifications/domain/repositories/INotificationsRepository.ts +++ b/src/notifications/domain/repositories/INotificationsRepository.ts @@ -3,6 +3,6 @@ import { Notification } from '../models/Notification' export interface INotificationsRepository { getAllNotificationsByUser(inAppNotificationFormat?: boolean): Promise deleteNotification(notificationId: number): Promise - getUnreadCount(): Promise - markAsRead(notificationId: number): Promise + getUnreadNotificationsCount(): Promise + markNotificationAsRead(notificationId: number): Promise } diff --git a/src/notifications/domain/useCases/GetUnreadCount.ts b/src/notifications/domain/useCases/GetUnreadNotificationsCount.ts similarity index 80% rename from src/notifications/domain/useCases/GetUnreadCount.ts rename to src/notifications/domain/useCases/GetUnreadNotificationsCount.ts index fff13899..2e59c55e 100644 --- a/src/notifications/domain/useCases/GetUnreadCount.ts +++ b/src/notifications/domain/useCases/GetUnreadNotificationsCount.ts @@ -1,7 +1,7 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { INotificationsRepository } from '../repositories/INotificationsRepository' -export class GetUnreadCount implements UseCase { +export class GetUnreadNotificationsCount implements UseCase { private notificationsRepository: INotificationsRepository constructor(notificationsRepository: INotificationsRepository) { @@ -14,6 +14,6 @@ export class GetUnreadCount implements UseCase { * @returns {Promise} - A promise that resolves to the number of unread notifications. */ async execute(): Promise { - return await this.notificationsRepository.getUnreadCount() + return await this.notificationsRepository.getUnreadNotificationsCount() } } diff --git a/src/notifications/domain/useCases/MarkAsRead.ts b/src/notifications/domain/useCases/MarkNotificationAsRead.ts similarity index 81% rename from src/notifications/domain/useCases/MarkAsRead.ts rename to src/notifications/domain/useCases/MarkNotificationAsRead.ts index 2789713e..017be28c 100644 --- a/src/notifications/domain/useCases/MarkAsRead.ts +++ b/src/notifications/domain/useCases/MarkNotificationAsRead.ts @@ -1,7 +1,7 @@ import { UseCase } from '../../../core/domain/useCases/UseCase' import { INotificationsRepository } from '../repositories/INotificationsRepository' -export class MarkAsRead implements UseCase { +export class MarkNotificationAsRead implements UseCase { private notificationsRepository: INotificationsRepository constructor(notificationsRepository: INotificationsRepository) { @@ -15,6 +15,6 @@ export class MarkAsRead implements UseCase { * @returns {Promise} - A promise that resolves when the notification is marked as read. */ async execute(notificationId: number): Promise { - return await this.notificationsRepository.markAsRead(notificationId) + return await this.notificationsRepository.markNotificationAsRead(notificationId) } } diff --git a/src/notifications/index.ts b/src/notifications/index.ts index 1c819dbc..3075ee90 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -1,16 +1,21 @@ import { NotificationsRepository } from './infra/repositories/NotificationsRepository' import { GetAllNotificationsByUser } from './domain/useCases/GetAllNotificationsByUser' import { DeleteNotification } from './domain/useCases/DeleteNotification' -import { GetUnreadCount } from './domain/useCases/GetUnreadCount' -import { MarkAsRead } from './domain/useCases/MarkAsRead' +import { GetUnreadNotificationsCount } from './domain/useCases/GetUnreadNotificationsCount' +import { MarkNotificationAsRead } from './domain/useCases/MarkNotificationAsRead' const notificationsRepository = new NotificationsRepository() const getAllNotificationsByUser = new GetAllNotificationsByUser(notificationsRepository) const deleteNotification = new DeleteNotification(notificationsRepository) -const getUnreadCount = new GetUnreadCount(notificationsRepository) -const markAsRead = new MarkAsRead(notificationsRepository) +const getUnreadNotificationsCount = new GetUnreadNotificationsCount(notificationsRepository) +const markNotificationAsRead = new MarkNotificationAsRead(notificationsRepository) -export { getAllNotificationsByUser, deleteNotification, getUnreadCount, markAsRead } +export { + getAllNotificationsByUser, + deleteNotification, + getUnreadNotificationsCount, + markNotificationAsRead +} export { Notification, NotificationType, RoleAssignment } from './domain/models/Notification' diff --git a/src/notifications/infra/repositories/NotificationsRepository.ts b/src/notifications/infra/repositories/NotificationsRepository.ts index 8d677721..f310c34a 100644 --- a/src/notifications/infra/repositories/NotificationsRepository.ts +++ b/src/notifications/infra/repositories/NotificationsRepository.ts @@ -1,6 +1,7 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { INotificationsRepository } from '../../domain/repositories/INotificationsRepository' import { Notification } from '../../domain/models/Notification' +import { NotificationPayload } from '../transformers/NotificationPayload' export class NotificationsRepository extends ApiRepository implements INotificationsRepository { private readonly notificationsResourceName: string = 'notifications' @@ -14,7 +15,17 @@ export class NotificationsRepository extends ApiRepository implements INotificat true, queryParams ) - .then((response) => response.data.data.notifications as Notification[]) + .then((response) => { + const notifications = response.data.data.notifications + return notifications.map((notification: NotificationPayload) => { + const { dataverseDisplayName, dataverseAlias, ...restNotification } = notification + return { + ...restNotification, + ...(dataverseDisplayName && { collectionDisplayName: dataverseDisplayName }), + ...(dataverseAlias && { collectionAlias: dataverseAlias }) + } + }) as Notification[] + }) .catch((error) => { throw error }) @@ -30,14 +41,14 @@ export class NotificationsRepository extends ApiRepository implements INotificat }) } - public async getUnreadCount(): Promise { + public async getUnreadNotificationsCount(): Promise { return this.doGet( this.buildApiEndpoint(this.notificationsResourceName, 'unreadCount'), true ).then((response) => response.data.data.unreadCount as number) } - public async markAsRead(notificationId: number): Promise { + public async markNotificationAsRead(notificationId: number): Promise { return this.doPut( this.buildApiEndpoint(this.notificationsResourceName, 'markAsRead', notificationId), {} diff --git a/src/notifications/infra/transformers/NotificationPayload.ts b/src/notifications/infra/transformers/NotificationPayload.ts new file mode 100644 index 00000000..96d381ac --- /dev/null +++ b/src/notifications/infra/transformers/NotificationPayload.ts @@ -0,0 +1,30 @@ +import { RoleAssignment } from '../../domain/models/Notification' + +export interface NotificationPayload { + id: number + type: string + subjectText?: string + messageText?: string + sentTimestamp: string + displayAsRead: boolean + installationBrandName?: string + userGuidesBaseUrl?: string + userGuidesVersion?: string + userGuidesSectionPath?: string + roleAssignments?: RoleAssignment[] + dataverseAlias?: string + dataverseDisplayName?: string + datasetPersistentIdentifier?: string + datasetDisplayName?: string + ownerPersistentIdentifier?: string + ownerAlias?: string + ownerDisplayName?: string + requestorFirstName?: string + requestorLastName?: string + requestorEmail?: string + dataFileId?: number + dataFileDisplayName?: string + currentCurationStatus?: string + additionalInfo?: string + objectDeleted?: boolean +} diff --git a/test/integration/notifications/NotificationsRepository.test.ts b/test/integration/notifications/NotificationsRepository.test.ts index 2b7e7e13..5333e48d 100644 --- a/test/integration/notifications/NotificationsRepository.test.ts +++ b/test/integration/notifications/NotificationsRepository.test.ts @@ -11,6 +11,11 @@ import { import { createDataset, CreatedDatasetIdentifiers } from '../../../src/datasets' import { publishDatasetViaApi, waitForNoLocks } from '../../testHelpers/datasets/datasetHelper' import { WriteError } from '../../../src' +import { createCollection } from '../../../src/collections' +import { + createCollectionDTO, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' describe('NotificationsRepository', () => { const sut: NotificationsRepository = new NotificationsRepository() @@ -90,17 +95,18 @@ describe('NotificationsRepository', () => { expect(notification).toHaveProperty('displayAsRead') }) - test('should find notification with ASSIGNROLE type', async () => { + test('should find notification with ASSIGNROLE type that has not been deleted', async () => { const notifications: Notification[] = await sut.getAllNotificationsByUser(true) - // Find a notification with ASSIGNROLE type - const assignRoleNotification = notifications.find((n) => n.type === NotificationType.ASSIGNROLE) + const assignRoleNotification = notifications.find( + (n) => n.type === NotificationType.ASSIGNROLE && !n.objectDeleted + ) expect(assignRoleNotification).toBeDefined() expect(assignRoleNotification?.type).toBe(NotificationType.ASSIGNROLE) expect(assignRoleNotification?.sentTimestamp).toBeDefined() expect(assignRoleNotification?.displayAsRead).toBeDefined() - expect(assignRoleNotification?.dataverseDisplayName).toBeDefined() + expect(assignRoleNotification?.collectionDisplayName).toBeDefined() expect(assignRoleNotification?.roleAssignments).toBeDefined() expect(assignRoleNotification?.roleAssignments?.length).toBeGreaterThan(0) @@ -110,6 +116,34 @@ describe('NotificationsRepository', () => { expect(assignRoleNotification?.roleAssignments?.[0]).toHaveProperty('definitionPointId') }) + test('should create a collection and find the notification with CREATEDV type', async () => { + const testCollectionAlias = 'test-notification-collection' + const createdCollectionId = await createCollection.execute( + createCollectionDTO(testCollectionAlias) + ) + + expect(createdCollectionId).toBeDefined() + expect(createdCollectionId).toBeGreaterThan(0) + + const notifications: Notification[] = await sut.getAllNotificationsByUser(true) + expect(Array.isArray(notifications)).toBe(true) + expect(notifications.length).toBeGreaterThan(0) + + const createdvNotification = notifications.find( + (n) => n.collectionAlias === testCollectionAlias + ) + + expect(createdvNotification).toBeDefined() + expect(createdvNotification?.type).toBe(NotificationType.CREATEDV) + expect(createdvNotification?.collectionAlias).toBe(testCollectionAlias) + expect(createdvNotification?.sentTimestamp).toBeDefined() + expect(createdvNotification?.displayAsRead).toBe(false) + expect(createdvNotification?.collectionDisplayName).toBe('Test Collection') + expect(createdvNotification?.collectionAlias).toBe(testCollectionAlias) + + await deleteCollectionViaApi(testCollectionAlias) + }) + test('should return array when inAppNotificationFormat is false', async () => { const notifications: Notification[] = await sut.getAllNotificationsByUser(false) @@ -117,7 +151,7 @@ describe('NotificationsRepository', () => { }) test('should return unread count', async () => { - const unreadCount = await sut.getUnreadCount() + const unreadCount = await sut.getUnreadNotificationsCount() expect(typeof unreadCount).toBe('number') expect(unreadCount).toBeGreaterThanOrEqual(0) @@ -130,7 +164,7 @@ describe('NotificationsRepository', () => { const unreadNotification = notifications[0] - await expect(sut.markAsRead(unreadNotification.id)).resolves.toBeUndefined() + await expect(sut.markNotificationAsRead(unreadNotification.id)).resolves.toBeUndefined() const updatedNotifications: Notification[] = await sut.getAllNotificationsByUser() const updatedNotification = updatedNotifications.find((n) => n.id === unreadNotification.id) @@ -145,6 +179,8 @@ describe('NotificationsRepository', () => { `[404] Notification ${nonExistentNotificationId} not found.` ) - await expect(sut.markAsRead(nonExistentNotificationId)).rejects.toThrow(expectedError) + await expect(sut.markNotificationAsRead(nonExistentNotificationId)).rejects.toThrow( + expectedError + ) }) }) diff --git a/test/unit/notifications/GetUnreadCount.test.ts b/test/unit/notifications/GetUnreadCount.test.ts index af049c56..7a54f9e9 100644 --- a/test/unit/notifications/GetUnreadCount.test.ts +++ b/test/unit/notifications/GetUnreadCount.test.ts @@ -1,24 +1,26 @@ -import { GetUnreadCount } from '../../../src/notifications/domain/useCases/GetUnreadCount' +import { GetUnreadNotificationsCount } from '../../../src/notifications/domain/useCases/GetUnreadNotificationsCount' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' import { ReadError } from '../../../src' -describe('GetUnreadCount', () => { +describe('GetUnreadNotificationsCount', () => { test('should return unread count from repository', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.getUnreadCount = jest.fn().mockResolvedValue(5) - const sut = new GetUnreadCount(notificationsRepositoryStub) + notificationsRepositoryStub.getUnreadNotificationsCount = jest.fn().mockResolvedValue(5) + const sut = new GetUnreadNotificationsCount(notificationsRepositoryStub) const result = await sut.execute() - expect(notificationsRepositoryStub.getUnreadCount).toHaveBeenCalledWith() + expect(notificationsRepositoryStub.getUnreadNotificationsCount).toHaveBeenCalledWith() expect(result).toBe(5) }) test('should throw error when repository throws error', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.getUnreadCount = jest.fn().mockRejectedValue(new ReadError()) - const sut = new GetUnreadCount(notificationsRepositoryStub) + notificationsRepositoryStub.getUnreadNotificationsCount = jest + .fn() + .mockRejectedValue(new ReadError()) + const sut = new GetUnreadNotificationsCount(notificationsRepositoryStub) await expect(sut.execute()).rejects.toThrow(ReadError) }) diff --git a/test/unit/notifications/MarkAsRead.test.ts b/test/unit/notifications/MarkAsRead.test.ts index 41616a11..a1f57a59 100644 --- a/test/unit/notifications/MarkAsRead.test.ts +++ b/test/unit/notifications/MarkAsRead.test.ts @@ -1,23 +1,25 @@ -import { MarkAsRead } from '../../../src/notifications/domain/useCases/MarkAsRead' +import { MarkNotificationAsRead } from '../../../src/notifications/domain/useCases/MarkNotificationAsRead' import { INotificationsRepository } from '../../../src/notifications/domain/repositories/INotificationsRepository' import { ReadError } from '../../../src' -describe('MarkAsRead', () => { +describe('MarkNotificationAsRead', () => { test('should mark notification as read in repository', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.markAsRead = jest.fn().mockResolvedValue(undefined) - const sut = new MarkAsRead(notificationsRepositoryStub) + notificationsRepositoryStub.markNotificationAsRead = jest.fn().mockResolvedValue(undefined) + const sut = new MarkNotificationAsRead(notificationsRepositoryStub) await sut.execute(123) - expect(notificationsRepositoryStub.markAsRead).toHaveBeenCalledWith(123) + expect(notificationsRepositoryStub.markNotificationAsRead).toHaveBeenCalledWith(123) }) test('should throw error when repository throws error', async () => { const notificationsRepositoryStub: INotificationsRepository = {} as INotificationsRepository - notificationsRepositoryStub.markAsRead = jest.fn().mockRejectedValue(new ReadError()) - const sut = new MarkAsRead(notificationsRepositoryStub) + notificationsRepositoryStub.markNotificationAsRead = jest + .fn() + .mockRejectedValue(new ReadError()) + const sut = new MarkNotificationAsRead(notificationsRepositoryStub) await expect(sut.execute(123)).rejects.toThrow(ReadError) })