diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index e2b47a28a..127e2d97d 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -871,6 +871,7 @@ export class ContainerConfigLoader { .bind(TYPES.Sync_DesignateSurvivor) .toConstantValue( new DesignateSurvivor( + container.get(TYPES.Sync_SharedVaultRepository), container.get(TYPES.Sync_SharedVaultUserRepository), container.get(TYPES.Sync_Timer), container.get(TYPES.Sync_DomainEventFactory), diff --git a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts b/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts index 4428c50b0..e500a5bfe 100644 --- a/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts @@ -8,4 +8,5 @@ export interface SharedVaultUserRepositoryInterface { remove(sharedVault: SharedVaultUser): Promise removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise + findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise } diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.spec.ts index 8842844b5..08222b9b8 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.spec.ts @@ -5,8 +5,12 @@ import { DesignateSurvivor } from './DesignateSurvivor' import { TimerInterface } from '@standardnotes/time' import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events' import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' +import { SharedVault } from '../../../SharedVault/SharedVault' +import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' describe('DesignateSurvivor', () => { + let sharedVault: SharedVault + let sharedVaultRepository: SharedVaultRepositoryInterface let sharedVaultUserRepository: SharedVaultUserRepositoryInterface let sharedVaultUser: SharedVaultUser let sharedVaultOwner: SharedVaultUser @@ -15,9 +19,25 @@ describe('DesignateSurvivor', () => { let domainEventPublisher: DomainEventPublisherInterface const createUseCase = () => - new DesignateSurvivor(sharedVaultUserRepository, timer, domainEventFactory, domainEventPublisher) + new DesignateSurvivor( + sharedVaultRepository, + sharedVaultUserRepository, + timer, + domainEventFactory, + domainEventPublisher, + ) beforeEach(() => { + sharedVault = SharedVault.create({ + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + fileUploadBytesUsed: 123, + }).getValue() + + sharedVaultRepository = {} as jest.Mocked + sharedVaultRepository.findByUuid = jest.fn().mockReturnValue(sharedVault) + sharedVaultRepository.save = jest.fn() + timer = {} as jest.Mocked timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123) @@ -86,6 +106,20 @@ describe('DesignateSurvivor', () => { expect(result.isFailed()).toBe(true) }) + it('should fail if shared vault is not found', async () => { + sharedVaultRepository.findByUuid = jest.fn().mockReturnValue(null) + + const useCase = createUseCase() + + const result = await useCase.execute({ + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + userUuid: '00000000-0000-0000-0000-000000000000', + originatorUuid: '00000000-0000-0000-0000-000000000002', + }) + + expect(result.isFailed()).toBe(true) + }) + it('should fail if shared vault user is not found', async () => { sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner]) diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.ts index 6a96f853a..d9cb1a190 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.ts @@ -12,9 +12,11 @@ import { import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { DesignateSurvivorDTO } from './DesignateSurvivorDTO' import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface' +import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' export class DesignateSurvivor implements UseCaseInterface { constructor( + private sharedVaultRepository: SharedVaultRepositoryInterface, private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, private timer: TimerInterface, private domainEventFactory: DomainEventFactoryInterface, @@ -40,6 +42,11 @@ export class DesignateSurvivor implements UseCaseInterface { } const originatorUuid = originatorUuidOrError.getValue() + const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid) + if (!sharedVault) { + return Result.fail('Shared vault not found') + } + const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid) let sharedVaultExistingSurvivor: SharedVaultUser | undefined let toBeDesignatedAsASurvivor: SharedVaultUser | undefined @@ -92,6 +99,13 @@ export class DesignateSurvivor implements UseCaseInterface { }), ) + sharedVault.props.timestamps = Timestamps.create( + sharedVault.props.timestamps.createdAt, + this.timer.getTimestampInMicroseconds(), + ).getValue() + + await this.sharedVaultRepository.save(sharedVault) + return Result.ok() } } diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts index 18cb4be05..1755e4da4 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts @@ -40,7 +40,7 @@ describe('GetSharedVaults', () => { userUuid: '00000000-0000-0000-0000-000000000000', }) - expect(result.getValue()).toEqual([sharedVault]) + expect(result.getValue().sharedVaults).toEqual([sharedVault]) }) it('returns empty array if no shared vaults found', async () => { @@ -52,7 +52,7 @@ describe('GetSharedVaults', () => { userUuid: '00000000-0000-0000-0000-000000000000', }) - expect(result.getValue()).toEqual([]) + expect(result.getValue().sharedVaults).toEqual([]) }) it('returns error if user uuid is invalid', async () => { @@ -64,4 +64,17 @@ describe('GetSharedVaults', () => { expect(result.isFailed()).toBeTruthy() }) + + it('should fetch designated survivors if includeDesignatedSurvivors is true', async () => { + sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + includeDesignatedSurvivors: true, + }) + + expect(result.getValue().designatedSurvivors).toEqual([sharedVaultUser]) + }) }) diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.ts index 4f30924a4..d1a37a9fd 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.ts @@ -1,17 +1,28 @@ -import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { Result, SharedVaultUser, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { SharedVault } from '../../../SharedVault/SharedVault' import { GetSharedVaultsDTO } from './GetSharedVaultsDTO' import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface' -export class GetSharedVaults implements UseCaseInterface { +export class GetSharedVaults + implements + UseCaseInterface<{ + sharedVaults: SharedVault[] + designatedSurvivors: SharedVaultUser[] + }> +{ constructor( private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, private sharedVaultRepository: SharedVaultRepositoryInterface, ) {} - async execute(dto: GetSharedVaultsDTO): Promise> { + async execute(dto: GetSharedVaultsDTO): Promise< + Result<{ + sharedVaults: SharedVault[] + designatedSurvivors: SharedVaultUser[] + }> + > { const userUuidOrError = Uuid.create(dto.userUuid) if (userUuidOrError.isFailed()) { return Result.fail(userUuidOrError.getError()) @@ -25,11 +36,29 @@ export class GetSharedVaults implements UseCaseInterface { ) if (sharedVaultUuids.length === 0) { - return Result.ok([]) + return Result.ok({ + sharedVaults: [], + designatedSurvivors: [], + }) } const sharedVaults = await this.sharedVaultRepository.findByUuids(sharedVaultUuids, dto.lastSyncTime) - return Result.ok(sharedVaults) + const designatedSurvivors = [] + if (dto.includeDesignatedSurvivors) { + for (const sharedVault of sharedVaults) { + const designatedSurvivor = await this.sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid( + sharedVault.uuid, + ) + if (designatedSurvivor) { + designatedSurvivors.push(designatedSurvivor) + } + } + } + + return Result.ok({ + sharedVaults, + designatedSurvivors, + }) } } diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaultsDTO.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaultsDTO.ts index 78c61f1ca..0a6935bec 100644 --- a/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaultsDTO.ts +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaultsDTO.ts @@ -1,4 +1,5 @@ export interface GetSharedVaultsDTO { userUuid: string + includeDesignatedSurvivors?: boolean lastSyncTime?: number } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts index 014ebb806..66a70df7c 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts @@ -136,7 +136,7 @@ describe('SyncItems', () => { itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository) getSharedVaultsUseCase = {} as jest.Mocked - getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([])) + getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sharedVaults: [], designatedSurivors: [] })) getSharedVaultInvitesSentToUserUseCase = {} as jest.Mocked getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.ok([])) diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts index 7c302768f..e9c51deee 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts @@ -73,12 +73,13 @@ export class SyncItems implements UseCaseInterface { const sharedVaultsOrError = await this.getSharedVaultsUseCase.execute({ userUuid: dto.userUuid, + includeDesignatedSurvivors: false, lastSyncTime: getItemsResult.lastSyncTime ?? undefined, }) if (sharedVaultsOrError.isFailed()) { return Result.fail(sharedVaultsOrError.getError()) } - const sharedVaults = sharedVaultsOrError.getValue() + const sharedVaultsResult = sharedVaultsOrError.getValue() const sharedVaultInvitesOrError = await this.getSharedVaultInvitesSentToUserUseCase.execute({ userUuid: dto.userUuid, @@ -114,7 +115,7 @@ export class SyncItems implements UseCaseInterface { conflicts: saveItemsResult.conflicts, cursorToken: getItemsResult.cursorToken, sharedVaultInvites, - sharedVaults, + sharedVaults: sharedVaultsResult.sharedVaults, messages, notifications, } diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts index ad0f6e26d..e6cb79885 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts @@ -36,23 +36,30 @@ export class BaseSharedVaultsController extends BaseHttpController { } async getSharedVaults(_request: Request, response: Response): Promise { - const result = await this.getSharedVaultsUseCase.execute({ + const resultOrError = await this.getSharedVaultsUseCase.execute({ userUuid: response.locals.user.uuid, + includeDesignatedSurvivors: true, }) - if (result.isFailed()) { + if (resultOrError.isFailed()) { return this.json( { error: { - message: result.getError(), + message: resultOrError.getError(), }, }, HttpStatusCode.BadRequest, ) } + const result = resultOrError.getValue() + return this.json({ - sharedVaults: result.getValue().map((sharedVault) => this.sharedVaultHttpMapper.toProjection(sharedVault)), + sharedVaults: result.sharedVaults.map((sharedVault) => this.sharedVaultHttpMapper.toProjection(sharedVault)), + designatedSurvivors: result.designatedSurvivors.map((designatedSurvivor) => ({ + sharedVaultUuid: designatedSurvivor.props.sharedVaultUuid.value, + userUuid: designatedSurvivor.props.userUuid.value, + })), }) } diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts index 89bd49eb5..2eff51cad 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts @@ -10,6 +10,24 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito private mapper: MapperInterface, ) {} + async findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise { + const persistence = await this.ormRepository + .createQueryBuilder('shared_vault_user') + .where('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', { + sharedVaultUuid: sharedVaultUuid.value, + }) + .andWhere('shared_vault_user.is_designated_survivor = :isDesignatedSurvivor', { + isDesignatedSurvivor: true, + }) + .getOne() + + if (persistence === null) { + return null + } + + return this.mapper.toDomain(persistence) + } + async removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise { await this.ormRepository .createQueryBuilder('shared_vault_user')