浏览代码

feat: add removing revisions from shared vaults (#811)

Karol Sójko 1 年之前
父节点
当前提交
3bd63f7674
共有 18 个文件被更改,包括 316 次插入29 次删除
  1. 7 0
      packages/domain-events/src/Domain/Event/ItemRemovedFromSharedVaultEvent.ts
  2. 6 0
      packages/domain-events/src/Domain/Event/ItemRemovedFromSharedVaultEventPayload.ts
  3. 2 0
      packages/domain-events/src/Domain/index.ts
  4. 18 0
      packages/revisions/src/Bootstrap/Container.ts
  5. 2 0
      packages/revisions/src/Bootstrap/Types.ts
  6. 22 0
      packages/revisions/src/Domain/Handler/ItemRemovedFromSharedVaultEventHandler.ts
  7. 1 0
      packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts
  8. 66 0
      packages/revisions/src/Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault.spec.ts
  9. 33 0
      packages/revisions/src/Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault.ts
  10. 5 0
      packages/revisions/src/Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVaultDTO.ts
  11. 15 0
      packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts
  12. 4 0
      packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevisionRepository.ts
  13. 15 0
      packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts
  14. 21 0
      packages/syncing-server/src/Domain/Event/DomainEventFactory.ts
  15. 7 0
      packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts
  16. 44 0
      packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts
  17. 36 25
      packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts
  18. 12 4
      packages/syncing-server/src/Infra/TypeORM/MongoDBItemRepository.ts

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

@@ -0,0 +1,7 @@
+import { DomainEventInterface } from './DomainEventInterface'
+import { ItemRemovedFromSharedVaultEventPayload } from './ItemRemovedFromSharedVaultEventPayload'
+
+export interface ItemRemovedFromSharedVaultEvent extends DomainEventInterface {
+  type: 'ITEM_REMOVED_FROM_SHARED_VAULT'
+  payload: ItemRemovedFromSharedVaultEventPayload
+}

+ 6 - 0
packages/domain-events/src/Domain/Event/ItemRemovedFromSharedVaultEventPayload.ts

@@ -0,0 +1,6 @@
+export interface ItemRemovedFromSharedVaultEventPayload {
+  userUuid: string
+  itemUuid: string
+  sharedVaultUuid: string
+  roleNames: string[]
+}

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

@@ -32,6 +32,8 @@ export * from './Event/FileUploadedEvent'
 export * from './Event/FileUploadedEventPayload'
 export * from './Event/FileUploadedEventPayload'
 export * from './Event/ItemDumpedEvent'
 export * from './Event/ItemDumpedEvent'
 export * from './Event/ItemDumpedEventPayload'
 export * from './Event/ItemDumpedEventPayload'
+export * from './Event/ItemRemovedFromSharedVaultEvent'
+export * from './Event/ItemRemovedFromSharedVaultEventPayload'
 export * from './Event/ItemRevisionCreationRequestedEvent'
 export * from './Event/ItemRevisionCreationRequestedEvent'
 export * from './Event/ItemRevisionCreationRequestedEventPayload'
 export * from './Event/ItemRevisionCreationRequestedEventPayload'
 export * from './Event/ListedAccountCreatedEvent'
 export * from './Event/ListedAccountCreatedEvent'

+ 18 - 0
packages/revisions/src/Bootstrap/Container.ts

@@ -66,6 +66,8 @@ import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
 import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository'
 import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository'
 import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper'
 import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper'
 import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper'
 import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper'
+import { RemoveRevisionsFromSharedVault } from '../Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault'
+import { ItemRemovedFromSharedVaultEventHandler } from '../Domain/Handler/ItemRemovedFromSharedVaultEventHandler'
 
 
 export class ContainerConfigLoader {
 export class ContainerConfigLoader {
   async load(configuration?: {
   async load(configuration?: {
@@ -358,6 +360,13 @@ export class ContainerConfigLoader {
           container.get<DomainEventFactoryInterface>(TYPES.Revisions_DomainEventFactory),
           container.get<DomainEventFactoryInterface>(TYPES.Revisions_DomainEventFactory),
         ),
         ),
       )
       )
+    container
+      .bind<RemoveRevisionsFromSharedVault>(TYPES.Revisions_RemoveRevisionsFromSharedVault)
+      .toConstantValue(
+        new RemoveRevisionsFromSharedVault(
+          container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
+        ),
+      )
 
 
     // env vars
     // env vars
     container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
     container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
@@ -437,12 +446,21 @@ export class ContainerConfigLoader {
           container.get<winston.Logger>(TYPES.Revisions_Logger),
           container.get<winston.Logger>(TYPES.Revisions_Logger),
         ),
         ),
       )
       )
+    container
+      .bind<ItemRemovedFromSharedVaultEventHandler>(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)
+      .toConstantValue(
+        new ItemRemovedFromSharedVaultEventHandler(
+          container.get<RemoveRevisionsFromSharedVault>(TYPES.Revisions_RemoveRevisionsFromSharedVault),
+          container.get<winston.Logger>(TYPES.Revisions_Logger),
+        ),
+      )
 
 
     const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
     const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
       ['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
       ['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
       ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Revisions_AccountDeletionRequestedEventHandler)],
       ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Revisions_AccountDeletionRequestedEventHandler)],
       ['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
       ['REVISIONS_COPY_REQUESTED', container.get(TYPES.Revisions_RevisionsCopyRequestedEventHandler)],
       ['TRANSITION_STATUS_UPDATED', container.get(TYPES.Revisions_TransitionStatusUpdatedEventHandler)],
       ['TRANSITION_STATUS_UPDATED', container.get(TYPES.Revisions_TransitionStatusUpdatedEventHandler)],
+      ['ITEM_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Revisions_ItemRemovedFromSharedVaultEventHandler)],
     ])
     ])
 
 
     if (isConfiguredForHomeServer) {
     if (isConfiguredForHomeServer) {

+ 2 - 0
packages/revisions/src/Bootstrap/Types.ts

@@ -49,6 +49,7 @@ const TYPES = {
   Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
   Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser: Symbol.for(
     'Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser',
     'Revisions_TriggerTransitionFromPrimaryToSecondaryDatabaseForUser',
   ),
   ),
+  Revisions_RemoveRevisionsFromSharedVault: Symbol.for('Revisions_RemoveRevisionsFromSharedVault'),
   // Controller
   // Controller
   Revisions_ControllerContainer: Symbol.for('Revisions_ControllerContainer'),
   Revisions_ControllerContainer: Symbol.for('Revisions_ControllerContainer'),
   Revisions_RevisionsController: Symbol.for('Revisions_RevisionsController'),
   Revisions_RevisionsController: Symbol.for('Revisions_RevisionsController'),
@@ -58,6 +59,7 @@ const TYPES = {
   Revisions_AccountDeletionRequestedEventHandler: Symbol.for('Revisions_AccountDeletionRequestedEventHandler'),
   Revisions_AccountDeletionRequestedEventHandler: Symbol.for('Revisions_AccountDeletionRequestedEventHandler'),
   Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
   Revisions_RevisionsCopyRequestedEventHandler: Symbol.for('Revisions_RevisionsCopyRequestedEventHandler'),
   Revisions_TransitionStatusUpdatedEventHandler: Symbol.for('Revisions_TransitionStatusUpdatedEventHandler'),
   Revisions_TransitionStatusUpdatedEventHandler: Symbol.for('Revisions_TransitionStatusUpdatedEventHandler'),
+  Revisions_ItemRemovedFromSharedVaultEventHandler: Symbol.for('Revisions_ItemRemovedFromSharedVaultEventHandler'),
   // Services
   // Services
   Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
   Revisions_CrossServiceTokenDecoder: Symbol.for('Revisions_CrossServiceTokenDecoder'),
   Revisions_DomainEventSubscriberFactory: Symbol.for('Revisions_DomainEventSubscriberFactory'),
   Revisions_DomainEventSubscriberFactory: Symbol.for('Revisions_DomainEventSubscriberFactory'),

+ 22 - 0
packages/revisions/src/Domain/Handler/ItemRemovedFromSharedVaultEventHandler.ts

@@ -0,0 +1,22 @@
+import { DomainEventHandlerInterface, ItemRemovedFromSharedVaultEvent } from '@standardnotes/domain-events'
+import { RemoveRevisionsFromSharedVault } from '../UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault'
+import { Logger } from 'winston'
+
+export class ItemRemovedFromSharedVaultEventHandler implements DomainEventHandlerInterface {
+  constructor(
+    private removeRevisionsFromSharedVault: RemoveRevisionsFromSharedVault,
+    private logger: Logger,
+  ) {}
+
+  async handle(event: ItemRemovedFromSharedVaultEvent): Promise<void> {
+    const result = await this.removeRevisionsFromSharedVault.execute({
+      sharedVaultUuid: event.payload.sharedVaultUuid,
+      itemUuid: event.payload.itemUuid,
+      roleNames: event.payload.roleNames,
+    })
+
+    if (result.isFailed()) {
+      this.logger.error(`Failed to remove revisions from shared vault: ${result.getError()}`)
+    }
+  }
+}

+ 1 - 0
packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts

@@ -13,4 +13,5 @@ export interface RevisionRepositoryInterface {
   updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void>
   updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void>
   findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Array<Revision>>
   findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Array<Revision>>
   insert(revision: Revision): Promise<boolean>
   insert(revision: Revision): Promise<boolean>
+  clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void>
 }
 }

+ 66 - 0
packages/revisions/src/Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault.spec.ts

@@ -0,0 +1,66 @@
+import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
+import { RemoveRevisionsFromSharedVault } from './RemoveRevisionsFromSharedVault'
+
+describe('RemoveRevisionsFromSharedVault', () => {
+  let revisionRepositoryResolver: RevisionRepositoryResolverInterface
+  let revisionRepository: RevisionRepositoryInterface
+
+  const createUseCase = () => new RemoveRevisionsFromSharedVault(revisionRepositoryResolver)
+
+  beforeEach(() => {
+    revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
+    revisionRepository.clearSharedVaultAndKeySystemAssociations = jest.fn()
+
+    revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
+    revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
+  })
+
+  it('should clear shared vault and key system associations', async () => {
+    const useCase = createUseCase()
+
+    await useCase.execute({
+      itemUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+      roleNames: ['CORE_USER'],
+    })
+
+    expect(revisionRepository.clearSharedVaultAndKeySystemAssociations).toHaveBeenCalled()
+  })
+
+  it('should return error when shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      itemUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: 'invalid',
+      roleNames: ['CORE_USER'],
+    })
+
+    expect(result.isFailed()).toBe(true)
+  })
+
+  it('should return error when item uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      itemUuid: 'invalid',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+      roleNames: ['CORE_USER'],
+    })
+
+    expect(result.isFailed()).toBe(true)
+  })
+
+  it('should return error when role names are invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      itemUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+      roleNames: ['invalid'],
+    })
+
+    expect(result.isFailed()).toBe(true)
+  })
+})

+ 33 - 0
packages/revisions/src/Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVault.ts

@@ -0,0 +1,33 @@
+import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
+import { RemoveRevisionsFromSharedVaultDTO } from './RemoveRevisionsFromSharedVaultDTO'
+
+export class RemoveRevisionsFromSharedVault implements UseCaseInterface<void> {
+  constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {}
+
+  async execute(dto: RemoveRevisionsFromSharedVaultDTO): Promise<Result<void>> {
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const itemUuidOrError = Uuid.create(dto.itemUuid)
+    if (itemUuidOrError.isFailed()) {
+      return Result.fail(itemUuidOrError.getError())
+    }
+    const itemUuid = itemUuidOrError.getValue()
+
+    const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return Result.fail(roleNamesOrError.getError())
+    }
+    const roleNames = roleNamesOrError.getValue()
+
+    const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
+
+    await revisionRepository.clearSharedVaultAndKeySystemAssociations(itemUuid, sharedVaultUuid)
+
+    return Result.ok()
+  }
+}

+ 5 - 0
packages/revisions/src/Domain/UseCase/RemoveRevisionsFromSharedVault/RemoveRevisionsFromSharedVaultDTO.ts

@@ -0,0 +1,5 @@
+export interface RemoveRevisionsFromSharedVaultDTO {
+  itemUuid: string
+  sharedVaultUuid: string
+  roleNames: string[]
+}

+ 15 - 0
packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts

@@ -16,6 +16,21 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
     private logger: Logger,
     private logger: Logger,
   ) {}
   ) {}
 
 
+  async clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void> {
+    await this.mongoRepository.updateMany(
+      {
+        itemUuid: { $eq: itemUuid.value },
+        sharedVaultUuid: { $eq: sharedVaultUuid.value },
+      },
+      {
+        $set: {
+          sharedVaultUuid: null,
+          keySystemIdentifier: null,
+        },
+      },
+    )
+  }
+
   async countByUserUuid(userUuid: Uuid): Promise<number> {
   async countByUserUuid(userUuid: Uuid): Promise<number> {
     return this.mongoRepository.count({ userUuid: { $eq: userUuid.value } })
     return this.mongoRepository.count({ userUuid: { $eq: userUuid.value } })
   }
   }

+ 4 - 0
packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevisionRepository.ts

@@ -15,6 +15,10 @@ export class SQLLegacyRevisionRepository implements RevisionRepositoryInterface
     protected logger: Logger,
     protected logger: Logger,
   ) {}
   ) {}
 
 
+  async clearSharedVaultAndKeySystemAssociations(_itemUuid: Uuid, _sharedVaultUuid: Uuid): Promise<void> {
+    this.logger.error('Method clearSharedVaultAndKeySystemAssociations not implemented.')
+  }
+
   async countByUserUuid(userUuid: Uuid): Promise<number> {
   async countByUserUuid(userUuid: Uuid): Promise<number> {
     return this.ormRepository
     return this.ormRepository
       .createQueryBuilder()
       .createQueryBuilder()

+ 15 - 0
packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts

@@ -45,6 +45,21 @@ export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
     return this.revisionMapper.toDomain(sqlRevision)
     return this.revisionMapper.toDomain(sqlRevision)
   }
   }
 
 
+  override async clearSharedVaultAndKeySystemAssociations(itemUuid: Uuid, sharedVaultUuid: Uuid): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder()
+      .update()
+      .set({
+        sharedVaultUuid: null,
+        keySystemIdentifier: null,
+      })
+      .where('item_uuid = :itemUuid AND shared_vault_uuid = :sharedVaultUuid', {
+        itemUuid: itemUuid.value,
+        sharedVaultUuid: sharedVaultUuid.value,
+      })
+      .execute()
+  }
+
   override async findMetadataByItemId(
   override async findMetadataByItemId(
     itemUuid: Uuid,
     itemUuid: Uuid,
     userUuid: Uuid,
     userUuid: Uuid,

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

@@ -4,6 +4,7 @@ import {
   DuplicateItemSyncedEvent,
   DuplicateItemSyncedEvent,
   EmailRequestedEvent,
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemDumpedEvent,
+  ItemRemovedFromSharedVaultEvent,
   ItemRevisionCreationRequestedEvent,
   ItemRevisionCreationRequestedEvent,
   MessageSentToUserEvent,
   MessageSentToUserEvent,
   NotificationAddedForUserEvent,
   NotificationAddedForUserEvent,
@@ -20,6 +21,26 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 export class DomainEventFactory implements DomainEventFactoryInterface {
 export class DomainEventFactory implements DomainEventFactoryInterface {
   constructor(private timer: TimerInterface) {}
   constructor(private timer: TimerInterface) {}
 
 
+  createItemRemovedFromSharedVaultEvent(dto: {
+    sharedVaultUuid: string
+    itemUuid: string
+    userUuid: string
+    roleNames: string[]
+  }): ItemRemovedFromSharedVaultEvent {
+    return {
+      type: 'ITEM_REMOVED_FROM_SHARED_VAULT',
+      createdAt: this.timer.getUTCDate(),
+      meta: {
+        correlation: {
+          userIdentifier: dto.userUuid,
+          userIdentifierType: 'uuid',
+        },
+        origin: DomainEventService.SyncingServer,
+      },
+      payload: dto,
+    }
+  }
+
   createUserRemovedFromSharedVaultEvent(dto: {
   createUserRemovedFromSharedVaultEvent(dto: {
     sharedVaultUuid: string
     sharedVaultUuid: string
     userUuid: string
     userUuid: string

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

@@ -2,6 +2,7 @@ import {
   DuplicateItemSyncedEvent,
   DuplicateItemSyncedEvent,
   EmailRequestedEvent,
   EmailRequestedEvent,
   ItemDumpedEvent,
   ItemDumpedEvent,
+  ItemRemovedFromSharedVaultEvent,
   ItemRevisionCreationRequestedEvent,
   ItemRevisionCreationRequestedEvent,
   MessageSentToUserEvent,
   MessageSentToUserEvent,
   NotificationAddedForUserEvent,
   NotificationAddedForUserEvent,
@@ -93,4 +94,10 @@ export interface DomainEventFactoryInterface {
     sharedVaultUuid: string
     sharedVaultUuid: string
     userUuid: string
     userUuid: string
   }): UserRemovedFromSharedVaultEvent
   }): UserRemovedFromSharedVaultEvent
+  createItemRemovedFromSharedVaultEvent(dto: {
+    sharedVaultUuid: string
+    itemUuid: string
+    userUuid: string
+    roleNames: string[]
+  }): ItemRemovedFromSharedVaultEvent
 }
 }

+ 44 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts

@@ -111,6 +111,9 @@ describe('UpdateExistingItem', () => {
     domainEventFactory.createItemRevisionCreationRequested = jest
     domainEventFactory.createItemRevisionCreationRequested = jest
       .fn()
       .fn()
       .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
       .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
+    domainEventFactory.createItemRemovedFromSharedVaultEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
 
 
     determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
     determineSharedVaultOperationOnItem = {} as jest.Mocked<DetermineSharedVaultOperationOnItem>
     determineSharedVaultOperationOnItem.execute = jest.fn().mockResolvedValue(
     determineSharedVaultOperationOnItem.execute = jest.fn().mockResolvedValue(
@@ -400,6 +403,47 @@ describe('UpdateExistingItem', () => {
       expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
       expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
     })
     })
 
 
+    it('should remove a shared vault association and publish an event that item has been removed from shared vault', async () => {
+      const useCase = createUseCase()
+
+      item1.props.sharedVaultAssociation = SharedVaultAssociation.create({
+        sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      }).getValue()
+
+      const itemHash = ItemHash.create({
+        ...itemHash1.props,
+        shared_vault_uuid: null,
+      }).getValue()
+
+      determineSharedVaultOperationOnItem.execute = jest.fn().mockReturnValue(
+        Result.ok(
+          SharedVaultOperationOnItem.create({
+            existingItem: item1,
+            incomingItemHash: itemHash1,
+            sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+            type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
+            userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+          }).getValue(),
+        ),
+      )
+
+      const result = await useCase.execute({
+        existingItem: item1,
+        itemHash,
+        sessionUuid: '00000000-0000-0000-0000-000000000000',
+        performingUserUuid: '00000000-0000-0000-0000-000000000000',
+        roleNames: [RoleName.NAMES.CoreUser],
+      })
+
+      expect(result.isFailed()).toBeFalsy()
+
+      expect(item1.props.sharedVaultAssociation).toBeUndefined()
+
+      expect(domainEventFactory.createItemRemovedFromSharedVaultEvent).toHaveBeenCalled()
+      expect(domainEventPublisher.publish).toHaveBeenCalled()
+    })
+
     it('should return error if shared vault association could not be created', async () => {
     it('should return error if shared vault association could not be created', async () => {
       const useCase = createUseCase()
       const useCase = createUseCase()
 
 

+ 36 - 25
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts

@@ -37,16 +37,6 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
   ) {}
   ) {}
 
 
   async execute(dto: UpdateExistingItemDTO): Promise<Result<Item>> {
   async execute(dto: UpdateExistingItemDTO): Promise<Result<Item>> {
-    let sessionUuid = null
-    if (dto.sessionUuid) {
-      const sessionUuidOrError = Uuid.create(dto.sessionUuid)
-      if (sessionUuidOrError.isFailed()) {
-        return Result.fail(sessionUuidOrError.getError())
-      }
-      sessionUuid = sessionUuidOrError.getValue()
-    }
-    dto.existingItem.props.updatedWithSession = sessionUuid
-
     const userUuidOrError = Uuid.create(dto.performingUserUuid)
     const userUuidOrError = Uuid.create(dto.performingUserUuid)
     if (userUuidOrError.isFailed()) {
     if (userUuidOrError.isFailed()) {
       return Result.fail(userUuidOrError.getError())
       return Result.fail(userUuidOrError.getError())
@@ -59,6 +49,29 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
     }
     }
     const roleNames = roleNamesOrError.getValue()
     const roleNames = roleNamesOrError.getValue()
 
 
+    let sharedVaultOperation: SharedVaultOperationOnItem | null = null
+    if (dto.itemHash.representsASharedVaultItem() || dto.existingItem.isAssociatedWithASharedVault()) {
+      const sharedVaultOperationOrError = await this.determineSharedVaultOperationOnItem.execute({
+        existingItem: dto.existingItem,
+        itemHash: dto.itemHash,
+        userUuid: userUuid.value,
+      })
+      if (sharedVaultOperationOrError.isFailed()) {
+        return Result.fail(sharedVaultOperationOrError.getError())
+      }
+      sharedVaultOperation = sharedVaultOperationOrError.getValue()
+    }
+
+    let sessionUuid = null
+    if (dto.sessionUuid) {
+      const sessionUuidOrError = Uuid.create(dto.sessionUuid)
+      if (sessionUuidOrError.isFailed()) {
+        return Result.fail(sessionUuidOrError.getError())
+      }
+      sessionUuid = sessionUuidOrError.getValue()
+    }
+    dto.existingItem.props.updatedWithSession = sessionUuid
+
     if (dto.itemHash.props.content) {
     if (dto.itemHash.props.content) {
       dto.existingItem.props.content = dto.itemHash.props.content
       dto.existingItem.props.content = dto.itemHash.props.content
     }
     }
@@ -128,7 +141,6 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
 
 
     dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
     dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
 
 
-    let sharedVaultOperation: SharedVaultOperationOnItem | null = null
     if (dto.itemHash.representsASharedVaultItem()) {
     if (dto.itemHash.representsASharedVaultItem()) {
       const sharedVaultAssociationOrError = SharedVaultAssociation.create({
       const sharedVaultAssociationOrError = SharedVaultAssociation.create({
         lastEditedBy: userUuid,
         lastEditedBy: userUuid,
@@ -140,16 +152,6 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
       }
       }
 
 
       dto.existingItem.props.sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
       dto.existingItem.props.sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
-
-      const sharedVaultOperationOrError = await this.determineSharedVaultOperationOnItem.execute({
-        existingItem: dto.existingItem,
-        itemHash: dto.itemHash,
-        userUuid: userUuid.value,
-      })
-      if (sharedVaultOperationOrError.isFailed()) {
-        return Result.fail(sharedVaultOperationOrError.getError())
-      }
-      sharedVaultOperation = sharedVaultOperationOrError.getValue()
     } else {
     } else {
       dto.existingItem.props.sharedVaultAssociation = undefined
       dto.existingItem.props.sharedVaultAssociation = undefined
     }
     }
@@ -209,7 +211,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
       )
       )
     }
     }
 
 
-    const notificationsResult = await this.addNotifications(dto.existingItem.uuid, userUuid, sharedVaultOperation)
+    const notificationsResult = await this.addNotificationsAndPublishEvents(userUuid, sharedVaultOperation, dto)
     if (notificationsResult.isFailed()) {
     if (notificationsResult.isFailed()) {
       return Result.fail(notificationsResult.getError())
       return Result.fail(notificationsResult.getError())
     }
     }
@@ -217,10 +219,10 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
     return Result.ok(dto.existingItem)
     return Result.ok(dto.existingItem)
   }
   }
 
 
-  private async addNotifications(
-    itemUuid: Uuid,
+  private async addNotificationsAndPublishEvents(
     userUuid: Uuid,
     userUuid: Uuid,
     sharedVaultOperation: SharedVaultOperationOnItem | null,
     sharedVaultOperation: SharedVaultOperationOnItem | null,
+    dto: UpdateExistingItemDTO,
   ): Promise<Result<void>> {
   ): Promise<Result<void>> {
     if (
     if (
       sharedVaultOperation &&
       sharedVaultOperation &&
@@ -229,7 +231,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
       const notificationPayloadOrError = NotificationPayload.create({
       const notificationPayloadOrError = NotificationPayload.create({
         sharedVaultUuid: sharedVaultOperation.props.sharedVaultUuid,
         sharedVaultUuid: sharedVaultOperation.props.sharedVaultUuid,
         type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
         type: NotificationType.create(NotificationType.TYPES.SharedVaultItemRemoved).getValue(),
-        itemUuid: itemUuid,
+        itemUuid: dto.existingItem.uuid,
         version: '1.0',
         version: '1.0',
       })
       })
       if (notificationPayloadOrError.isFailed()) {
       if (notificationPayloadOrError.isFailed()) {
@@ -246,6 +248,15 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
       if (result.isFailed()) {
       if (result.isFailed()) {
         return Result.fail(result.getError())
         return Result.fail(result.getError())
       }
       }
+
+      await this.domainEventPublisher.publish(
+        this.domainEventFactory.createItemRemovedFromSharedVaultEvent({
+          sharedVaultUuid: sharedVaultOperation.props.sharedVaultUuid.value,
+          itemUuid: dto.existingItem.uuid.value,
+          userUuid: userUuid.value,
+          roleNames: dto.roleNames,
+        }),
+      )
     }
     }
 
 
     if (sharedVaultOperation && sharedVaultOperation.props.type === SharedVaultOperationOnItem.TYPES.AddToSharedVault) {
     if (sharedVaultOperation && sharedVaultOperation.props.type === SharedVaultOperationOnItem.TYPES.AddToSharedVault) {

+ 12 - 4
packages/syncing-server/src/Infra/TypeORM/MongoDBItemRepository.ts

@@ -155,15 +155,23 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
 
 
   async markItemsAsDeleted(itemUuids: string[], updatedAtTimestamp: number): Promise<void> {
   async markItemsAsDeleted(itemUuids: string[], updatedAtTimestamp: number): Promise<void> {
     await this.mongoRepository.updateMany(
     await this.mongoRepository.updateMany(
-      { where: { _id: { $in: itemUuids.map((uuid) => BSON.UUID.createFromHexString(uuid)) } } },
-      { deleted: true, content: null, encItemKey: null, authHash: null, updatedAtTimestamp },
+      { _id: { $in: itemUuids.map((uuid) => BSON.UUID.createFromHexString(uuid)) } },
+      {
+        $set: {
+          deleted: true,
+          content: null,
+          encItemKey: null,
+          authHash: null,
+          updatedAtTimestamp,
+        },
+      },
     )
     )
   }
   }
 
 
   async updateContentSize(itemUuid: string, contentSize: number): Promise<void> {
   async updateContentSize(itemUuid: string, contentSize: number): Promise<void> {
     await this.mongoRepository.updateOne(
     await this.mongoRepository.updateOne(
-      { where: { _id: { $eq: BSON.UUID.createFromHexString(itemUuid) } } },
-      { contentSize },
+      { _id: { $eq: BSON.UUID.createFromHexString(itemUuid) } },
+      { $set: { contentSize } },
     )
     )
   }
   }