소스 검색

feat(syncing-server): transfer shared vault items (#851)

Karol Sójko 1 년 전
부모
커밋
a8f03e157b

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

@@ -174,6 +174,7 @@ import { RemoveUserFromSharedVaults } from '../Domain/UseCase/SharedVaults/Remov
 import { TransferSharedVault } from '../Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVault'
 import { TransitionRepositoryInterface } from '../Domain/Transition/TransitionRepositoryInterface'
 import { RedisTransitionRepository } from '../Infra/Redis/RedisTransitionRepository'
+import { TransferSharedVaultItems } from '../Domain/UseCase/SharedVaults/TransferSharedVaultItems/TransferSharedVaultItems'
 
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -890,12 +891,22 @@ export class ContainerConfigLoader {
           container.get<Logger>(TYPES.Sync_Logger),
         ),
       )
+    container
+      .bind<TransferSharedVaultItems>(TYPES.Sync_TransferSharedVaultItems)
+      .toConstantValue(
+        new TransferSharedVaultItems(
+          isSecondaryDatabaseEnabled
+            ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
+            : container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
+        ),
+      )
     container
       .bind<TransferSharedVault>(TYPES.Sync_TransferSharedVault)
       .toConstantValue(
         new TransferSharedVault(
           container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
           container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
+          container.get<TransferSharedVaultItems>(TYPES.Sync_TransferSharedVaultItems),
           container.get<TimerInterface>(TYPES.Sync_Timer),
         ),
       )

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

@@ -91,6 +91,7 @@ const TYPES = {
   Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'),
   Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'),
   Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
+  Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
   // Handlers
   Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
   Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),

+ 1 - 0
packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts

@@ -20,4 +20,5 @@ export interface ItemRepositoryInterface {
   markItemsAsDeleted(itemUuids: Array<string>, updatedAtTimestamp: number): Promise<void>
   updateContentSize(itemUuid: string, contentSize: number): Promise<void>
   unassignFromSharedVault(sharedVaultUuid: Uuid): Promise<void>
+  updateSharedVaultOwner(dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void>
 }

+ 24 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVault.spec.ts

@@ -1,10 +1,11 @@
 import { TimerInterface } from '@standardnotes/time'
-import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
 
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { TransferSharedVault } from './TransferSharedVault'
 import { SharedVault } from '../../../SharedVault/SharedVault'
+import { TransferSharedVaultItems } from '../TransferSharedVaultItems/TransferSharedVaultItems'
 
 describe('TransferSharedVault', () => {
   let sharedVault: SharedVault
@@ -12,8 +13,10 @@ describe('TransferSharedVault', () => {
   let sharedVaultRepository: SharedVaultRepositoryInterface
   let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
   let timer: TimerInterface
+  let transferSharedVaultItems: TransferSharedVaultItems
 
-  const createUseCase = () => new TransferSharedVault(sharedVaultRepository, sharedVaultUserRepository, timer)
+  const createUseCase = () =>
+    new TransferSharedVault(sharedVaultRepository, sharedVaultUserRepository, transferSharedVaultItems, timer)
 
   beforeEach(() => {
     sharedVault = SharedVault.create({
@@ -40,6 +43,9 @@ describe('TransferSharedVault', () => {
 
     timer = {} as jest.Mocked<TimerInterface>
     timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
+
+    transferSharedVaultItems = {} as jest.Mocked<TransferSharedVaultItems>
+    transferSharedVaultItems.execute = jest.fn().mockResolvedValue(Result.ok())
   })
 
   it('should transfer shared vault to another user', async () => {
@@ -145,4 +151,20 @@ describe('TransferSharedVault', () => {
     expect(sharedVaultRepository.save).not.toHaveBeenCalled()
     expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
   })
+
+  it('should fail if transfering items fails', async () => {
+    const useCase = createUseCase()
+
+    transferSharedVaultItems.execute = jest.fn().mockResolvedValue(Result.fail('error'))
+
+    const result = await useCase.execute({
+      sharedVaultUid: '00000000-0000-0000-0000-000000000000',
+      fromUserUuid: '00000000-0000-0000-0000-000000000000',
+      toUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(sharedVaultRepository.save).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
 })

+ 11 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVault.ts

@@ -4,11 +4,13 @@ import { TimerInterface } from '@standardnotes/time'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 import { TransferSharedVaultDTO } from './TransferSharedVaultDTO'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { TransferSharedVaultItems } from '../TransferSharedVaultItems/TransferSharedVaultItems'
 
 export class TransferSharedVault implements UseCaseInterface<void> {
   constructor(
     private sharedVaultRepository: SharedVaultRepositoryInterface,
     private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
+    private transferSharedVaultItems: TransferSharedVaultItems,
     private timer: TimerInterface,
   ) {}
 
@@ -48,6 +50,15 @@ export class TransferSharedVault implements UseCaseInterface<void> {
       return Result.fail('New owner is not a member of this shared vault')
     }
 
+    const transferingItemsResult = await this.transferSharedVaultItems.execute({
+      fromUserUuid: fromUserUuid.value,
+      toUserUuid: toUserUuid.value,
+      sharedVaultUuid: sharedVaultUuid.value,
+    })
+    if (transferingItemsResult.isFailed()) {
+      return Result.fail(`Could not transfer items: ${transferingItemsResult.getError()}`)
+    }
+
     newOwner.props.isDesignatedSurvivor = false
     newOwner.props.permission = SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Admin).getValue()
     newOwner.props.timestamps = Timestamps.create(

+ 65 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVaultItems/TransferSharedVaultItems.spec.ts

@@ -0,0 +1,65 @@
+import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
+import { TransferSharedVaultItems } from './TransferSharedVaultItems'
+
+describe('TransferSharedVaultItems', () => {
+  let itemRepository: ItemRepositoryInterface
+
+  const createUseCase = () => new TransferSharedVaultItems(itemRepository)
+
+  beforeEach(() => {
+    itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
+    itemRepository.updateSharedVaultOwner = jest.fn()
+  })
+
+  it('should update shared vault owner', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      fromUserUuid: '0d1d1c7c-5e3e-4b0b-8b4a-8c5b1f8c5b1f',
+      toUserUuid: '0d1d1c7c-5e3e-4b0b-8b4a-8c5b1f8c5b1a',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(false)
+    expect(itemRepository.updateSharedVaultOwner).toHaveBeenCalled()
+  })
+
+  it('should return error when from user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      fromUserUuid: 'invalid',
+      toUserUuid: '0d1d1c7c-5e3e-4b0b-8b4a-8c5b1f8c5b1a',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should return error when to user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      fromUserUuid: '0d1d1c7c-5e3e-4b0b-8b4a-8c5b1f8c5b1f',
+      toUserUuid: 'invalid',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+
+  it('should return error when shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      fromUserUuid: '0d1d1c7c-5e3e-4b0b-8b4a-8c5b1f8c5b1f',
+      toUserUuid: '0d1d1c7c-5e3e-4b0b-8b4a-8c5b1f8c5b1a',
+      sharedVaultUuid: 'invalid',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(result.getError()).toBe('Given value is not a valid uuid: invalid')
+  })
+})

+ 36 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVaultItems/TransferSharedVaultItems.ts

@@ -0,0 +1,36 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+
+import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
+import { TransferSharedVaultItemsDTO } from './TransferSharedVaultItemsDTO'
+
+export class TransferSharedVaultItems implements UseCaseInterface<void> {
+  constructor(private itemRepository: ItemRepositoryInterface) {}
+
+  async execute(dto: TransferSharedVaultItemsDTO): Promise<Result<void>> {
+    const fromUserUuidOrError = Uuid.create(dto.fromUserUuid)
+    if (fromUserUuidOrError.isFailed()) {
+      return Result.fail(fromUserUuidOrError.getError())
+    }
+    const fromUserUuid = fromUserUuidOrError.getValue()
+
+    const toUserUuidOrError = Uuid.create(dto.toUserUuid)
+    if (toUserUuidOrError.isFailed()) {
+      return Result.fail(toUserUuidOrError.getError())
+    }
+    const toUserUuid = toUserUuidOrError.getValue()
+
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    await this.itemRepository.updateSharedVaultOwner({
+      sharedVaultUuid,
+      fromOwnerUuid: fromUserUuid,
+      toOwnerUuid: toUserUuid,
+    })
+
+    return Result.ok()
+  }
+}

+ 5 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVaultItems/TransferSharedVaultItemsDTO.ts

@@ -0,0 +1,5 @@
+export interface TransferSharedVaultItemsDTO {
+  sharedVaultUuid: string
+  fromUserUuid: string
+  toUserUuid: string
+}

+ 9 - 0
packages/syncing-server/src/Infra/TypeORM/MongoDBItemRepository.ts

@@ -17,6 +17,15 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
     private logger: Logger,
   ) {}
 
+  async updateSharedVaultOwner(dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
+    await this.mongoRepository.updateMany(
+      {
+        $and: [{ sharedVaultUuid: { $eq: dto.sharedVaultUuid.value } }, { userUuid: { $eq: dto.fromOwnerUuid.value } }],
+      },
+      { $set: { userUuid: dto.toOwnerUuid.value } },
+    )
+  }
+
   async unassignFromSharedVault(sharedVaultUuid: Uuid): Promise<void> {
     await this.mongoRepository.updateMany(
       { sharedVaultUuid: { $eq: sharedVaultUuid.value } },

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

@@ -16,6 +16,24 @@ export class SQLItemRepository extends SQLLegacyItemRepository {
     super(ormRepository, mapper, logger)
   }
 
+  override async updateSharedVaultOwner(dto: {
+    sharedVaultUuid: Uuid
+    fromOwnerUuid: Uuid
+    toOwnerUuid: Uuid
+  }): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder('item')
+      .update()
+      .set({
+        userUuid: dto.toOwnerUuid.value,
+      })
+      .where('shared_vault_uuid = :sharedVaultUuid AND user_uuid = :fromOwnerUuid', {
+        sharedVaultUuid: dto.sharedVaultUuid.value,
+        fromOwnerUuid: dto.fromOwnerUuid.value,
+      })
+      .execute()
+  }
+
   override async unassignFromSharedVault(sharedVaultUuid: Uuid): Promise<void> {
     await this.ormRepository
       .createQueryBuilder('item')

+ 4 - 0
packages/syncing-server/src/Infra/TypeORM/SQLLegacyItemRepository.ts

@@ -16,6 +16,10 @@ export class SQLLegacyItemRepository implements ItemRepositoryInterface {
     protected logger: Logger,
   ) {}
 
+  async updateSharedVaultOwner(_dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
+    this.logger.error('Method updateSharedVaultOwner not supported.')
+  }
+
   async unassignFromSharedVault(_sharedVaultUuid: Uuid): Promise<void> {
     this.logger.error('Method unassignFromSharedVault not supported.')
   }