diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 8140429a6..d33e27e9d 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/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(TYPES.Sync_Logger), ), ) + container + .bind(TYPES.Sync_SendEventToClients) + .toConstantValue( + new SendEventToClients( + container.get(TYPES.Sync_SharedVaultUserRepository), + container.get(TYPES.Sync_SendEventToClient), + container.get(TYPES.Sync_Logger), + ), + ) container .bind(TYPES.Sync_AddNotificationForUser) .toConstantValue( @@ -607,6 +617,7 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_SaveNewItem), container.get(TYPES.Sync_UpdateExistingItem), container.get(TYPES.Sync_SendEventToClient), + container.get(TYPES.Sync_SendEventToClients), container.get(TYPES.Sync_DomainEventFactory), container.get(TYPES.Sync_Logger), ), diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index cef5463be..846eb186e 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/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'), diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.spec.ts index e434f5079..038d278e9 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.spec.ts +++ b/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.execute = jest.fn().mockReturnValue(Result.ok()) + sendEventToClients = {} as jest.Mocked + sendEventToClients.execute = jest.fn().mockReturnValue(Result.ok()) + domainEventFactory = {} as jest.Mocked 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 () => { diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.ts index e1175e1e5..bb13d1e0c 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.ts +++ b/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 { private readonly SYNC_TOKEN_VERSION = 2 @@ -24,6 +25,7 @@ export class SaveItems implements UseCaseInterface { 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 { }) /* 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() + 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, + }) + } } } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClients.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClients.spec.ts new file mode 100644 index 000000000..6062cdb64 --- /dev/null +++ b/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 + sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultUser]) + + sendEventToClient = {} as jest.Mocked + sendEventToClient.execute = jest.fn().mockReturnValue(Result.ok()) + + logger = {} as jest.Mocked + 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, + 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, + 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, + 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, + 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, + originatingUserUuid: '00000000-0000-0000-0000-000000000003', + }) + + expect(result.isFailed()).toBeFalsy() + expect(logger.error).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClients.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClients.ts new file mode 100644 index 000000000..263f39c1e --- /dev/null +++ b/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 { + constructor( + private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, + private sendEventToClient: SendEventToClient, + private logger: Logger, + ) {} + + async execute(dto: SendEventToClientsDTO): Promise> { + 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() + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClientsDTO.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SendEventToClients/SendEventToClientsDTO.ts new file mode 100644 index 000000000..32551a119 --- /dev/null +++ b/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 +}