فهرست منبع

feat(syncing-server): determin shared vault operation type (#669)

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko 1 سال پیش
والد
کامیت
71721ab198

+ 53 - 0
packages/syncing-server/src/Domain/Item/Item.spec.ts

@@ -1,6 +1,7 @@
 import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
 
 import { Item } from './Item'
+import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
 
 describe('Item', () => {
   it('should create an aggregate', () => {
@@ -44,4 +45,56 @@ describe('Item', () => {
     expect(entityOrError.isFailed()).toBeFalsy()
     expect(() => entityOrError.getValue().uuid).toThrow()
   })
+
+  it('should tell if an item is associated with a shared vault', () => {
+    const entityOrError = Item.create({
+      duplicateOf: null,
+      itemsKeyId: 'items-key-id',
+      content: 'content',
+      contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+      encItemKey: 'enc-item-key',
+      authHash: 'auth-hash',
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      deleted: false,
+      updatedWithSession: null,
+      dates: Dates.create(new Date(123), new Date(123)).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+      sharedVaultAssociation: SharedVaultAssociation.create({
+        itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        timestamps: Timestamps.create(123, 123).getValue(),
+      }).getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+    expect(
+      entityOrError
+        .getValue()
+        .isAssociatedWithSharedVault(Uuid.create('00000000-0000-0000-0000-000000000000').getValue()),
+    ).toBeTruthy()
+  })
+
+  it('should tell that an item is not associated with a shared vault', () => {
+    const entityOrError = Item.create({
+      duplicateOf: null,
+      itemsKeyId: 'items-key-id',
+      content: 'content',
+      contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+      encItemKey: 'enc-item-key',
+      authHash: 'auth-hash',
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      deleted: false,
+      updatedWithSession: null,
+      dates: Dates.create(new Date(123), new Date(123)).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+    expect(
+      entityOrError
+        .getValue()
+        .isAssociatedWithSharedVault(Uuid.create('00000000-0000-0000-0000-000000000000').getValue()),
+    ).toBeFalsy()
+  })
 })

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

@@ -12,6 +12,31 @@ export class Item extends Aggregate<ItemProps> {
     return uuidOrError.getValue()
   }
 
+  get sharedVaultUuid(): Uuid | null {
+    if (!this.props.sharedVaultAssociation) {
+      return null
+    }
+
+    return this.props.sharedVaultAssociation.props.sharedVaultUuid
+  }
+
+  isAssociatedWithSharedVault(sharedVaultUuid: Uuid): boolean {
+    const associatedSharedVaultUuid = this.sharedVaultUuid
+    if (!associatedSharedVaultUuid) {
+      return false
+    }
+
+    return associatedSharedVaultUuid.equals(sharedVaultUuid)
+  }
+
+  isAssociatedWithKeySystem(keySystemIdentifier: string): boolean {
+    if (!this.props.keySystemAssociation) {
+      return false
+    }
+
+    return this.props.keySystemAssociation.props.keySystemIdentifier === keySystemIdentifier
+  }
+
   private constructor(props: ItemProps, id?: UniqueEntityId) {
     super(props, id)
   }

+ 38 - 0
packages/syncing-server/src/Domain/Item/ItemHash.spec.ts

@@ -0,0 +1,38 @@
+import { ContentType } from '@standardnotes/domain-core'
+import { ItemHash } from './ItemHash'
+
+describe('ItemHash', () => {
+  it('should create a value object', () => {
+    const valueOrError = ItemHash.create({
+      uuid: '00000000-0000-0000-0000-000000000000',
+      content_type: ContentType.TYPES.Note,
+      user_uuid: '00000000-0000-0000-0000-000000000000',
+      content: 'foobar',
+      created_at: '2020-01-01T00:00:00.000Z',
+      updated_at: '2020-01-01T00:00:00.000Z',
+      created_at_timestamp: 123,
+      updated_at_timestamp: 123,
+      key_system_identifier: null,
+      shared_vault_uuid: null,
+    })
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+  })
+
+  it('should return error if shared vault uuid is not valid', () => {
+    const valueOrError = ItemHash.create({
+      uuid: '00000000-0000-0000-0000-000000000000',
+      content_type: ContentType.TYPES.Note,
+      user_uuid: '00000000-0000-0000-0000-000000000000',
+      content: 'foobar',
+      created_at: '2020-01-01T00:00:00.000Z',
+      updated_at: '2020-01-01T00:00:00.000Z',
+      created_at_timestamp: 123,
+      updated_at_timestamp: 123,
+      key_system_identifier: null,
+      shared_vault_uuid: 'invalid',
+    })
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+})

+ 16 - 1
packages/syncing-server/src/Domain/Item/ItemHash.ts

@@ -1,4 +1,4 @@
-import { Result, ValueObject } from '@standardnotes/domain-core'
+import { Result, Uuid, ValueObject } from '@standardnotes/domain-core'
 
 import { ItemHashProps } from './ItemHashProps'
 
@@ -8,6 +8,13 @@ export class ItemHash extends ValueObject<ItemHashProps> {
   }
 
   static create(props: ItemHashProps): Result<ItemHash> {
+    if (props.shared_vault_uuid) {
+      const sharedVaultUuidOrError = Uuid.create(props.shared_vault_uuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        return Result.fail<ItemHash>(sharedVaultUuidOrError.getError())
+      }
+    }
+
     return Result.ok<ItemHash>(new ItemHash(props))
   }
 
@@ -15,6 +22,14 @@ export class ItemHash extends ValueObject<ItemHashProps> {
     return this.props.shared_vault_uuid !== null
   }
 
+  get sharedVaultUuid(): Uuid | null {
+    if (!this.representsASharedVaultItem()) {
+      return null
+    }
+
+    return Uuid.create(this.props.shared_vault_uuid as string).getValue()
+  }
+
   hasDedicatedKeySystemAssociation(): boolean {
     return this.props.key_system_identifier !== null
   }

+ 95 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVaultOperationOnItem.spec.ts

@@ -0,0 +1,95 @@
+import { ContentType } from '@standardnotes/domain-core'
+
+import { ItemHash } from '../Item/ItemHash'
+import { SharedVaultOperationOnItem } from './SharedVaultOperationOnItem'
+
+describe('SharedVaultOperationOnItem', () => {
+  let itemHash: ItemHash
+
+  beforeEach(() => {
+    itemHash = ItemHash.create({
+      uuid: '2-3-4',
+      content_type: ContentType.TYPES.Note,
+      user_uuid: '00000000-0000-0000-0000-000000000000',
+      content: 'foobar',
+      created_at: '2020-01-01T00:00:00.000Z',
+      updated_at: '2020-01-01T00:00:00.000Z',
+      created_at_timestamp: 123,
+      updated_at_timestamp: 123,
+      key_system_identifier: null,
+      shared_vault_uuid: null,
+    }).getValue()
+  })
+
+  it('should create a value object', () => {
+    const valueOrError = SharedVaultOperationOnItem.create({
+      incomingItemHash: itemHash,
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      targetSharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+  })
+
+  it('should return error if user uuid is not valid', () => {
+    const valueOrError = SharedVaultOperationOnItem.create({
+      incomingItemHash: itemHash,
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      targetSharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
+      userUuid: 'invalid',
+    })
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if shared vault uuid is not valid', () => {
+    const valueOrError = SharedVaultOperationOnItem.create({
+      incomingItemHash: itemHash,
+      sharedVaultUuid: 'invalid',
+      targetSharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if shared vault operation type is invalid', () => {
+    const valueOrError = SharedVaultOperationOnItem.create({
+      incomingItemHash: itemHash,
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      targetSharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      type: 'invalid',
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if target shared vault uuid is not valid', () => {
+    const valueOrError = SharedVaultOperationOnItem.create({
+      incomingItemHash: itemHash,
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      targetSharedVaultUuid: 'invalid',
+      type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+
+  it('should return error if operation type is move to other shared vault and target shared vault uuid is not provided', () => {
+    const valueOrError = SharedVaultOperationOnItem.create({
+      incomingItemHash: itemHash,
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      targetSharedVaultUuid: undefined,
+      type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+})

+ 47 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVaultOperationOnItem.ts

@@ -0,0 +1,47 @@
+import { ValueObject, Result, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultOperationOnItemProps } from './SharedVaultOperationOnItemProps'
+
+export class SharedVaultOperationOnItem extends ValueObject<SharedVaultOperationOnItemProps> {
+  static readonly TYPES = {
+    AddToSharedVault: 'add-to-shared-vault',
+    RemoveFromSharedVault: 'remove-from-shared-vault',
+    MoveToOtherSharedVault: 'move-to-other-shared-vault',
+    SaveToSharedVault: 'save-to-shared-vault',
+    CreateToSharedVault: 'create-to-shared-vault',
+  }
+
+  private constructor(props: SharedVaultOperationOnItemProps) {
+    super(props)
+  }
+
+  static create(props: SharedVaultOperationOnItemProps): Result<SharedVaultOperationOnItem> {
+    const userUuidOrError = Uuid.create(props.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail<SharedVaultOperationOnItem>(userUuidOrError.getError())
+    }
+
+    const isValidType = Object.values(this.TYPES).includes(props.type)
+    if (!isValidType) {
+      return Result.fail<SharedVaultOperationOnItem>(`Invalid shared vault operation type: ${props.type}`)
+    }
+
+    const sharedVaultUuidOrError = Uuid.create(props.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail<SharedVaultOperationOnItem>(sharedVaultUuidOrError.getError())
+    }
+
+    if (props.targetSharedVaultUuid) {
+      const targetSharedVaultUuidOrError = Uuid.create(props.targetSharedVaultUuid)
+      if (targetSharedVaultUuidOrError.isFailed()) {
+        return Result.fail<SharedVaultOperationOnItem>(targetSharedVaultUuidOrError.getError())
+      }
+    }
+
+    if (props.type === this.TYPES.MoveToOtherSharedVault && !props.targetSharedVaultUuid) {
+      return Result.fail<SharedVaultOperationOnItem>('Missing target shared vault uuid')
+    }
+
+    return Result.ok<SharedVaultOperationOnItem>(new SharedVaultOperationOnItem(props))
+  }
+}

+ 11 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVaultOperationOnItemProps.ts

@@ -0,0 +1,11 @@
+import { Item } from '../Item/Item'
+import { ItemHash } from '../Item/ItemHash'
+
+export interface SharedVaultOperationOnItemProps {
+  incomingItemHash: ItemHash
+  userUuid: string
+  type: string
+  sharedVaultUuid: string
+  targetSharedVaultUuid?: string
+  existingItem?: Item
+}

+ 223 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem.spec.ts

@@ -0,0 +1,223 @@
+import { ContentType, Uuid, Dates, Timestamps, UniqueEntityId, Result } from '@standardnotes/domain-core'
+import { Item } from '../../../Item/Item'
+import { ItemHash } from '../../../Item/ItemHash'
+import { DetermineSharedVaultOperationOnItem } from './DetermineSharedVaultOperationOnItem'
+import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
+import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
+
+describe('DetermineSharedVaultOperationOnItem', () => {
+  let itemHash: ItemHash
+  let existingItem: Item
+
+  const createUseCase = () => new DetermineSharedVaultOperationOnItem()
+
+  beforeEach(() => {
+    itemHash = ItemHash.create({
+      uuid: '2-3-4',
+      content_type: ContentType.TYPES.Note,
+      user_uuid: '00000000-0000-0000-0000-000000000000',
+      content: 'foobar',
+      created_at: '2020-01-01T00:00:00.000Z',
+      updated_at: '2020-01-01T00:00:00.000Z',
+      created_at_timestamp: 123,
+      updated_at_timestamp: 123,
+      key_system_identifier: null,
+      shared_vault_uuid: null,
+    }).getValue()
+
+    existingItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
+  })
+
+  it('should return an error if user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: 'invalid',
+      existingItem,
+      itemHash,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
+  })
+
+  it('should return an operation representing moving to another shared vault', async () => {
+    existingItem = Item.create({
+      ...existingItem.props,
+      sharedVaultAssociation: SharedVaultAssociation.create({
+        itemUuid: existingItem.uuid,
+        lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        timestamps: Timestamps.create(123, 123).getValue(),
+      }).getValue(),
+    }).getValue()
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem,
+      itemHash: ItemHash.create({
+        ...itemHash.props,
+        shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
+      }).getValue(),
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault)
+    expect(result.getValue().props.sharedVaultUuid).toEqual('00000000-0000-0000-0000-000000000000')
+    expect(result.getValue().props.targetSharedVaultUuid).toEqual('00000000-0000-0000-0000-000000000001')
+  })
+
+  it('should return an operation representing removing from shared vault', async () => {
+    existingItem = Item.create({
+      ...existingItem.props,
+      sharedVaultAssociation: SharedVaultAssociation.create({
+        itemUuid: existingItem.uuid,
+        lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        timestamps: Timestamps.create(123, 123).getValue(),
+      }).getValue(),
+    }).getValue()
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem,
+      itemHash: ItemHash.create({
+        ...itemHash.props,
+        shared_vault_uuid: null,
+      }).getValue(),
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault)
+    expect(result.getValue().props.sharedVaultUuid).toEqual('00000000-0000-0000-0000-000000000000')
+  })
+
+  it('should return an operation representing adding to shared vault', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem,
+      itemHash: ItemHash.create({
+        ...itemHash.props,
+        shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
+      }).getValue(),
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.AddToSharedVault)
+    expect(result.getValue().props.sharedVaultUuid).toEqual('00000000-0000-0000-0000-000000000001')
+  })
+
+  it('should return an operation representing saving to shared vault', async () => {
+    existingItem = Item.create({
+      ...existingItem.props,
+      sharedVaultAssociation: SharedVaultAssociation.create({
+        itemUuid: existingItem.uuid,
+        lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        timestamps: Timestamps.create(123, 123).getValue(),
+      }).getValue(),
+    }).getValue()
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem,
+      itemHash: ItemHash.create({
+        ...itemHash.props,
+        shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
+      }).getValue(),
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.SaveToSharedVault)
+    expect(result.getValue().props.sharedVaultUuid).toEqual('00000000-0000-0000-0000-000000000000')
+  })
+
+  it('should return an operation representing creating to shared vault', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem: null,
+      itemHash: ItemHash.create({
+        ...itemHash.props,
+        shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
+      }).getValue(),
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(result.getValue().props.type).toEqual(SharedVaultOperationOnItem.TYPES.CreateToSharedVault)
+    expect(result.getValue().props.sharedVaultUuid).toEqual('00000000-0000-0000-0000-000000000001')
+  })
+
+  it('should return an error if both existing and incoming item hash do not have shared vault uuid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem: null,
+      itemHash: ItemHash.create({
+        ...itemHash.props,
+        shared_vault_uuid: null,
+      }).getValue(),
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(result.getError()).toEqual('Invalid save operation')
+  })
+
+  it('should return error if operation could not be determined based on input values', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem: null,
+      itemHash,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(result.getError()).toEqual('Invalid save operation')
+  })
+
+  it('should return error if shared vault operation on item could not be created', async () => {
+    const mock = jest.spyOn(SharedVaultOperationOnItem, 'create')
+    mock.mockImplementationOnce(() => Result.fail('error'))
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      existingItem: null,
+      itemHash: ItemHash.create({
+        ...itemHash.props,
+        shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
+      }).getValue(),
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+
+    mock.mockRestore()
+  })
+})

+ 87 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem.ts

@@ -0,0 +1,87 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultOperationOnItem } from '../../../SharedVault/SharedVaultOperationOnItem'
+import { DetermineSharedVaultOperationOnItemDTO } from './DetermineSharedVaultOperationOnItemDTO'
+import { Item } from '../../../Item/Item'
+
+export class DetermineSharedVaultOperationOnItem implements UseCaseInterface<SharedVaultOperationOnItem> {
+  async execute(dto: DetermineSharedVaultOperationOnItemDTO): Promise<Result<SharedVaultOperationOnItem>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    let existingItemSharedVaultUuid = null
+    if (dto.existingItem) {
+      existingItemSharedVaultUuid = dto.existingItem.sharedVaultUuid
+    }
+    const targetItemSharedVaultUuid = dto.itemHash.sharedVaultUuid
+
+    if (!existingItemSharedVaultUuid && !targetItemSharedVaultUuid) {
+      return Result.fail('Invalid save operation')
+    }
+
+    const isMovingToOtherSharedVault =
+      dto.existingItem &&
+      existingItemSharedVaultUuid &&
+      targetItemSharedVaultUuid &&
+      !existingItemSharedVaultUuid.equals(targetItemSharedVaultUuid)
+    const isRemovingFromSharedVault = dto.existingItem && existingItemSharedVaultUuid && !targetItemSharedVaultUuid
+    const isAddingToSharedVault = dto.existingItem && !existingItemSharedVaultUuid && targetItemSharedVaultUuid
+    const isSavingToSharedVault =
+      dto.existingItem &&
+      existingItemSharedVaultUuid &&
+      targetItemSharedVaultUuid &&
+      existingItemSharedVaultUuid.equals(targetItemSharedVaultUuid)
+
+    let operationOrError: Result<SharedVaultOperationOnItem>
+    if (isMovingToOtherSharedVault) {
+      operationOrError = SharedVaultOperationOnItem.create({
+        existingItem: dto.existingItem as Item,
+        sharedVaultUuid: (existingItemSharedVaultUuid as Uuid).value,
+        targetSharedVaultUuid: targetItemSharedVaultUuid.value,
+        incomingItemHash: dto.itemHash,
+        userUuid: userUuid.value,
+        type: SharedVaultOperationOnItem.TYPES.MoveToOtherSharedVault,
+      })
+    } else if (isRemovingFromSharedVault) {
+      operationOrError = SharedVaultOperationOnItem.create({
+        existingItem: dto.existingItem as Item,
+        sharedVaultUuid: (existingItemSharedVaultUuid as Uuid).value,
+        incomingItemHash: dto.itemHash,
+        userUuid: userUuid.value,
+        type: SharedVaultOperationOnItem.TYPES.RemoveFromSharedVault,
+      })
+    } else if (isAddingToSharedVault) {
+      operationOrError = SharedVaultOperationOnItem.create({
+        existingItem: dto.existingItem as Item,
+        sharedVaultUuid: targetItemSharedVaultUuid.value,
+        incomingItemHash: dto.itemHash,
+        userUuid: userUuid.value,
+        type: SharedVaultOperationOnItem.TYPES.AddToSharedVault,
+      })
+    } else if (isSavingToSharedVault) {
+      operationOrError = SharedVaultOperationOnItem.create({
+        existingItem: dto.existingItem as Item,
+        sharedVaultUuid: (existingItemSharedVaultUuid as Uuid).value,
+        incomingItemHash: dto.itemHash,
+        userUuid: userUuid.value,
+        type: SharedVaultOperationOnItem.TYPES.SaveToSharedVault,
+      })
+    } else {
+      operationOrError = SharedVaultOperationOnItem.create({
+        sharedVaultUuid: (targetItemSharedVaultUuid as Uuid).value,
+        incomingItemHash: dto.itemHash,
+        userUuid: userUuid.value,
+        type: SharedVaultOperationOnItem.TYPES.CreateToSharedVault,
+      })
+    }
+
+    if (operationOrError.isFailed()) {
+      return Result.fail(operationOrError.getError())
+    }
+
+    return Result.ok(operationOrError.getValue())
+  }
+}

+ 8 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItemDTO.ts

@@ -0,0 +1,8 @@
+import { ItemHash } from '../../../Item/ItemHash'
+import { Item } from '../../../Item/Item'
+
+export interface DetermineSharedVaultOperationOnItemDTO {
+  userUuid: string
+  itemHash: ItemHash
+  existingItem: Item | null
+}

+ 0 - 17
packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.spec.ts

@@ -303,23 +303,6 @@ describe('SaveNewItem', () => {
   })
 
   describe('when item hash represents a shared vault item', () => {
-    it('returns a failure if the shared vault uuid is invalid', async () => {
-      const useCase = createUseCase()
-
-      itemHash1 = ItemHash.create({
-        ...itemHash1.props,
-        shared_vault_uuid: '1-2-3',
-      }).getValue()
-
-      const result = await useCase.execute({
-        userUuid: '00000000-0000-0000-0000-000000000000',
-        sessionUuid: '00000000-0000-0000-0000-000000000001',
-        itemHash: itemHash1,
-      })
-
-      expect(result.isFailed()).toBeTruthy()
-    })
-
     it('should create a shared vault association between the item and the shared vault', async () => {
       const useCase = createUseCase()
 

+ 1 - 7
packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts

@@ -89,15 +89,9 @@ export class SaveNewItem implements UseCaseInterface<Item> {
 
     let sharedVaultAssociation = undefined
     if (dto.itemHash.representsASharedVaultItem()) {
-      const sharedVaultUuidOrError = Uuid.create(dto.itemHash.props.shared_vault_uuid as string)
-      if (sharedVaultUuidOrError.isFailed()) {
-        return Result.fail(sharedVaultUuidOrError.getError())
-      }
-      const sharedVaultUuid = sharedVaultUuidOrError.getValue()
-
       const sharedVaultAssociationOrError = SharedVaultAssociation.create({
         lastEditedBy: userUuid,
-        sharedVaultUuid,
+        sharedVaultUuid: dto.itemHash.sharedVaultUuid as Uuid,
         timestamps: Timestamps.create(
           this.timer.getTimestampInMicroseconds(),
           this.timer.getTimestampInMicroseconds(),

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

@@ -327,23 +327,6 @@ describe('UpdateExistingItem', () => {
       expect(item1.props.sharedVaultAssociation.id.toString()).toEqual(idBefore)
     })
 
-    it('should return error if shared vault uuid is invalid', async () => {
-      const useCase = createUseCase()
-
-      const itemHash = ItemHash.create({
-        ...itemHash1.props,
-        shared_vault_uuid: 'invalid-uuid',
-      }).getValue()
-
-      const result = await useCase.execute({
-        existingItem: item1,
-        itemHash,
-        sessionUuid: '00000000-0000-0000-0000-000000000000',
-        performingUserUuid: '00000000-0000-0000-0000-000000000000',
-      })
-      expect(result.isFailed()).toBeTruthy()
-    })
-
     it('should return error if shared vault association could not be created', async () => {
       const useCase = createUseCase()
 

+ 30 - 35
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts

@@ -1,4 +1,13 @@
-import { ContentType, Dates, Result, Timestamps, UseCaseInterface, Uuid, Validator } from '@standardnotes/domain-core'
+import {
+  ContentType,
+  Dates,
+  Result,
+  Timestamps,
+  UniqueEntityId,
+  UseCaseInterface,
+  Uuid,
+  Validator,
+} from '@standardnotes/domain-core'
 import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import { TimerInterface } from '@standardnotes/time'
 
@@ -8,7 +17,6 @@ import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
 import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
-import { ItemHash } from '../../../Item/ItemHash'
 
 export class UpdateExistingItem implements UseCaseInterface<Item> {
   constructor(
@@ -105,25 +113,26 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
 
     dto.existingItem.props.contentSize = Buffer.byteLength(JSON.stringify(dto.existingItem))
 
-    if (
-      dto.itemHash.representsASharedVaultItem() &&
-      !this.itemIsAlreadyAssociatedWithTheSharedVault(dto.existingItem, dto.itemHash)
-    ) {
-      const sharedVaultUuidOrError = Uuid.create(dto.itemHash.props.shared_vault_uuid as string)
-      if (sharedVaultUuidOrError.isFailed()) {
-        return Result.fail(sharedVaultUuidOrError.getError())
-      }
-      const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+    if (dto.itemHash.representsASharedVaultItem()) {
+      const sharedVaultAssociationOrError = SharedVaultAssociation.create(
+        {
+          lastEditedBy: userUuid,
+          sharedVaultUuid: dto.itemHash.sharedVaultUuid as Uuid,
+          timestamps: Timestamps.create(
+            dto.existingItem.props.sharedVaultAssociation
+              ? dto.existingItem.props.sharedVaultAssociation.props.timestamps.createdAt
+              : this.timer.getTimestampInMicroseconds(),
+            this.timer.getTimestampInMicroseconds(),
+          ).getValue(),
+          itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
+        },
+        new UniqueEntityId(
+          dto.existingItem.props.sharedVaultAssociation
+            ? dto.existingItem.props.sharedVaultAssociation.id.toString()
+            : undefined,
+        ),
+      )
 
-      const sharedVaultAssociationOrError = SharedVaultAssociation.create({
-        lastEditedBy: userUuid,
-        sharedVaultUuid,
-        timestamps: Timestamps.create(
-          this.timer.getTimestampInMicroseconds(),
-          this.timer.getTimestampInMicroseconds(),
-        ).getValue(),
-        itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(),
-      })
       if (sharedVaultAssociationOrError.isFailed()) {
         return Result.fail(sharedVaultAssociationOrError.getError())
       }
@@ -133,7 +142,7 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
 
     if (
       dto.itemHash.hasDedicatedKeySystemAssociation() &&
-      !this.itemIsAlreadyAssociatedWithTheKeySystem(dto.existingItem, dto.itemHash)
+      !dto.existingItem.isAssociatedWithKeySystem(dto.itemHash.props.key_system_identifier as string)
     ) {
       const keySystemIdentifiedValidationResult = Validator.isNotEmptyString(dto.itemHash.props.key_system_identifier)
       if (keySystemIdentifiedValidationResult.isFailed()) {
@@ -192,18 +201,4 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
 
     return Result.ok(dto.existingItem)
   }
-
-  private itemIsAlreadyAssociatedWithTheSharedVault(item: Item, itemHash: ItemHash): boolean {
-    return (
-      item.props.sharedVaultAssociation !== undefined &&
-      item.props.sharedVaultAssociation.props.sharedVaultUuid.value === itemHash.props.shared_vault_uuid
-    )
-  }
-
-  private itemIsAlreadyAssociatedWithTheKeySystem(item: Item, itemHash: ItemHash): boolean {
-    return (
-      item.props.keySystemAssociation !== undefined &&
-      item.props.keySystemAssociation.props.keySystemIdentifier === itemHash.props.key_system_identifier
-    )
-  }
 }