From c34f548e45bbd8defb8d490936e90755fd284e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Wed, 26 Jul 2023 12:23:10 +0200 Subject: [PATCH] fix(syncing-server): persisting aggregate changes from root (#674) --- .../domain-core/src/Domain/Core/Aggregate.ts | 17 +- .../domain-core/src/Domain/Core/Change.ts | 26 +++ .../src/Domain/Core/ChangeProps.ts | 9 ++ packages/domain-core/src/Domain/index.ts | 2 + .../src/Domain/Item/Item.spec.ts | 152 ++++++++++++++++++ .../syncing-server/src/Domain/Item/Item.ts | 72 ++++++++- .../Item/SaveRule/SharedVaultFilter.spec.ts | 15 +- .../Domain/Item/SaveRule/SharedVaultFilter.ts | 2 +- .../Syncing/SaveNewItem/SaveNewItem.ts | 50 +++--- .../Syncing/SyncItems/SyncItems.spec.ts | 28 +++- .../UseCase/Syncing/SyncItems/SyncItems.ts | 3 +- .../UpdateExistingItem.spec.ts | 30 ++-- .../UpdateExistingItem/UpdateExistingItem.ts | 36 +++-- .../Infra/TypeORM/TypeORMItemRepository.ts | 35 +++- 14 files changed, 389 insertions(+), 88 deletions(-) create mode 100644 packages/domain-core/src/Domain/Core/Change.ts create mode 100644 packages/domain-core/src/Domain/Core/ChangeProps.ts diff --git a/packages/domain-core/src/Domain/Core/Aggregate.ts b/packages/domain-core/src/Domain/Core/Aggregate.ts index 3e6ad3a39..b46e8b215 100644 --- a/packages/domain-core/src/Domain/Core/Aggregate.ts +++ b/packages/domain-core/src/Domain/Core/Aggregate.ts @@ -1,5 +1,20 @@ /* istanbul ignore file */ +import { Change } from './Change' import { Entity } from './Entity' -export abstract class Aggregate extends Entity {} +export abstract class Aggregate extends Entity { + private changesOnAggregateRoot: Change[] = [] + + addChange(change: Change): void { + this.changesOnAggregateRoot.push(change) + } + + flushChanges(): void { + this.changesOnAggregateRoot = [] + } + + getChanges(): Change[] { + return this.changesOnAggregateRoot + } +} diff --git a/packages/domain-core/src/Domain/Core/Change.ts b/packages/domain-core/src/Domain/Core/Change.ts new file mode 100644 index 000000000..4ee52419f --- /dev/null +++ b/packages/domain-core/src/Domain/Core/Change.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ + +import { ChangeProps } from './ChangeProps' +import { Result } from './Result' + +export class Change { + static readonly TYPES = { + Add: 'add', + Remove: 'remove', + Modify: 'modify', + } + + public readonly props: ChangeProps + + constructor(props: ChangeProps) { + this.props = Object.freeze(props) + } + + static create(props: ChangeProps): Result { + if (!Object.values(Change.TYPES).includes(props.changeType)) { + return Result.fail('Invalid change type') + } + + return Result.ok(new Change(props)) + } +} diff --git a/packages/domain-core/src/Domain/Core/ChangeProps.ts b/packages/domain-core/src/Domain/Core/ChangeProps.ts new file mode 100644 index 000000000..12634b362 --- /dev/null +++ b/packages/domain-core/src/Domain/Core/ChangeProps.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ + +import { Entity } from './Entity' + +export interface ChangeProps { + aggregateRootUuid: string + changeType: string + changeData: Entity +} diff --git a/packages/domain-core/src/Domain/index.ts b/packages/domain-core/src/Domain/index.ts index d20d5f519..eb312f931 100644 --- a/packages/domain-core/src/Domain/index.ts +++ b/packages/domain-core/src/Domain/index.ts @@ -27,6 +27,8 @@ export * from './Common/Uuid' export * from './Common/UuidProps' export * from './Core/Aggregate' +export * from './Core/Change' +export * from './Core/ChangeProps' export * from './Core/Entity' export * from './Core/Id' export * from './Core/Result' diff --git a/packages/syncing-server/src/Domain/Item/Item.spec.ts b/packages/syncing-server/src/Domain/Item/Item.spec.ts index 73f83372b..d7afd0ece 100644 --- a/packages/syncing-server/src/Domain/Item/Item.spec.ts +++ b/packages/syncing-server/src/Domain/Item/Item.spec.ts @@ -2,6 +2,7 @@ import { ContentType, Dates, Timestamps, UniqueEntityId, Uuid } from '@standardn import { Item } from './Item' import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation' +import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation' describe('Item', () => { it('should create an aggregate', () => { @@ -97,4 +98,155 @@ describe('Item', () => { .isAssociatedWithSharedVault(Uuid.create('00000000-0000-0000-0000-000000000000').getValue()), ).toBeFalsy() }) + + it('should tell if an item is associated with a key system', () => { + 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(), + keySystemAssociation: KeySystemAssociation.create({ + itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + keySystemIdentifier: 'key-system-identifier', + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue(), + }) + + expect(entityOrError.isFailed()).toBeFalsy() + expect(entityOrError.getValue().isAssociatedWithKeySystem('key-system-identifier')).toBeTruthy() + }) + + it('should tell that an item is not associated with a key system', () => { + 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().isAssociatedWithKeySystem('key-system-identifier')).toBeFalsy() + }) + + it('should set shared vault association', () => { + const 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() + + const entity = 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(), + }).getValue() + + entity.setSharedVaultAssociation(sharedVaultAssociation) + + expect(entity.props.sharedVaultAssociation).toEqual(sharedVaultAssociation) + expect(entity.getChanges()).toHaveLength(1) + }) + + it('should unset a shared vault association', () => { + const entity = 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(), + }).getValue() + + entity.unsetSharedVaultAssociation() + + expect(entity.props.sharedVaultAssociation).toBeUndefined() + expect(entity.getChanges()).toHaveLength(1) + }) + + it('should set key system association', () => { + const keySystemAssociation = KeySystemAssociation.create({ + itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + keySystemIdentifier: 'key-system-identifier', + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + const entity = 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(), + }).getValue() + + entity.setKeySystemAssociation(keySystemAssociation) + + expect(entity.props.keySystemAssociation).toEqual(keySystemAssociation) + expect(entity.getChanges()).toHaveLength(1) + }) + + it('should unset a key system association', () => { + const entity = 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(), + keySystemAssociation: KeySystemAssociation.create({ + itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + keySystemIdentifier: 'key-system-identifier', + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue(), + }).getValue() + + entity.unsetKeySystemAssociation() + + expect(entity.props.keySystemAssociation).toBeUndefined() + expect(entity.getChanges()).toHaveLength(1) + }) }) diff --git a/packages/syncing-server/src/Domain/Item/Item.ts b/packages/syncing-server/src/Domain/Item/Item.ts index 58449988f..3a6e17010 100644 --- a/packages/syncing-server/src/Domain/Item/Item.ts +++ b/packages/syncing-server/src/Domain/Item/Item.ts @@ -1,8 +1,23 @@ -import { Aggregate, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core' +import { Aggregate, Change, Result, UniqueEntityId, Uuid } from '@standardnotes/domain-core' import { ItemProps } from './ItemProps' +import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation' +import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation' export class Item extends Aggregate { + private constructor(props: ItemProps, id?: UniqueEntityId) { + super(props, id) + } + + static create(props: ItemProps, id?: UniqueEntityId): Result { + if (!props.contentSize) { + const contentSize = Buffer.byteLength(JSON.stringify(props)) + props.contentSize = contentSize + } + + return Result.ok(new Item(props, id)) + } + get uuid(): Uuid { const uuidOrError = Uuid.create(this._id.toString()) if (uuidOrError.isFailed()) { @@ -40,16 +55,57 @@ export class Item extends Aggregate { return this.props.keySystemAssociation.props.keySystemIdentifier === keySystemIdentifier } - private constructor(props: ItemProps, id?: UniqueEntityId) { - super(props, id) + setSharedVaultAssociation(sharedVaultAssociation: SharedVaultAssociation): void { + this.addChange( + Change.create({ + aggregateRootUuid: this.uuid.value, + changeType: this.props.sharedVaultAssociation ? Change.TYPES.Modify : Change.TYPES.Add, + changeData: sharedVaultAssociation, + }).getValue(), + ) + + this.props.sharedVaultAssociation = sharedVaultAssociation } - static create(props: ItemProps, id?: UniqueEntityId): Result { - if (!props.contentSize) { - const contentSize = Buffer.byteLength(JSON.stringify(props)) - props.contentSize = contentSize + unsetSharedVaultAssociation(): void { + if (!this.props.sharedVaultAssociation) { + return } - return Result.ok(new Item(props, id)) + this.addChange( + Change.create({ + aggregateRootUuid: this.uuid.value, + changeType: Change.TYPES.Remove, + changeData: this.props.sharedVaultAssociation, + }).getValue(), + ) + this.props.sharedVaultAssociation = undefined + } + + setKeySystemAssociation(keySystemAssociation: KeySystemAssociation): void { + this.addChange( + Change.create({ + aggregateRootUuid: this.uuid.value, + changeType: this.props.keySystemAssociation ? Change.TYPES.Modify : Change.TYPES.Add, + changeData: keySystemAssociation, + }).getValue(), + ) + + this.props.keySystemAssociation = keySystemAssociation + } + + unsetKeySystemAssociation(): void { + if (!this.props.keySystemAssociation) { + return + } + + this.addChange( + Change.create({ + aggregateRootUuid: this.uuid.value, + changeType: Change.TYPES.Remove, + changeData: this.props.keySystemAssociation, + }).getValue(), + ) + this.props.keySystemAssociation = undefined } } diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts b/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts index eb49c5b76..a07f6d66a 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts @@ -72,24 +72,11 @@ describe('SharedVaultFilter', () => { .mockResolvedValueOnce(null) }) - it('should return as passed if the item hash does not represent a shared vault item', async () => { + it('should return as passed if the item hash does not represent a shared vault item and existing item is not a shared vault item', async () => { itemHash = ItemHash.create({ ...itemHash.props, shared_vault_uuid: null, }).getValue() - - const filter = createFilter() - const result = await filter.check({ - apiVersion: '001', - existingItem: existingItem, - itemHash: itemHash, - userUuid: '00000000-0000-0000-0000-000000000000', - }) - - expect(result.passed).toBe(true) - }) - - it('should return as passed if the item is not a shared vault item', async () => { existingItem = Item.create({ ...existingItem.props, sharedVaultAssociation: undefined, diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.ts index 17a437035..b5a70834f 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.ts @@ -16,7 +16,7 @@ export class SharedVaultFilter implements ItemSaveRuleInterface { ) {} async check(dto: ItemSaveValidationDTO): Promise { - if (!dto.itemHash.representsASharedVaultItem() || !dto.existingItem?.isAssociatedWithASharedVault()) { + if (!dto.itemHash.representsASharedVaultItem() && !dto.existingItem?.isAssociatedWithASharedVault()) { return { passed: true, } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts index 6bc0261de..6b5303783 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts @@ -87,7 +87,27 @@ export class SaveNewItem implements UseCaseInterface { } const timestamps = timestampsOrError.getValue() - let sharedVaultAssociation = undefined + const itemOrError = Item.create( + { + updatedWithSession, + content: dto.itemHash.props.content ?? null, + userUuid, + contentType, + encItemKey: dto.itemHash.props.enc_item_key ?? null, + authHash: dto.itemHash.props.auth_hash ?? null, + itemsKeyId: dto.itemHash.props.items_key_id ?? null, + duplicateOf, + deleted: dto.itemHash.props.deleted ?? false, + dates, + timestamps, + }, + new UniqueEntityId(uuid.value), + ) + if (itemOrError.isFailed()) { + return Result.fail(itemOrError.getError()) + } + const newItem = itemOrError.getValue() + if (dto.itemHash.representsASharedVaultItem()) { const sharedVaultAssociationOrError = SharedVaultAssociation.create({ lastEditedBy: userUuid, @@ -101,10 +121,9 @@ export class SaveNewItem implements UseCaseInterface { if (sharedVaultAssociationOrError.isFailed()) { return Result.fail(sharedVaultAssociationOrError.getError()) } - sharedVaultAssociation = sharedVaultAssociationOrError.getValue() + newItem.setSharedVaultAssociation(sharedVaultAssociationOrError.getValue()) } - let keySystemAssociation = undefined if (dto.itemHash.hasDedicatedKeySystemAssociation()) { const keySystemIdentifiedValidationResult = Validator.isNotEmptyString(dto.itemHash.props.key_system_identifier) if (keySystemIdentifiedValidationResult.isFailed()) { @@ -123,32 +142,9 @@ export class SaveNewItem implements UseCaseInterface { if (keySystemAssociationOrError.isFailed()) { return Result.fail(keySystemAssociationOrError.getError()) } - keySystemAssociation = keySystemAssociationOrError.getValue() + newItem.setKeySystemAssociation(keySystemAssociationOrError.getValue()) } - const itemOrError = Item.create( - { - updatedWithSession, - content: dto.itemHash.props.content ?? null, - userUuid, - contentType, - encItemKey: dto.itemHash.props.enc_item_key ?? null, - authHash: dto.itemHash.props.auth_hash ?? null, - itemsKeyId: dto.itemHash.props.items_key_id ?? null, - duplicateOf, - deleted: dto.itemHash.props.deleted ?? false, - dates, - timestamps, - keySystemAssociation, - sharedVaultAssociation, - }, - new UniqueEntityId(uuid.value), - ) - if (itemOrError.isFailed()) { - return Result.fail(itemOrError.getError()) - } - const newItem = itemOrError.getValue() - await this.itemRepository.save(newItem) if (contentType.value !== null && [ContentType.TYPES.Note, ContentType.TYPES.File].includes(contentType.value)) { diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts index 0bda2ae60..a3f224a00 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts @@ -177,7 +177,7 @@ describe('SyncItems', () => { }) }) - it('should sync items and return items keys on top for first sync', async () => { + it('should sync items and return items keys on top for first sync that is not a shared vault exclusive sync', async () => { const result = await createUseCase().execute({ userUuid: '1-2-3', itemHashes: [itemHash], @@ -202,6 +202,32 @@ describe('SyncItems', () => { }) }) + it('should sync items and not return items keys on top for first sync that is a shared vault exclusive sync', async () => { + const result = await createUseCase().execute({ + userUuid: '1-2-3', + itemHashes: [itemHash], + computeIntegrityHash: false, + limit: 10, + readOnlyAccess: false, + sessionUuid: '2-3-4', + contentType: 'Note', + apiVersion: ApiVersion.v20200115, + snjsVersion: '1.2.3', + sharedVaultUuids: ['00000000-0000-0000-0000-000000000000'], + }) + expect(result.getValue()).toEqual({ + conflicts: [], + cursorToken: 'asdzxc', + retrievedItems: [item1], + savedItems: [item2], + syncToken: 'qwerty', + sharedVaults: [], + sharedVaultInvites: [], + notifications: [], + messages: [], + }) + }) + it('should sync items and return filtered out sync conflicts for consecutive sync operations', async () => { getItemsUseCase.execute = jest.fn().mockReturnValue( Result.ok({ diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts index 2cd4a415a..b5ea29f29 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts @@ -50,7 +50,8 @@ export class SyncItems implements UseCaseInterface { const saveItemsResult = saveItemsResultOrError.getValue() let retrievedItems = this.filterOutSyncConflictsForConsecutiveSyncs(getItemsResult.items, saveItemsResult.conflicts) - if (this.isFirstSync(dto)) { + const isSharedVaultExclusiveSync = dto.sharedVaultUuids && dto.sharedVaultUuids.length > 0 + if (this.isFirstSync(dto) && !isSharedVaultExclusiveSync) { retrievedItems = await this.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems) } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts index 9c899c406..5ca8ae445 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.spec.ts @@ -350,12 +350,14 @@ describe('UpdateExistingItem', () => { shared_vault_uuid: '00000000-0000-0000-0000-000000000000', }).getValue() - item1.props.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() + item1.setSharedVaultAssociation( + 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(), + ) const idBefore = item1.props.sharedVaultAssociation?.id.toString() const result = await useCase.execute({ @@ -368,7 +370,7 @@ describe('UpdateExistingItem', () => { expect(result.isFailed()).toBeFalsy() expect(item1.props.sharedVaultAssociation).not.toBeUndefined() - expect(item1.props.sharedVaultAssociation.id.toString()).toEqual(idBefore) + expect((item1.props.sharedVaultAssociation as SharedVaultAssociation).id.toString()).toEqual(idBefore) }) it('should return error if shared vault association could not be created', async () => { @@ -528,11 +530,13 @@ describe('UpdateExistingItem', () => { key_system_identifier: '00000000-0000-0000-0000-000000000000', }).getValue() - item1.props.keySystemAssociation = KeySystemAssociation.create({ - itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), - keySystemIdentifier: '00000000-0000-0000-0000-000000000000', - timestamps: Timestamps.create(123, 123).getValue(), - }).getValue() + item1.setKeySystemAssociation( + KeySystemAssociation.create({ + itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + keySystemIdentifier: '00000000-0000-0000-0000-000000000000', + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue(), + ) const idBefore = item1.props.keySystemAssociation?.id.toString() const result = await useCase.execute({ @@ -545,7 +549,7 @@ describe('UpdateExistingItem', () => { expect(result.isFailed()).toBeFalsy() expect(item1.props.keySystemAssociation).not.toBeUndefined() - expect(item1.props.keySystemAssociation.id.toString()).toEqual(idBefore) + expect((item1.props.keySystemAssociation as KeySystemAssociation).id.toString()).toEqual(idBefore) }) it('should return error if key system identifier is invalid', async () => { diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts index 86abaac19..9cda3f2f6 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts @@ -147,7 +147,7 @@ export class UpdateExistingItem implements UseCaseInterface { return Result.fail(sharedVaultAssociationOrError.getError()) } - dto.existingItem.props.sharedVaultAssociation = sharedVaultAssociationOrError.getValue() + dto.existingItem.setSharedVaultAssociation(sharedVaultAssociationOrError.getValue()) const sharedVaultOperationOrError = await this.determineSharedVaultOperationOnItem.execute({ existingItem: dto.existingItem, @@ -158,31 +158,39 @@ export class UpdateExistingItem implements UseCaseInterface { return Result.fail(sharedVaultOperationOrError.getError()) } sharedVaultOperation = sharedVaultOperationOrError.getValue() + } else { + dto.existingItem.unsetSharedVaultAssociation() } - if ( - dto.itemHash.hasDedicatedKeySystemAssociation() && - !dto.existingItem.isAssociatedWithKeySystem(dto.itemHash.props.key_system_identifier as string) - ) { + if (dto.itemHash.hasDedicatedKeySystemAssociation()) { const keySystemIdentifiedValidationResult = Validator.isNotEmptyString(dto.itemHash.props.key_system_identifier) if (keySystemIdentifiedValidationResult.isFailed()) { return Result.fail(keySystemIdentifiedValidationResult.getError()) } const keySystemIdentifier = dto.itemHash.props.key_system_identifier as string - const keySystemAssociationOrError = KeySystemAssociation.create({ - itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(), - timestamps: Timestamps.create( - this.timer.getTimestampInMicroseconds(), - this.timer.getTimestampInMicroseconds(), - ).getValue(), - keySystemIdentifier, - }) + const keySystemAssociationOrError = KeySystemAssociation.create( + { + itemUuid: Uuid.create(dto.existingItem.id.toString()).getValue(), + timestamps: Timestamps.create( + this.timer.getTimestampInMicroseconds(), + this.timer.getTimestampInMicroseconds(), + ).getValue(), + keySystemIdentifier, + }, + new UniqueEntityId( + dto.existingItem.props.keySystemAssociation + ? dto.existingItem.props.keySystemAssociation.id.toString() + : undefined, + ), + ) if (keySystemAssociationOrError.isFailed()) { return Result.fail(keySystemAssociationOrError.getError()) } - dto.existingItem.props.keySystemAssociation = keySystemAssociationOrError.getValue() + dto.existingItem.setKeySystemAssociation(keySystemAssociationOrError.getValue()) + } else { + dto.existingItem.unsetKeySystemAssociation() } if (dto.itemHash.props.deleted === true) { diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts index cd4a34113..6174e9c0a 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts @@ -1,6 +1,6 @@ import { ReadStream } from 'fs' import { Repository, SelectQueryBuilder } from 'typeorm' -import { MapperInterface, Uuid } from '@standardnotes/domain-core' +import { Change, MapperInterface, Uuid } from '@standardnotes/domain-core' import { Item } from '../../Domain/Item/Item' import { ItemQuery } from '../../Domain/Item/ItemQuery' @@ -10,6 +10,8 @@ import { TypeORMItem } from './TypeORMItem' import { KeySystemAssociationRepositoryInterface } from '../../Domain/KeySystem/KeySystemAssociationRepositoryInterface' import { SharedVaultAssociationRepositoryInterface } from '../../Domain/SharedVault/SharedVaultAssociationRepositoryInterface' import { TypeORMSharedVaultAssociation } from './TypeORMSharedVaultAssociation' +import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation' +import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation' export class TypeORMItemRepository implements ItemRepositoryInterface { constructor( @@ -24,13 +26,7 @@ export class TypeORMItemRepository implements ItemRepositoryInterface { await this.ormRepository.save(persistence) - if (item.props.sharedVaultAssociation) { - await this.sharedVaultAssociationRepository.save(item.props.sharedVaultAssociation) - } - - if (item.props.keySystemAssociation) { - await this.keySystemAssociationRepository.save(item.props.keySystemAssociation) - } + await this.persistAssociationChanges(item) } async remove(item: Item): Promise { @@ -273,4 +269,27 @@ export class TypeORMItemRepository implements ItemRepositoryInterface { item.props.sharedVaultAssociation = sharedVaultAssociation } } + + private async persistAssociationChanges(item: Item): Promise { + for (const change of item.getChanges()) { + if (change.props.changeData instanceof SharedVaultAssociation) { + if ([Change.TYPES.Add, Change.TYPES.Modify].includes(change.props.changeType)) { + await this.sharedVaultAssociationRepository.save(change.props.changeData) + } + if (change.props.changeType === Change.TYPES.Remove) { + await this.sharedVaultAssociationRepository.remove(change.props.changeData) + } + } + if (change.props.changeData instanceof KeySystemAssociation) { + if ([Change.TYPES.Add, Change.TYPES.Modify].includes(change.props.changeType)) { + await this.keySystemAssociationRepository.save(change.props.changeData) + } + if (change.props.changeType === Change.TYPES.Remove) { + await this.keySystemAssociationRepository.remove(change.props.changeData) + } + } + } + + item.flushChanges() + } }