feat(syncing-server): send websocket event to shared vault members upon items change in shared vault (#961)
This commit is contained in:
parent
d15d51eae6
commit
6dbb87708f
7 changed files with 256 additions and 1 deletions
|
@ -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),
|
||||
),
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEventInterface } from '@standardnotes/domain-events'
|
||||
|
||||
export interface SendEventToClientsDTO {
|
||||
sharedVaultUuid: string
|
||||
event: DomainEventInterface
|
||||
originatingUserUuid: string
|
||||
}
|
Loading…
Reference in a new issue