浏览代码

feat(syncing-server): notify shared vault users upon file uploads or removals (#692)

Karol Sójko 1 年之前
父节点
当前提交
46867c1a4d

+ 2 - 0
packages/domain-core/src/Domain/Notification/NotificationType.ts

@@ -6,6 +6,8 @@ export class NotificationType extends ValueObject<NotificationTypeProps> {
   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 {

+ 11 - 0
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<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers)
+      .toConstantValue(
+        new AddNotificationsForUsers(
+          container.get(TYPES.Sync_SharedVaultUserRepository),
+          container.get(TYPES.Sync_AddNotificationForUser),
+        ),
+      )
     container
       .bind<RemoveNotificationsForUser>(TYPES.Sync_RemoveNotificationsForUser)
       .toConstantValue(new RemoveNotificationsForUser(container.get(TYPES.Sync_NotificationRepository)))
@@ -827,6 +836,7 @@ export class ContainerConfigLoader {
       .toConstantValue(
         new SharedVaultFileUploadedEventHandler(
           container.get(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault),
+          container.get(TYPES.Sync_AddNotificationsForUsers),
           container.get(TYPES.Sync_Logger),
         ),
       )
@@ -835,6 +845,7 @@ export class ContainerConfigLoader {
       .toConstantValue(
         new SharedVaultFileRemovedEventHandler(
           container.get(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault),
+          container.get(TYPES.Sync_AddNotificationsForUsers),
           container.get(TYPES.Sync_Logger),
         ),
       )

+ 1 - 0
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'),

+ 29 - 0
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<void> {
+    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()}`)
     }
   }
 }

+ 29 - 0
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<void> {
+    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()}`)
     }
   }
 }

+ 83 - 0
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<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockResolvedValue([sharedVaultUser])
+
+    addNotificationForUser = {} as jest.Mocked<AddNotificationForUser>
+    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()
+  })
+})

+ 35 - 0
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<void> {
+  constructor(
+    private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
+    private addNotificationForUser: AddNotificationForUser,
+  ) {}
+
+  async execute(dto: AddNotificationsForUsersDTO): Promise<Result<void>> {
+    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()
+  }
+}

+ 8 - 0
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
+}