diff --git a/packages/domain-core/src/Domain/Notification/NotificationType.ts b/packages/domain-core/src/Domain/Notification/NotificationType.ts index ced21df18..5b02db1c1 100644 --- a/packages/domain-core/src/Domain/Notification/NotificationType.ts +++ b/packages/domain-core/src/Domain/Notification/NotificationType.ts @@ -6,6 +6,8 @@ export class NotificationType extends ValueObject { static readonly TYPES = { SharedVaultItemRemoved: 'shared_vault_item_removed', RemovedFromSharedVault: 'removed_from_shared_vault', + SharedVaultFileUploaded: 'shared_vault_file_uploaded', + SharedVaultFileRemoved: 'shared_vault_file_removed', } get value(): string { diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 4dcc221a7..2d1f98313 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -157,6 +157,7 @@ import { SharedVaultSnjsFilter } from '../Domain/Item/SaveRule/SharedVaultSnjsFi import { UpdateStorageQuotaUsedInSharedVault } from '../Domain/UseCase/SharedVaults/UpdateStorageQuotaUsedInSharedVault/UpdateStorageQuotaUsedInSharedVault' import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler' import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler' +import { AddNotificationsForUsers } from '../Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers' export class ContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -562,6 +563,14 @@ export class ContainerConfigLoader { .toConstantValue( new AddNotificationForUser(container.get(TYPES.Sync_NotificationRepository), container.get(TYPES.Sync_Timer)), ) + container + .bind(TYPES.Sync_AddNotificationsForUsers) + .toConstantValue( + new AddNotificationsForUsers( + container.get(TYPES.Sync_SharedVaultUserRepository), + container.get(TYPES.Sync_AddNotificationForUser), + ), + ) container .bind(TYPES.Sync_RemoveNotificationsForUser) .toConstantValue(new RemoveNotificationsForUser(container.get(TYPES.Sync_NotificationRepository))) @@ -831,6 +840,7 @@ export class ContainerConfigLoader { .toConstantValue( new SharedVaultFileUploadedEventHandler( container.get(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault), + container.get(TYPES.Sync_AddNotificationsForUsers), container.get(TYPES.Sync_Logger), ), ) @@ -839,6 +849,7 @@ export class ContainerConfigLoader { .toConstantValue( new SharedVaultFileRemovedEventHandler( container.get(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault), + container.get(TYPES.Sync_AddNotificationsForUsers), container.get(TYPES.Sync_Logger), ), ) diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index 1928aa171..03c3dcb1a 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -79,6 +79,7 @@ const TYPES = { Sync_GetUserNotifications: Symbol.for('Sync_GetUserNotifications'), Sync_DetermineSharedVaultOperationOnItem: Symbol.for('Sync_DetermineSharedVaultOperationOnItem'), Sync_UpdateStorageQuotaUsedInSharedVault: Symbol.for('Sync_UpdateStorageQuotaUsedInSharedVault'), + Sync_AddNotificationsForUsers: Symbol.for('Sync_AddNotificationsForUsers'), // Handlers Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'), Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'), diff --git a/packages/syncing-server/src/Domain/Handler/SharedVaultFileRemovedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/SharedVaultFileRemovedEventHandler.ts index a5e997d00..f456c2ed3 100644 --- a/packages/syncing-server/src/Domain/Handler/SharedVaultFileRemovedEventHandler.ts +++ b/packages/syncing-server/src/Domain/Handler/SharedVaultFileRemovedEventHandler.ts @@ -1,15 +1,26 @@ import { DomainEventHandlerInterface, SharedVaultFileRemovedEvent } from '@standardnotes/domain-events' +import { NotificationPayload, NotificationType, Uuid } from '@standardnotes/domain-core' import { Logger } from 'winston' import { UpdateStorageQuotaUsedInSharedVault } from '../UseCase/SharedVaults/UpdateStorageQuotaUsedInSharedVault/UpdateStorageQuotaUsedInSharedVault' +import { AddNotificationsForUsers } from '../UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers' export class SharedVaultFileRemovedEventHandler implements DomainEventHandlerInterface { constructor( private updateStorageQuotaUsedInSharedVaultUseCase: UpdateStorageQuotaUsedInSharedVault, + private addNotificationsForUsers: AddNotificationsForUsers, private logger: Logger, ) {} async handle(event: SharedVaultFileRemovedEvent): Promise { + const sharedVaultUuidOrError = Uuid.create(event.payload.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + this.logger.error(sharedVaultUuidOrError.getError()) + + return + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + const result = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({ sharedVaultUuid: event.payload.sharedVaultUuid, bytesUsed: -event.payload.fileByteSize, @@ -17,6 +28,24 @@ export class SharedVaultFileRemovedEventHandler implements DomainEventHandlerInt if (result.isFailed()) { this.logger.error(`Failed to update storage quota used in shared vault: ${result.getError()}`) + + return + } + + const notificationPayload = NotificationPayload.create({ + sharedVaultUuid, + type: NotificationType.create(NotificationType.TYPES.SharedVaultFileRemoved).getValue(), + version: '1.0', + }).getValue() + + const notificationResult = await this.addNotificationsForUsers.execute({ + sharedVaultUuid: event.payload.sharedVaultUuid, + type: NotificationType.TYPES.SharedVaultFileRemoved, + payload: notificationPayload, + version: '1.0', + }) + if (notificationResult.isFailed()) { + this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`) } } } diff --git a/packages/syncing-server/src/Domain/Handler/SharedVaultFileUploadedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/SharedVaultFileUploadedEventHandler.ts index a7e1a08ac..2cf224492 100644 --- a/packages/syncing-server/src/Domain/Handler/SharedVaultFileUploadedEventHandler.ts +++ b/packages/syncing-server/src/Domain/Handler/SharedVaultFileUploadedEventHandler.ts @@ -1,15 +1,26 @@ import { DomainEventHandlerInterface, SharedVaultFileUploadedEvent } from '@standardnotes/domain-events' +import { NotificationPayload, NotificationType, Uuid } from '@standardnotes/domain-core' import { Logger } from 'winston' import { UpdateStorageQuotaUsedInSharedVault } from '../UseCase/SharedVaults/UpdateStorageQuotaUsedInSharedVault/UpdateStorageQuotaUsedInSharedVault' +import { AddNotificationsForUsers } from '../UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers' export class SharedVaultFileUploadedEventHandler implements DomainEventHandlerInterface { constructor( private updateStorageQuotaUsedInSharedVaultUseCase: UpdateStorageQuotaUsedInSharedVault, + private addNotificationsForUsers: AddNotificationsForUsers, private logger: Logger, ) {} async handle(event: SharedVaultFileUploadedEvent): Promise { + const sharedVaultUuidOrError = Uuid.create(event.payload.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + this.logger.error(sharedVaultUuidOrError.getError()) + + return + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + const result = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({ sharedVaultUuid: event.payload.sharedVaultUuid, bytesUsed: event.payload.fileByteSize, @@ -17,6 +28,24 @@ export class SharedVaultFileUploadedEventHandler implements DomainEventHandlerIn if (result.isFailed()) { this.logger.error(`Failed to update storage quota used in shared vault: ${result.getError()}`) + + return + } + + const notificationPayload = NotificationPayload.create({ + sharedVaultUuid, + type: NotificationType.create(NotificationType.TYPES.SharedVaultFileUploaded).getValue(), + version: '1.0', + }).getValue() + + const notificationResult = await this.addNotificationsForUsers.execute({ + sharedVaultUuid: event.payload.sharedVaultUuid, + type: NotificationType.TYPES.SharedVaultFileUploaded, + payload: notificationPayload, + version: '1.0', + }) + if (notificationResult.isFailed()) { + this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`) } } } diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts new file mode 100644 index 000000000..57f0a6c11 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts @@ -0,0 +1,83 @@ +import { + SharedVaultUserPermission, + Uuid, + Timestamps, + Result, + NotificationPayload, + NotificationType, +} from '@standardnotes/domain-core' +import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser' +import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' +import { AddNotificationForUser } from '../AddNotificationForUser/AddNotificationForUser' +import { AddNotificationsForUsers } from './AddNotificationsForUsers' + +describe('AddNotificationsForUsers', () => { + let sharedVaultUserRepository: SharedVaultUserRepositoryInterface + let addNotificationForUser: AddNotificationForUser + let sharedVaultUser: SharedVaultUser + let payload: NotificationPayload + + const createUseCase = () => new AddNotificationsForUsers(sharedVaultUserRepository, addNotificationForUser) + + beforeEach(() => { + sharedVaultUser = SharedVaultUser.create({ + permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(), + sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + sharedVaultUserRepository = {} as jest.Mocked + sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockResolvedValue([sharedVaultUser]) + + addNotificationForUser = {} as jest.Mocked + addNotificationForUser.execute = jest.fn().mockResolvedValue(Result.ok()) + + payload = NotificationPayload.create({ + sharedVaultUuid: Uuid.create('0e8c3c7e-3f1a-4f7a-9b5a-5b2b0a7d4b1e').getValue(), + type: NotificationType.create(NotificationType.TYPES.SharedVaultFileUploaded).getValue(), + version: '1.0', + }).getValue() + }) + + it('should add notifications for all users in a shared vault', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + type: 'test', + payload, + version: '1.0', + }) + + expect(result.isFailed()).toBeFalsy() + expect(addNotificationForUser.execute).toHaveBeenCalledTimes(1) + }) + + it('should return error if shared vault uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + sharedVaultUuid: 'invalid', + type: 'test', + payload, + version: '1.0', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should return error if adding notification fails', async () => { + const useCase = createUseCase() + addNotificationForUser.execute = jest.fn().mockResolvedValue(Result.fail('test')) + + const result = await useCase.execute({ + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + type: 'test', + payload, + version: '1.0', + }) + + expect(result.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.ts new file mode 100644 index 000000000..7b00fab29 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.ts @@ -0,0 +1,35 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { AddNotificationsForUsersDTO } from './AddNotificationsForUsersDTO' +import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' +import { AddNotificationForUser } from '../AddNotificationForUser/AddNotificationForUser' + +export class AddNotificationsForUsers implements UseCaseInterface { + constructor( + private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, + private addNotificationForUser: AddNotificationForUser, + ) {} + + async execute(dto: AddNotificationsForUsersDTO): Promise> { + const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + return Result.fail(sharedVaultUuidOrError.getError()) + } + const sharedVaultUuid = sharedVaultUuidOrError.getValue() + + const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid) + for (const sharedVaultUser of sharedVaultUsers) { + const result = await this.addNotificationForUser.execute({ + userUuid: sharedVaultUser.id.toString(), + type: dto.type, + payload: dto.payload, + version: dto.version, + }) + if (result.isFailed()) { + return Result.fail(result.getError()) + } + } + + return Result.ok() + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsersDTO.ts b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsersDTO.ts new file mode 100644 index 000000000..54a200d54 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsersDTO.ts @@ -0,0 +1,8 @@ +import { NotificationPayload } from '@standardnotes/domain-core' + +export interface AddNotificationsForUsersDTO { + sharedVaultUuid: string + version: string + type: string + payload: NotificationPayload +}