Sfoglia il codice sorgente

feat(syncing-server): add designated survivors in fetching shared vaults response (#844)

Karol Sójko 1 anno fa
parent
commit
bcd95cdbe9

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

@@ -871,6 +871,7 @@ export class ContainerConfigLoader {
       .bind<DesignateSurvivor>(TYPES.Sync_DesignateSurvivor)
       .toConstantValue(
         new DesignateSurvivor(
+          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
           container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
           container.get<TimerInterface>(TYPES.Sync_Timer),
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),

+ 1 - 0
packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts

@@ -8,4 +8,5 @@ export interface SharedVaultUserRepositoryInterface {
   remove(sharedVault: SharedVaultUser): Promise<void>
   removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
   findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
+  findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null>
 }

+ 35 - 1
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<SharedVaultRepositoryInterface>
+    sharedVaultRepository.findByUuid = jest.fn().mockReturnValue(sharedVault)
+    sharedVaultRepository.save = jest.fn()
+
     timer = {} as jest.Mocked<TimerInterface>
     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])
 

+ 14 - 0
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<void> {
   constructor(
+    private sharedVaultRepository: SharedVaultRepositoryInterface,
     private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
     private timer: TimerInterface,
     private domainEventFactory: DomainEventFactoryInterface,
@@ -40,6 +42,11 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
     }
     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<void> {
       }),
     )
 
+    sharedVault.props.timestamps = Timestamps.create(
+      sharedVault.props.timestamps.createdAt,
+      this.timer.getTimestampInMicroseconds(),
+    ).getValue()
+
+    await this.sharedVaultRepository.save(sharedVault)
+
     return Result.ok()
   }
 }

+ 15 - 2
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])
+  })
 })

+ 34 - 5
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<SharedVault[]> {
+export class GetSharedVaults
+  implements
+    UseCaseInterface<{
+      sharedVaults: SharedVault[]
+      designatedSurvivors: SharedVaultUser[]
+    }>
+{
   constructor(
     private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
     private sharedVaultRepository: SharedVaultRepositoryInterface,
   ) {}
 
-  async execute(dto: GetSharedVaultsDTO): Promise<Result<SharedVault[]>> {
+  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<SharedVault[]> {
     )
 
     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,
+    })
   }
 }

+ 1 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaultsDTO.ts

@@ -1,4 +1,5 @@
 export interface GetSharedVaultsDTO {
   userUuid: string
+  includeDesignatedSurvivors?: boolean
   lastSyncTime?: number
 }

+ 1 - 1
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<GetSharedVaults>
-    getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))
+    getSharedVaultsUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sharedVaults: [], designatedSurivors: [] }))
 
     getSharedVaultInvitesSentToUserUseCase = {} as jest.Mocked<GetSharedVaultInvitesSentToUser>
     getSharedVaultInvitesSentToUserUseCase.execute = jest.fn().mockReturnValue(Result.ok([]))

+ 3 - 2
packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts

@@ -73,12 +73,13 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
 
       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<SyncItemsResponse> {
         conflicts: saveItemsResult.conflicts,
         cursorToken: getItemsResult.cursorToken,
         sharedVaultInvites,
-        sharedVaults,
+        sharedVaults: sharedVaultsResult.sharedVaults,
         messages,
         notifications,
       }

+ 11 - 4
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<results.JsonResult> {
-    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,
+      })),
     })
   }
 

+ 18 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts

@@ -10,6 +10,24 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito
     private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
   ) {}
 
+  async findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser | null> {
+    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<void> {
     await this.ormRepository
       .createQueryBuilder('shared_vault_user')