Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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_.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './collections'
export * from './metadataBlocks'
export * from './files'
export * from './contactInfo'
export * from './notifications'
7 changes: 7 additions & 0 deletions src/notifications/domain/models/Notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Notification {
id: number
type: string
subjectText: string
messageText: string
sentTimestamp: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Notification } from '../models/Notification'

export interface INotificationsRepository {
getAllNotificationsByUser(): Promise<Notification[]>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A pagination feature would be nice:)

deleteNotificationByUser(notificationId: number): Promise<void>
}
16 changes: 16 additions & 0 deletions src/notifications/domain/useCases/DeleteNotificationByUser.ts
Original file line number Diff line number Diff line change
@@ -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<void>} - A promise that resolves when the notification is deleted.
*/
export class DeleteNotificationByUser implements UseCase<void> {
constructor(private readonly notificationsRepository: INotificationsRepository) {}

async execute(notificationId: number): Promise<void> {
return this.notificationsRepository.deleteNotificationByUser(notificationId)
}
}
16 changes: 16 additions & 0 deletions src/notifications/domain/useCases/GetAllNotificationsByUser.ts
Original file line number Diff line number Diff line change
@@ -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<Notification[]> {
constructor(private readonly notificationsRepository: INotificationsRepository) {}

/**
* Use case for retrieving all notifications for the current user.
*
* @returns {Promise<Notification[]>} - A promise that resolves to an array of Notification instances.
*/
async execute(): Promise<Notification[]> {
return (await this.notificationsRepository.getAllNotificationsByUser()) as Notification[]
}
}
12 changes: 12 additions & 0 deletions src/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -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'
25 changes: 25 additions & 0 deletions src/notifications/infra/repositories/NotificationsRepository.ts
Original file line number Diff line number Diff line change
@@ -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<Notification[]> {
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<void> {
return this.doDelete(
this.buildApiEndpoint(this.notificationsResourceName, notificationId.toString())
)
.then(() => {})
.catch((error) => {
throw error
})
}
}
32 changes: 32 additions & 0 deletions test/functional/notifications/DeleteNotificationByUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 notifications = await getAllNotificationsByUser.execute()
const notificationId = notifications[notifications.length - 1].id

await deleteNotificationByUser.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)
})
})
28 changes: 28 additions & 0 deletions test/functional/notifications/GetAllNotificationsByUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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()

expect(notifications[0]).toHaveProperty('id')
expect(notifications[0]).toHaveProperty('type')
expect(notifications[0]).toHaveProperty('sentTimestamp')
})
})
78 changes: 78 additions & 0 deletions test/integration/notifications/NotificationsRepository.test.ts
Original file line number Diff line number Diff line change
@@ -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).toBeGreaterThan(0)

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
)
})
})
46 changes: 46 additions & 0 deletions test/unit/notifications/DeleteNotificationByUser.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
44 changes: 44 additions & 0 deletions test/unit/notifications/GetAllNotificationsByUser.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})