浏览代码

feat(syncing-server): send websocket event to shared vault members upon items change in shared vault (#961)

Karol Sójko 1 年之前
父节点
当前提交
6dbb87708f

+ 11 - 0
packages/syncing-server/src/Bootstrap/Container.ts

@@ -161,6 +161,7 @@ import { SyncResponse20200115 } from '../Domain/Item/SyncResponse/SyncResponse20
 import { SyncResponse } from '@standardnotes/grpc'
 import { SyncResponseGRPCMapper } from '../Mapping/gRPC/SyncResponseGRPCMapper'
 import { AccountDeletionVerificationRequestedEventHandler } from '../Domain/Handler/AccountDeletionVerificationRequestedEventHandler'
+import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients/SendEventToClients'
 
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -561,6 +562,15 @@ export class ContainerConfigLoader {
           container.get<Logger>(TYPES.Sync_Logger),
         ),
       )
+    container
+      .bind<SendEventToClients>(TYPES.Sync_SendEventToClients)
+      .toConstantValue(
+        new SendEventToClients(
+          container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
+          container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
+          container.get<Logger>(TYPES.Sync_Logger),
+        ),
+      )
     container
       .bind<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser)
       .toConstantValue(
@@ -607,6 +617,7 @@ export class ContainerConfigLoader {
           container.get<SaveNewItem>(TYPES.Sync_SaveNewItem),
           container.get<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem),
           container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
+          container.get<SendEventToClients>(TYPES.Sync_SendEventToClients),
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
           container.get<Logger>(TYPES.Sync_Logger),
         ),

+ 1 - 0
packages/syncing-server/src/Bootstrap/Types.ts

@@ -77,6 +77,7 @@ const TYPES = {
   Sync_UpdateStorageQuotaUsedInSharedVault: Symbol.for('Sync_UpdateStorageQuotaUsedInSharedVault'),
   Sync_AddNotificationsForUsers: Symbol.for('Sync_AddNotificationsForUsers'),
   Sync_SendEventToClient: Symbol.for('Sync_SendEventToClient'),
+  Sync_SendEventToClients: Symbol.for('Sync_SendEventToClients'),
   Sync_RemoveItemsFromSharedVault: Symbol.for('Sync_RemoveItemsFromSharedVault'),
   Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'),
   Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'),

+ 52 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.spec.ts

@@ -11,6 +11,8 @@ import { Item } from '../../../Item/Item'
 import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 import { ItemsChangedOnServerEvent } from '@standardnotes/domain-events'
+import { SendEventToClients } from '../SendEventToClients/SendEventToClients'
+import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
 
 describe('SaveItems', () => {
   let itemSaveValidator: ItemSaveValidatorInterface
@@ -22,6 +24,7 @@ describe('SaveItems', () => {
   let itemHash1: ItemHash
   let savedItem: Item
   let sendEventToClient: SendEventToClient
+  let sendEventToClients: SendEventToClients
   let domainEventFactory: DomainEventFactoryInterface
 
   const createUseCase = () =>
@@ -32,6 +35,7 @@ describe('SaveItems', () => {
       saveNewItem,
       updateExistingItem,
       sendEventToClient,
+      sendEventToClients,
       domainEventFactory,
       logger,
     )
@@ -40,6 +44,9 @@ describe('SaveItems', () => {
     sendEventToClient = {} as jest.Mocked<SendEventToClient>
     sendEventToClient.execute = jest.fn().mockReturnValue(Result.ok())
 
+    sendEventToClients = {} as jest.Mocked<SendEventToClients>
+    sendEventToClients.execute = jest.fn().mockReturnValue(Result.ok())
+
     domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
     domainEventFactory.createItemsChangedOnServerEvent = jest
       .fn()
@@ -243,6 +250,51 @@ describe('SaveItems', () => {
       performingUserUuid: '00000000-0000-0000-0000-000000000000',
     })
     expect(sendEventToClient.execute).toHaveBeenCalled()
+    expect(sendEventToClients.execute).not.toHaveBeenCalled()
+  })
+
+  it('should update existing shared vault items', async () => {
+    savedItem = Item.create({
+      duplicateOf: null,
+      itemsKeyId: 'items-key-id',
+      content: 'content',
+      contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+      encItemKey: 'enc-item-key',
+      authHash: 'auth-hash',
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      deleted: false,
+      updatedWithSession: null,
+      sharedVaultAssociation: SharedVaultAssociation.create({
+        sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
+        lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      }).getValue(),
+      dates: Dates.create(new Date(123), new Date(123)).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    const useCase = createUseCase()
+
+    itemRepository.findByUuid = jest.fn().mockResolvedValue(savedItem)
+    updateExistingItem.execute = jest.fn().mockResolvedValue(Result.ok(savedItem))
+
+    const result = await useCase.execute({
+      itemHashes: [itemHash1],
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      apiVersion: '1',
+      readOnlyAccess: false,
+      sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(updateExistingItem.execute).toHaveBeenCalledWith({
+      itemHash: itemHash1,
+      existingItem: savedItem,
+      sessionUuid: 'session-uuid',
+      performingUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+    expect(sendEventToClient.execute).toHaveBeenCalled()
+    expect(sendEventToClients.execute).toHaveBeenCalled()
   })
 
   it('should mark items as conflicts if updating existing item fails', async () => {

+ 27 - 1
packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.ts

@@ -13,6 +13,7 @@ import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+import { SendEventToClients } from '../SendEventToClients/SendEventToClients'
 
 export class SaveItems implements UseCaseInterface<SaveItemsResult> {
   private readonly SYNC_TOKEN_VERSION = 2
@@ -24,6 +25,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
     private saveNewItem: SaveNewItem,
     private updateExistingItem: UpdateExistingItem,
     private sendEventToClient: SendEventToClient,
+    private sendEventToClients: SendEventToClients,
     private domainEventFactory: DomainEventFactoryInterface,
     private logger: Logger,
   ) {}
@@ -167,7 +169,31 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
     })
     /* istanbul ignore next */
     if (result.isFailed()) {
-      this.logger.error(`[${dto.userUuid}] Sending items changed event to client failed. Error: ${result.getError()}`)
+      this.logger.error(`Sending items changed event to client failed. Error: ${result.getError()}`, {
+        userId: dto.userUuid,
+      })
+    }
+
+    const sharedVaultUuidsMap = new Map<string, boolean>()
+    for (const item of savedItems) {
+      if (item.isAssociatedWithASharedVault()) {
+        sharedVaultUuidsMap.set((item.sharedVaultUuid as Uuid).value, true)
+      }
+    }
+    const sharedVaultUuids = Array.from(sharedVaultUuidsMap.keys())
+    for (const sharedVaultUuid of sharedVaultUuids) {
+      const result = await this.sendEventToClients.execute({
+        sharedVaultUuid,
+        event: itemsChangedEvent,
+        originatingUserUuid: dto.userUuid,
+      })
+      /* istanbul ignore next */
+      if (result.isFailed()) {
+        this.logger.error(`Sending items changed event to clients failed. Error: ${result.getError()}`, {
+          userId: dto.userUuid,
+          sharedVaultUuid,
+        })
+      }
     }
   }
 

+ 108 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClients.spec.ts

@@ -0,0 +1,108 @@
+import { Logger } from 'winston'
+import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
+import { SendEventToClients } from './SendEventToClients'
+import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { DomainEventInterface } from '@standardnotes/domain-events'
+
+describe('SendEventToClients', () => {
+  let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
+  let sendEventToClient: SendEventToClient
+  let logger: Logger
+
+  const createUseCase = () => new SendEventToClients(sharedVaultUserRepository, sendEventToClient, logger)
+
+  beforeEach(() => {
+    const sharedVaultUser = SharedVaultUser.create({
+      permission: SharedVaultUserPermission.create('read').getValue(),
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      timestamps: Timestamps.create(123456789, 123456789).getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
+      isDesignatedSurvivor: false,
+    }).getValue()
+
+    sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultUser])
+
+    sendEventToClient = {} as jest.Mocked<SendEventToClient>
+    sendEventToClient.execute = jest.fn().mockReturnValue(Result.ok())
+
+    logger = {} as jest.Mocked<Logger>
+    logger.error = jest.fn()
+  })
+
+  it('should send event to all users', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      event: {
+        type: 'test',
+      } as jest.Mocked<DomainEventInterface>,
+      originatingUserUuid: '00000000-0000-0000-0000-000000000003',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(sendEventToClient.execute).toHaveBeenCalledTimes(1)
+  })
+
+  it('should send event to all users except the originating one', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      event: {
+        type: 'test',
+      } as jest.Mocked<DomainEventInterface>,
+      originatingUserUuid: '00000000-0000-0000-0000-000000000001',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(sendEventToClient.execute).toHaveBeenCalledTimes(0)
+  })
+
+  it('should return error if shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: 'invalid',
+      event: {
+        type: 'test',
+      } as jest.Mocked<DomainEventInterface>,
+      originatingUserUuid: '00000000-0000-0000-0000-000000000001',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if originating user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      event: {
+        type: 'test',
+      } as jest.Mocked<DomainEventInterface>,
+      originatingUserUuid: 'invalid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should log error if sending event to client failed', async () => {
+    sendEventToClient.execute = jest.fn().mockReturnValue(Result.fail('test error'))
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      event: {
+        type: 'test',
+      } as jest.Mocked<DomainEventInterface>,
+      originatingUserUuid: '00000000-0000-0000-0000-000000000003',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(logger.error).toHaveBeenCalledTimes(1)
+  })
+})

+ 50 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClients.ts

@@ -0,0 +1,50 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { Logger } from 'winston'
+
+import { SendEventToClientsDTO } from './SendEventToClientsDTO'
+import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
+import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
+
+export class SendEventToClients implements UseCaseInterface<void> {
+  constructor(
+    private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
+    private sendEventToClient: SendEventToClient,
+    private logger: Logger,
+  ) {}
+
+  async execute(dto: SendEventToClientsDTO): Promise<Result<void>> {
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const originatingUserUuidOrError = Uuid.create(dto.originatingUserUuid)
+    if (originatingUserUuidOrError.isFailed()) {
+      return Result.fail(originatingUserUuidOrError.getError())
+    }
+    const originatingUserUuid = originatingUserUuidOrError.getValue()
+
+    const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
+
+    for (const sharedVaultUser of sharedVaultUsers) {
+      if (originatingUserUuid.equals(sharedVaultUser.props.userUuid)) {
+        continue
+      }
+
+      const result = await this.sendEventToClient.execute({
+        event: dto.event,
+        userUuid: sharedVaultUser.props.userUuid.value,
+      })
+
+      if (result.isFailed()) {
+        this.logger.error(`Failed to send event to client: ${result.getError()}`, {
+          userId: sharedVaultUser.props.userUuid.value,
+          sharedVaultUuid: sharedVaultUuid.value,
+        })
+      }
+    }
+
+    return Result.ok()
+  }
+}

+ 7 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClientsDTO.ts

@@ -0,0 +1,7 @@
+import { DomainEventInterface } from '@standardnotes/domain-events'
+
+export interface SendEventToClientsDTO {
+  sharedVaultUuid: string
+  event: DomainEventInterface
+  originatingUserUuid: string
+}