浏览代码

feat: deleting shared vaults. (#640)

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko 1 年之前
父节点
当前提交
f3161c2712
共有 18 个文件被更改,包括 287 次插入5 次删除
  1. 1 2
      packages/auth/src/Domain/Notifications/Notification.spec.ts
  2. 1 2
      packages/auth/src/Domain/Notifications/NotificationProps.ts
  3. 0 0
      packages/domain-core/src/Domain/Notification/NotificationType.spec.ts
  4. 2 1
      packages/domain-core/src/Domain/Notification/NotificationType.ts
  5. 0 0
      packages/domain-core/src/Domain/Notification/NotificationTypeProps.ts
  6. 3 0
      packages/domain-core/src/Domain/index.ts
  7. 7 0
      packages/domain-events/src/Domain/Event/NotificationRequestedEvent.ts
  8. 5 0
      packages/domain-events/src/Domain/Event/NotificationRequestedEventPayload.ts
  9. 2 0
      packages/domain-events/src/Domain/index.ts
  10. 20 0
      packages/syncing-server/src/Domain/Event/DomainEventFactory.ts
  11. 2 0
      packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts
  12. 1 0
      packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts
  13. 2 0
      packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts
  14. 146 0
      packages/syncing-server/src/Domain/UseCase/DeleteSharedVault/DeleteSharedVault.spec.ts
  15. 62 0
      packages/syncing-server/src/Domain/UseCase/DeleteSharedVault/DeleteSharedVault.ts
  16. 4 0
      packages/syncing-server/src/Domain/UseCase/DeleteSharedVault/DeleteSharedVaultDTO.ts
  17. 9 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts
  18. 20 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts

+ 1 - 2
packages/auth/src/Domain/Notifications/Notification.spec.ts

@@ -1,7 +1,6 @@
-import { Timestamps, Uuid } from '@standardnotes/domain-core'
+import { NotificationType, Timestamps, Uuid } from '@standardnotes/domain-core'
 
 import { Notification } from './Notification'
-import { NotificationType } from './NotificationType'
 
 describe('Notification', () => {
   it('should create an entity', () => {

+ 1 - 2
packages/auth/src/Domain/Notifications/NotificationProps.ts

@@ -1,5 +1,4 @@
-import { Timestamps, Uuid } from '@standardnotes/domain-core'
-import { NotificationType } from './NotificationType'
+import { NotificationType, Timestamps, Uuid } from '@standardnotes/domain-core'
 
 export interface NotificationProps {
   userUuid: Uuid

+ 0 - 0
packages/auth/src/Domain/Notifications/NotificationType.spec.ts → packages/domain-core/src/Domain/Notification/NotificationType.spec.ts


+ 2 - 1
packages/auth/src/Domain/Notifications/NotificationType.ts → packages/domain-core/src/Domain/Notification/NotificationType.ts

@@ -1,4 +1,5 @@
-import { Result, ValueObject } from '@standardnotes/domain-core'
+import { Result } from '../Core/Result'
+import { ValueObject } from '../Core/ValueObject'
 
 import { NotificationTypeProps } from './NotificationTypeProps'
 

+ 0 - 0
packages/auth/src/Domain/Notifications/NotificationTypeProps.ts → packages/domain-core/src/Domain/Notification/NotificationTypeProps.ts


+ 3 - 0
packages/domain-core/src/Domain/index.ts

@@ -43,6 +43,9 @@ export * from './Env/AbstractEnv'
 
 export * from './Mapping/MapperInterface'
 
+export * from './Notification/NotificationType'
+export * from './Notification/NotificationTypeProps'
+
 export * from './Service/ServiceConfiguration'
 export * from './Service/ServiceContainer'
 export * from './Service/ServiceContainerInterface'

+ 7 - 0
packages/domain-events/src/Domain/Event/NotificationRequestedEvent.ts

@@ -0,0 +1,7 @@
+import { DomainEventInterface } from './DomainEventInterface'
+import { NotificationRequestedEventPayload } from './NotificationRequestedEventPayload'
+
+export interface NotificationRequestedEvent extends DomainEventInterface {
+  type: 'NOTIFICATION_REQUESTED'
+  payload: NotificationRequestedEventPayload
+}

+ 5 - 0
packages/domain-events/src/Domain/Event/NotificationRequestedEventPayload.ts

@@ -0,0 +1,5 @@
+export interface NotificationRequestedEventPayload {
+  userUuid: string
+  type: string
+  payload: string
+}

+ 2 - 0
packages/domain-events/src/Domain/index.ts

@@ -42,6 +42,8 @@ export * from './Event/ListedAccountRequestedEvent'
 export * from './Event/ListedAccountRequestedEventPayload'
 export * from './Event/MuteEmailsSettingChangedEvent'
 export * from './Event/MuteEmailsSettingChangedEventPayload'
+export * from './Event/NotificationRequestedEvent'
+export * from './Event/NotificationRequestedEventPayload'
 export * from './Event/PaymentFailedEvent'
 export * from './Event/PaymentFailedEventPayload'
 export * from './Event/PaymentSuccessEvent'

+ 20 - 0
packages/syncing-server/src/Domain/Event/DomainEventFactory.ts

@@ -5,6 +5,7 @@ import {
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemRevisionCreationRequestedEvent,
+  NotificationRequestedEvent,
   RevisionsCopyRequestedEvent,
 } from '@standardnotes/domain-events'
 import { TimerInterface } from '@standardnotes/time'
@@ -13,6 +14,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 export class DomainEventFactory implements DomainEventFactoryInterface {
   constructor(private timer: TimerInterface) {}
 
+  createNotificationRequestedEvent(dto: {
+    userUuid: string
+    type: string
+    payload: string
+  }): NotificationRequestedEvent {
+    return {
+      type: 'NOTIFICATION_REQUESTED',
+      createdAt: this.timer.getUTCDate(),
+      meta: {
+        correlation: {
+          userIdentifier: dto.userUuid,
+          userIdentifierType: 'uuid',
+        },
+        origin: DomainEventService.SyncingServer,
+      },
+      payload: dto,
+    }
+  }
+
   createRevisionsCopyRequestedEvent(
     userUuid: string,
     dto: {

+ 2 - 0
packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -3,10 +3,12 @@ import {
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemRevisionCreationRequestedEvent,
+  NotificationRequestedEvent,
   RevisionsCopyRequestedEvent,
 } from '@standardnotes/domain-events'
 
 export interface DomainEventFactoryInterface {
+  createNotificationRequestedEvent(dto: { userUuid: string; type: string; payload: string }): NotificationRequestedEvent
   createEmailRequestedEvent(dto: {
     userEmail: string
     messageIdentifier: string

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

@@ -6,5 +6,6 @@ export interface SharedVaultInviteRepositoryInterface {
   findByUuid(sharedVaultInviteUuid: Uuid): Promise<SharedVaultInvite | null>
   save(sharedVaultInvite: SharedVaultInvite): Promise<void>
   remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
+  removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
   findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite | null>
 }

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

@@ -5,7 +5,9 @@ import { SharedVaultUser } from './SharedVaultUser'
 export interface SharedVaultUserRepositoryInterface {
   findByUuid(sharedVaultUserUuid: Uuid): Promise<SharedVaultUser | null>
   findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]>
+  findBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser[]>
   save(sharedVaultUser: SharedVaultUser): Promise<void>
   remove(sharedVault: SharedVaultUser): Promise<void>
+  removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
   findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
 }

+ 146 - 0
packages/syncing-server/src/Domain/UseCase/DeleteSharedVault/DeleteSharedVault.spec.ts

@@ -0,0 +1,146 @@
+import { DomainEventPublisherInterface, NotificationRequestedEvent } from '@standardnotes/domain-events'
+import { Uuid, Timestamps } from '@standardnotes/domain-core'
+
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { DeleteSharedVault } from './DeleteSharedVault'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
+import { SharedVault } from '../../SharedVault/SharedVault'
+import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+
+describe('DeleteSharedVault', () => {
+  let sharedVaultRepository: SharedVaultRepositoryInterface
+  let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
+  let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
+  let domainEventPublisher: DomainEventPublisherInterface
+  let domainEventFactory: DomainEventFactoryInterface
+  let sharedVault: SharedVault
+  let sharedVaultUser: SharedVaultUser
+
+  const createUseCase = () =>
+    new DeleteSharedVault(
+      sharedVaultRepository,
+      sharedVaultUserRepository,
+      sharedVaultInviteRepository,
+      domainEventPublisher,
+      domainEventFactory,
+    )
+
+  beforeEach(() => {
+    sharedVault = SharedVault.create({
+      fileUploadBytesLimit: 100,
+      fileUploadBytesUsed: 2,
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+    sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
+    sharedVaultRepository.remove = jest.fn()
+
+    sharedVaultUser = SharedVaultUser.create({
+      permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+    sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockResolvedValue([sharedVaultUser])
+    sharedVaultUserRepository.removeBySharedVaultUuid = jest.fn()
+
+    sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
+    sharedVaultInviteRepository.removeBySharedVaultUuid = jest.fn()
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createNotificationRequestedEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<NotificationRequestedEvent>)
+  })
+
+  it('should remove shared vault', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(sharedVaultRepository.remove).toHaveBeenCalled()
+    expect(sharedVaultUserRepository.removeBySharedVaultUuid).toHaveBeenCalled()
+    expect(sharedVaultInviteRepository.removeBySharedVaultUuid).toHaveBeenCalled()
+    expect(domainEventPublisher.publish).toHaveBeenCalled()
+  })
+
+  it('should return error when shared vault does not exist', async () => {
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(null)
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(domainEventPublisher.publish).not.toHaveBeenCalled()
+  })
+
+  it('should return error when shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: 'invalid',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(domainEventPublisher.publish).not.toHaveBeenCalled()
+  })
+
+  it('should return error when originator uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: 'invalid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(domainEventPublisher.publish).not.toHaveBeenCalled()
+  })
+
+  it('should return error when originator of the delete request is not the owner of the shared vault', async () => {
+    sharedVault = SharedVault.create({
+      fileUploadBytesLimit: 100,
+      fileUploadBytesUsed: 2,
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(sharedVaultInviteRepository.removeBySharedVaultUuid).not.toHaveBeenCalled()
+    expect(domainEventPublisher.publish).not.toHaveBeenCalled()
+  })
+})

+ 62 - 0
packages/syncing-server/src/Domain/UseCase/DeleteSharedVault/DeleteSharedVault.ts

@@ -0,0 +1,62 @@
+import { NotificationType, Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { DeleteSharedVaultDTO } from './DeleteSharedVaultDTO'
+import { SharedVaultRepositoryInterface } from '../../SharedVault/SharedVaultRepositoryInterface'
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+
+export class DeleteSharedVault implements UseCaseInterface<void> {
+  constructor(
+    private sharedVaultRepository: SharedVaultRepositoryInterface,
+    private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
+    private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
+    private domainEventPublisher: DomainEventPublisherInterface,
+    private domainEventFactory: DomainEventFactoryInterface,
+  ) {}
+
+  async execute(dto: DeleteSharedVaultDTO): Promise<Result<void>> {
+    const originatorUuidOrError = Uuid.create(dto.originatorUuid)
+    if (originatorUuidOrError.isFailed()) {
+      return Result.fail(originatorUuidOrError.getError())
+    }
+    const originatorUuid = originatorUuidOrError.getValue()
+
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
+    if (!sharedVault) {
+      return Result.fail('Shared vault not found')
+    }
+
+    if (sharedVault.props.userUuid.value !== originatorUuid.value) {
+      return Result.fail('Shared vault does not belong to the user')
+    }
+
+    const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
+    for (const sharedVaultUser of sharedVaultUsers) {
+      await this.domainEventPublisher.publish(
+        this.domainEventFactory.createNotificationRequestedEvent({
+          payload: JSON.stringify({
+            sharedVaultUuid: sharedVault.id.toString(),
+            version: '1.0',
+          }),
+          userUuid: sharedVaultUser.props.userUuid.value,
+          type: NotificationType.TYPES.RemovedFromSharedVault,
+        }),
+      )
+    }
+
+    await this.sharedVaultInviteRepository.removeBySharedVaultUuid(sharedVaultUuid)
+
+    await this.sharedVaultUserRepository.removeBySharedVaultUuid(sharedVaultUuid)
+
+    await this.sharedVaultRepository.remove(sharedVault)
+
+    return Result.ok()
+  }
+}

+ 4 - 0
packages/syncing-server/src/Domain/UseCase/DeleteSharedVault/DeleteSharedVaultDTO.ts

@@ -0,0 +1,4 @@
+export interface DeleteSharedVaultDTO {
+  originatorUuid: string
+  sharedVaultUuid: string
+}

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

@@ -11,6 +11,15 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
     private mapper: MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>,
   ) {}
 
+  async removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder('shared_vault_invite')
+      .delete()
+      .from('shared_vault_invites')
+      .where('shared_vault_uuid = :sharedVaultUuid', { sharedVaultUuid: sharedVaultUuid.value })
+      .execute()
+  }
+
   async findByUserUuidAndSharedVaultUuid(dto: {
     userUuid: Uuid
     sharedVaultUuid: Uuid

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

@@ -11,6 +11,26 @@ export class TypeORMSharedVaultUserRepository implements SharedVaultUserReposito
     private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
   ) {}
 
+  async removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder('shared_vault_user')
+      .delete()
+      .from('shared_vault_users')
+      .where('shared_vault_uuid = :sharedVaultUuid', { sharedVaultUuid: sharedVaultUuid.value })
+      .execute()
+  }
+
+  async findBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<SharedVaultUser[]> {
+    const persistence = await this.ormRepository
+      .createQueryBuilder('shared_vault_user')
+      .where('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', {
+        sharedVaultUuid: sharedVaultUuid.value,
+      })
+      .getMany()
+
+    return persistence.map((p) => this.mapper.toDomain(p))
+  }
+
   async findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]> {
     const persistence = await this.ormRepository
       .createQueryBuilder('shared_vault_user')