Browse Source

feat(syncing-server): persisting shared vault and key system associations (#660)

Karol Sójko 1 year ago
parent
commit
479d20e76f
44 changed files with 662 additions and 119 deletions
  1. 25 0
      packages/syncing-server/migrations/mysql/1689671563305-add-shared-vault-and-key-system-associations.ts
  2. 35 0
      packages/syncing-server/migrations/sqlite/1689672099829-add-shared-vault-and-key-system-associations.ts
  3. 45 0
      packages/syncing-server/src/Bootstrap/Container.ts
  4. 3 1
      packages/syncing-server/src/Bootstrap/DataSource.ts
  5. 6 0
      packages/syncing-server/src/Bootstrap/Types.ts
  6. 2 2
      packages/syncing-server/src/Domain/Item/Item.ts
  7. 8 0
      packages/syncing-server/src/Domain/Item/ItemHash.ts
  8. 5 0
      packages/syncing-server/src/Domain/Item/ItemProps.ts
  9. 7 0
      packages/syncing-server/src/Domain/KeySystem/KeySystemAssocationProps.ts
  10. 16 0
      packages/syncing-server/src/Domain/KeySystem/KeySystemAssociation.spec.ts
  11. 13 0
      packages/syncing-server/src/Domain/KeySystem/KeySystemAssociation.ts
  12. 6 0
      packages/syncing-server/src/Domain/KeySystem/KeySystemAssociationRepositoryInterface.ts
  13. 0 18
      packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItem.spec.ts
  14. 0 13
      packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItem.ts
  15. 0 9
      packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItemProps.ts
  16. 0 8
      packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItemRepositoryInterface.ts
  17. 1 2
      packages/syncing-server/src/Domain/SharedVault/SharedVault.spec.ts
  18. 2 2
      packages/syncing-server/src/Domain/SharedVault/SharedVault.ts
  19. 17 0
      packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociation.spec.ts
  20. 13 0
      packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociation.ts
  21. 8 0
      packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociationProps.ts
  22. 6 0
      packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociationRepositoryInterface.ts
  23. 0 3
      packages/syncing-server/src/Domain/SharedVault/SharedVaultProps.ts
  24. 0 1
      packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVault.ts
  25. 0 1
      packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts
  26. 0 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts
  27. 0 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts
  28. 0 1
      packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts
  29. 0 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts
  30. 0 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts
  31. 148 1
      packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.spec.ts
  32. 56 1
      packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts
  33. 24 1
      packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts
  34. 33 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMKeySystemAssociation.ts
  35. 21 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMKeySystemAssociationRepository.ts
  36. 5 10
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultAssociation.ts
  37. 21 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultAssociationRepository.ts
  38. 0 30
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultItemRepository.ts
  39. 0 6
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultRepository.ts
  40. 10 0
      packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts
  41. 4 0
      packages/syncing-server/src/Mapping/Http/ItemHttpRepresentation.ts
  42. 57 0
      packages/syncing-server/src/Mapping/Persistence/KeySystemAssociationPersistenceMapper.ts
  43. 65 0
      packages/syncing-server/src/Mapping/Persistence/SharedVaultAssociationPersistenceMapper.ts
  44. 0 1
      packages/syncing-server/src/Mapping/Persistence/SharedVaultPersistenceMapper.ts

+ 25 - 0
packages/syncing-server/migrations/mysql/1689671563305-add-shared-vault-and-key-system-associations.ts

@@ -0,0 +1,25 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddSharedVaultAndKeySystemAssociations1689671563305 implements MigrationInterface {
+  name = 'AddSharedVaultAndKeySystemAssociations1689671563305'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE `shared_vault_associations` (`uuid` varchar(36) NOT NULL, `shared_vault_uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `last_edited_by` varchar(36) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `shared_vault_uuid_on_shared_vault_associations` (`shared_vault_uuid`), INDEX `item_uuid_on_shared_vault_associations` (`item_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
+    )
+    await queryRunner.query(
+      'CREATE TABLE `key_system_associations` (`uuid` varchar(36) NOT NULL, `key_system_uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `key_system_uuid_on_key_system_associations` (`key_system_uuid`), INDEX `item_uuid_on_key_system_associations` (`item_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
+    )
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `item_uuid_on_key_system_associations` ON `key_system_associations`')
+    await queryRunner.query('DROP INDEX `key_system_uuid_on_key_system_associations` ON `key_system_associations`')
+    await queryRunner.query('DROP TABLE `key_system_associations`')
+    await queryRunner.query('DROP INDEX `item_uuid_on_shared_vault_associations` ON `shared_vault_associations`')
+    await queryRunner.query(
+      'DROP INDEX `shared_vault_uuid_on_shared_vault_associations` ON `shared_vault_associations`',
+    )
+    await queryRunner.query('DROP TABLE `shared_vault_associations`')
+  }
+}

+ 35 - 0
packages/syncing-server/migrations/sqlite/1689672099829-add-shared-vault-and-key-system-associations.ts

@@ -0,0 +1,35 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddSharedVaultAndKeySystemAssociations1689672099829 implements MigrationInterface {
+  name = 'AddSharedVaultAndKeySystemAssociations1689672099829'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE "shared_vault_associations" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "item_uuid" varchar(36) NOT NULL, "last_edited_by" varchar(36) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
+    )
+    await queryRunner.query(
+      'CREATE INDEX "shared_vault_uuid_on_shared_vault_associations" ON "shared_vault_associations" ("shared_vault_uuid") ',
+    )
+    await queryRunner.query(
+      'CREATE INDEX "item_uuid_on_shared_vault_associations" ON "shared_vault_associations" ("item_uuid") ',
+    )
+    await queryRunner.query(
+      'CREATE TABLE "key_system_associations" ("uuid" varchar PRIMARY KEY NOT NULL, "key_system_uuid" varchar(36) NOT NULL, "item_uuid" varchar(36) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
+    )
+    await queryRunner.query(
+      'CREATE INDEX "key_system_uuid_on_key_system_associations" ON "key_system_associations" ("key_system_uuid") ',
+    )
+    await queryRunner.query(
+      'CREATE INDEX "item_uuid_on_key_system_associations" ON "key_system_associations" ("item_uuid") ',
+    )
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX "item_uuid_on_key_system_associations"')
+    await queryRunner.query('DROP INDEX "key_system_uuid_on_key_system_associations"')
+    await queryRunner.query('DROP TABLE "key_system_associations"')
+    await queryRunner.query('DROP INDEX "item_uuid_on_shared_vault_associations"')
+    await queryRunner.query('DROP INDEX "shared_vault_uuid_on_shared_vault_associations"')
+    await queryRunner.query('DROP TABLE "shared_vault_associations"')
+  }
+}

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

@@ -78,6 +78,16 @@ import { SaveItems } from '../Domain/UseCase/Syncing/SaveItems/SaveItems'
 import { ItemHashHttpMapper } from '../Mapping/Http/ItemHashHttpMapper'
 import { ItemHashHttpMapper } from '../Mapping/Http/ItemHashHttpMapper'
 import { ItemHash } from '../Domain/Item/ItemHash'
 import { ItemHash } from '../Domain/Item/ItemHash'
 import { ItemHashHttpRepresentation } from '../Mapping/Http/ItemHashHttpRepresentation'
 import { ItemHashHttpRepresentation } from '../Mapping/Http/ItemHashHttpRepresentation'
+import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
+import { SharedVaultAssociation } from '../Domain/SharedVault/SharedVaultAssociation'
+import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
+import { SharedVaultAssociationPersistenceMapper } from '../Mapping/Persistence/SharedVaultAssociationPersistenceMapper'
+import { TypeORMKeySystemAssociationRepository } from '../Infra/TypeORM/TypeORMKeySystemAssociationRepository'
+import { SharedVaultAssociationRepositoryInterface } from '../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
+import { TypeORMSharedVaultAssociationRepository } from '../Infra/TypeORM/TypeORMSharedVaultAssociationRepository'
+import { KeySystemAssociation } from '../Domain/KeySystem/KeySystemAssociation'
+import { KeySystemAssociationRepositoryInterface } from '../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
+import { KeySystemAssociationPersistenceMapper } from '../Mapping/Persistence/KeySystemAssociationPersistenceMapper'
 
 
 export class ContainerConfigLoader {
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -232,19 +242,54 @@ export class ContainerConfigLoader {
     container
     container
       .bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
       .bind<MapperInterface<Item, ItemBackupRepresentation>>(TYPES.Sync_ItemBackupMapper)
       .toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
       .toConstantValue(new ItemBackupMapper(container.get(TYPES.Sync_Timer)))
+    container
+      .bind<MapperInterface<KeySystemAssociation, TypeORMKeySystemAssociation>>(
+        TYPES.Sync_KeySystemAssociationPersistenceMapper,
+      )
+      .toConstantValue(new KeySystemAssociationPersistenceMapper())
+    container
+      .bind<MapperInterface<SharedVaultAssociation, TypeORMSharedVaultAssociation>>(
+        TYPES.Sync_SharedVaultAssociationPersistenceMapper,
+      )
+      .toConstantValue(new SharedVaultAssociationPersistenceMapper())
 
 
     // ORM
     // ORM
     container
     container
       .bind<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository)
       .bind<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository)
       .toDynamicValue(() => appDataSource.getRepository(TypeORMItem))
       .toDynamicValue(() => appDataSource.getRepository(TypeORMItem))
+    container
+      .bind<Repository<TypeORMSharedVaultAssociation>>(TYPES.Sync_ORMSharedVaultAssociationRepository)
+      .toConstantValue(appDataSource.getRepository(TypeORMSharedVaultAssociation))
+    container
+      .bind<Repository<TypeORMKeySystemAssociation>>(TYPES.Sync_ORMKeySystemAssociationRepository)
+      .toConstantValue(appDataSource.getRepository(TypeORMKeySystemAssociation))
 
 
     // Repositories
     // Repositories
+    container
+      .bind<KeySystemAssociationRepositoryInterface>(TYPES.Sync_KeySystemAssociationRepository)
+      .toConstantValue(
+        new TypeORMKeySystemAssociationRepository(
+          container.get(TYPES.Sync_ORMKeySystemAssociationRepository),
+          container.get(TYPES.Sync_KeySystemAssociationPersistenceMapper),
+        ),
+      )
+    container
+      .bind<SharedVaultAssociationRepositoryInterface>(TYPES.Sync_SharedVaultAssociationRepository)
+      .toConstantValue(
+        new TypeORMSharedVaultAssociationRepository(
+          container.get(TYPES.Sync_ORMSharedVaultAssociationRepository),
+          container.get(TYPES.Sync_SharedVaultAssociationPersistenceMapper),
+        ),
+      )
+
     container
     container
       .bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository)
       .bind<ItemRepositoryInterface>(TYPES.Sync_ItemRepository)
       .toConstantValue(
       .toConstantValue(
         new TypeORMItemRepository(
         new TypeORMItemRepository(
           container.get(TYPES.Sync_ORMItemRepository),
           container.get(TYPES.Sync_ORMItemRepository),
           container.get(TYPES.Sync_ItemPersistenceMapper),
           container.get(TYPES.Sync_ItemPersistenceMapper),
+          container.get(TYPES.Sync_KeySystemAssociationRepository),
+          container.get(TYPES.Sync_SharedVaultAssociationRepository),
         ),
         ),
       )
       )
 
 

+ 3 - 1
packages/syncing-server/src/Bootstrap/DataSource.ts

@@ -4,6 +4,8 @@ import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
 import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
 import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
 import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
 import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
+import { TypeORMSharedVaultAssociation } from '../Infra/TypeORM/TypeORMSharedVaultAssociation'
+import { TypeORMKeySystemAssociation } from '../Infra/TypeORM/TypeORMKeySystemAssociation'
 
 
 export class AppDataSource {
 export class AppDataSource {
   private _dataSource: DataSource | undefined
   private _dataSource: DataSource | undefined
@@ -33,7 +35,7 @@ export class AppDataSource {
 
 
     const commonDataSourceOptions = {
     const commonDataSourceOptions = {
       maxQueryExecutionTime,
       maxQueryExecutionTime,
-      entities: [TypeORMItem, TypeORMNotification],
+      entities: [TypeORMItem, TypeORMNotification, TypeORMSharedVaultAssociation, TypeORMKeySystemAssociation],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrationsRun: true,
       migrationsRun: true,
       logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
       logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',

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

@@ -8,8 +8,12 @@ const TYPES = {
   Sync_Env: Symbol.for('Sync_Env'),
   Sync_Env: Symbol.for('Sync_Env'),
   // Repositories
   // Repositories
   Sync_ItemRepository: Symbol.for('Sync_ItemRepository'),
   Sync_ItemRepository: Symbol.for('Sync_ItemRepository'),
+  Sync_KeySystemAssociationRepository: Symbol.for('Sync_KeySystemAssociationRepository'),
+  Sync_SharedVaultAssociationRepository: Symbol.for('Sync_SharedVaultAssociationRepository'),
   // ORM
   // ORM
   Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
   Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
+  Sync_ORMSharedVaultAssociationRepository: Symbol.for('Sync_ORMSharedVaultAssociationRepository'),
+  Sync_ORMKeySystemAssociationRepository: Symbol.for('Sync_ORMKeySystemAssociationRepository'),
   // Middleware
   // Middleware
   Sync_AuthMiddleware: Symbol.for('Sync_AuthMiddleware'),
   Sync_AuthMiddleware: Symbol.for('Sync_AuthMiddleware'),
   // env vars
   // env vars
@@ -95,6 +99,8 @@ const TYPES = {
   Sync_SavedItemHttpMapper: Symbol.for('Sync_SavedItemHttpMapper'),
   Sync_SavedItemHttpMapper: Symbol.for('Sync_SavedItemHttpMapper'),
   Sync_ItemConflictHttpMapper: Symbol.for('Sync_ItemConflictHttpMapper'),
   Sync_ItemConflictHttpMapper: Symbol.for('Sync_ItemConflictHttpMapper'),
   Sync_ItemBackupMapper: Symbol.for('Sync_ItemBackupMapper'),
   Sync_ItemBackupMapper: Symbol.for('Sync_ItemBackupMapper'),
+  Sync_KeySystemAssociationPersistenceMapper: Symbol.for('Sync_KeySystemAssociationPersistenceMapper'),
+  Sync_SharedVaultAssociationPersistenceMapper: Symbol.for('Sync_SharedVaultAssociationPersistenceMapper'),
 }
 }
 
 
 export default TYPES
 export default TYPES

+ 2 - 2
packages/syncing-server/src/Domain/Item/Item.ts

@@ -1,8 +1,8 @@
-import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
+import { Aggregate, Result, UniqueEntityId } from '@standardnotes/domain-core'
 
 
 import { ItemProps } from './ItemProps'
 import { ItemProps } from './ItemProps'
 
 
-export class Item extends Entity<ItemProps> {
+export class Item extends Aggregate<ItemProps> {
   private constructor(props: ItemProps, id?: UniqueEntityId) {
   private constructor(props: ItemProps, id?: UniqueEntityId) {
     super(props, id)
     super(props, id)
   }
   }

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

@@ -10,4 +10,12 @@ export class ItemHash extends ValueObject<ItemHashProps> {
   static create(props: ItemHashProps): Result<ItemHash> {
   static create(props: ItemHashProps): Result<ItemHash> {
     return Result.ok<ItemHash>(new ItemHash(props))
     return Result.ok<ItemHash>(new ItemHash(props))
   }
   }
+
+  representsASharedVaultItem(): boolean {
+    return this.props.shared_vault_uuid !== null
+  }
+
+  hasDedicatedKeySystemAssociation(): boolean {
+    return this.props.key_system_identifier !== null
+  }
 }
 }

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

@@ -1,5 +1,8 @@
 import { ContentType, Dates, Timestamps, Uuid } from '@standardnotes/domain-core'
 import { ContentType, Dates, Timestamps, Uuid } from '@standardnotes/domain-core'
 
 
+import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation'
+import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
+
 export interface ItemProps {
 export interface ItemProps {
   duplicateOf: Uuid | null
   duplicateOf: Uuid | null
   itemsKeyId: string | null
   itemsKeyId: string | null
@@ -13,4 +16,6 @@ export interface ItemProps {
   dates: Dates
   dates: Dates
   timestamps: Timestamps
   timestamps: Timestamps
   contentSize?: number
   contentSize?: number
+  sharedVaultAssociation?: SharedVaultAssociation
+  keySystemAssociation?: KeySystemAssociation
 }
 }

+ 7 - 0
packages/syncing-server/src/Domain/KeySystem/KeySystemAssocationProps.ts

@@ -0,0 +1,7 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+export interface KeySystemAssociationProps {
+  itemUuid: Uuid
+  keySystemUuid: Uuid
+  timestamps: Timestamps
+}

+ 16 - 0
packages/syncing-server/src/Domain/KeySystem/KeySystemAssociation.spec.ts

@@ -0,0 +1,16 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { KeySystemAssociation } from './KeySystemAssociation'
+
+describe('KeySystemAssociation', () => {
+  it('should create an entity', () => {
+    const entityOrError = KeySystemAssociation.create({
+      timestamps: Timestamps.create(123456789, 123456789).getValue(),
+      itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      keySystemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+    expect(entityOrError.getValue().id).not.toBeNull()
+  })
+})

+ 13 - 0
packages/syncing-server/src/Domain/KeySystem/KeySystemAssociation.ts

@@ -0,0 +1,13 @@
+import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
+
+import { KeySystemAssociationProps } from './KeySystemAssocationProps'
+
+export class KeySystemAssociation extends Entity<KeySystemAssociationProps> {
+  private constructor(props: KeySystemAssociationProps, id?: UniqueEntityId) {
+    super(props, id)
+  }
+
+  static create(props: KeySystemAssociationProps, id?: UniqueEntityId): Result<KeySystemAssociation> {
+    return Result.ok<KeySystemAssociation>(new KeySystemAssociation(props, id))
+  }
+}

+ 6 - 0
packages/syncing-server/src/Domain/KeySystem/KeySystemAssociationRepositoryInterface.ts

@@ -0,0 +1,6 @@
+import { KeySystemAssociation } from './KeySystemAssociation'
+
+export interface KeySystemAssociationRepositoryInterface {
+  save(keySystem: KeySystemAssociation): Promise<void>
+  remove(keySystem: KeySystemAssociation): Promise<void>
+}

+ 0 - 18
packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItem.spec.ts

@@ -1,18 +0,0 @@
-import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
-
-import { SharedVaultItem } from './SharedVaultItem'
-
-describe('SharedVaultItem', () => {
-  it('should create an entity', () => {
-    const entityOrError = SharedVaultItem.create({
-      itemId: new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
-      sharedVaultId: new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
-      keySystemIdentifier: 'key-system-identifier',
-      lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
-      timestamps: Timestamps.create(123456789, 123456789).getValue(),
-    })
-
-    expect(entityOrError.isFailed()).toBeFalsy()
-    expect(entityOrError.getValue().id).not.toBeNull()
-  })
-})

+ 0 - 13
packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItem.ts

@@ -1,13 +0,0 @@
-import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
-
-import { SharedVaultItemProps } from './SharedVaultItemProps'
-
-export class SharedVaultItem extends Entity<SharedVaultItemProps> {
-  private constructor(props: SharedVaultItemProps, id?: UniqueEntityId) {
-    super(props, id)
-  }
-
-  static create(props: SharedVaultItemProps, id?: UniqueEntityId): Result<SharedVaultItem> {
-    return Result.ok<SharedVaultItem>(new SharedVaultItem(props, id))
-  }
-}

+ 0 - 9
packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItemProps.ts

@@ -1,9 +0,0 @@
-import { Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
-
-export interface SharedVaultItemProps {
-  sharedVaultId: UniqueEntityId
-  itemId: UniqueEntityId
-  keySystemIdentifier: string
-  lastEditedBy: Uuid
-  timestamps: Timestamps
-}

+ 0 - 8
packages/syncing-server/src/Domain/SharedVault/Item/SharedVaultItemRepositoryInterface.ts

@@ -1,8 +0,0 @@
-import { UniqueEntityId } from '@standardnotes/domain-core'
-
-import { SharedVaultItem } from './SharedVaultItem'
-
-export interface SharedVaultItemRepositoryInterface {
-  save(sharedVaultItem: SharedVaultItem): Promise<void>
-  findBySharedVaultId(sharedVaultId: UniqueEntityId): Promise<SharedVaultItem[]>
-}

+ 1 - 2
packages/syncing-server/src/Domain/SharedVault/SharedVault.spec.ts

@@ -3,13 +3,12 @@ import { Timestamps, Uuid } from '@standardnotes/domain-core'
 import { SharedVault } from './SharedVault'
 import { SharedVault } from './SharedVault'
 
 
 describe('SharedVault', () => {
 describe('SharedVault', () => {
-  it('should create an aggregate', () => {
+  it('should create an entity', () => {
     const entityOrError = SharedVault.create({
     const entityOrError = SharedVault.create({
       fileUploadBytesLimit: 1_000_000,
       fileUploadBytesLimit: 1_000_000,
       fileUploadBytesUsed: 0,
       fileUploadBytesUsed: 0,
       timestamps: Timestamps.create(123456789, 123456789).getValue(),
       timestamps: Timestamps.create(123456789, 123456789).getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
-      sharedVaultItems: [],
     })
     })
 
 
     expect(entityOrError.isFailed()).toBeFalsy()
     expect(entityOrError.isFailed()).toBeFalsy()

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

@@ -1,8 +1,8 @@
-import { Aggregate, Result, UniqueEntityId } from '@standardnotes/domain-core'
+import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
 
 
 import { SharedVaultProps } from './SharedVaultProps'
 import { SharedVaultProps } from './SharedVaultProps'
 
 
-export class SharedVault extends Aggregate<SharedVaultProps> {
+export class SharedVault extends Entity<SharedVaultProps> {
   private constructor(props: SharedVaultProps, id?: UniqueEntityId) {
   private constructor(props: SharedVaultProps, id?: UniqueEntityId) {
     super(props, id)
     super(props, id)
   }
   }

+ 17 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociation.spec.ts

@@ -0,0 +1,17 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultAssociation } from './SharedVaultAssociation'
+
+describe('SharedVaultAssociation', () => {
+  it('should create an entity', () => {
+    const entityOrError = SharedVaultAssociation.create({
+      timestamps: Timestamps.create(123456789, 123456789).getValue(),
+      itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+    expect(entityOrError.getValue().id).not.toBeNull()
+  })
+})

+ 13 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociation.ts

@@ -0,0 +1,13 @@
+import { Aggregate, Result, UniqueEntityId } from '@standardnotes/domain-core'
+
+import { SharedVaultAssociationProps } from './SharedVaultAssociationProps'
+
+export class SharedVaultAssociation extends Aggregate<SharedVaultAssociationProps> {
+  private constructor(props: SharedVaultAssociationProps, id?: UniqueEntityId) {
+    super(props, id)
+  }
+
+  static create(props: SharedVaultAssociationProps, id?: UniqueEntityId): Result<SharedVaultAssociation> {
+    return Result.ok<SharedVaultAssociation>(new SharedVaultAssociation(props, id))
+  }
+}

+ 8 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociationProps.ts

@@ -0,0 +1,8 @@
+import { Timestamps, Uuid } from '@standardnotes/domain-core'
+
+export interface SharedVaultAssociationProps {
+  lastEditedBy: Uuid
+  sharedVaultUuid: Uuid
+  itemUuid: Uuid
+  timestamps: Timestamps
+}

+ 6 - 0
packages/syncing-server/src/Domain/SharedVault/SharedVaultAssociationRepositoryInterface.ts

@@ -0,0 +1,6 @@
+import { SharedVaultAssociation } from './SharedVaultAssociation'
+
+export interface SharedVaultAssociationRepositoryInterface {
+  save(sharedVaultAssociation: SharedVaultAssociation): Promise<void>
+  remove(sharedVaultAssociation: SharedVaultAssociation): Promise<void>
+}

+ 0 - 3
packages/syncing-server/src/Domain/SharedVault/SharedVaultProps.ts

@@ -1,11 +1,8 @@
 import { Uuid, Timestamps } from '@standardnotes/domain-core'
 import { Uuid, Timestamps } from '@standardnotes/domain-core'
 
 
-import { SharedVaultItem } from './Item/SharedVaultItem'
-
 export interface SharedVaultProps {
 export interface SharedVaultProps {
   userUuid: Uuid
   userUuid: Uuid
   fileUploadBytesUsed: number
   fileUploadBytesUsed: number
   fileUploadBytesLimit: number
   fileUploadBytesLimit: number
   timestamps: Timestamps
   timestamps: Timestamps
-  sharedVaultItems: SharedVaultItem[]
 }
 }

+ 0 - 1
packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVault.ts

@@ -32,7 +32,6 @@ export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResu
       fileUploadBytesUsed: 0,
       fileUploadBytesUsed: 0,
       userUuid,
       userUuid,
       timestamps,
       timestamps,
-      sharedVaultItems: [],
     })
     })
     if (sharedVaultOrError.isFailed()) {
     if (sharedVaultOrError.isFailed()) {
       return Result.fail(sharedVaultOrError.getError())
       return Result.fail(sharedVaultOrError.getError())

+ 0 - 1
packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts

@@ -24,7 +24,6 @@ describe('CreateSharedVaultFileValetToken', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
 
 
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>

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

@@ -31,7 +31,6 @@ describe('DeleteSharedVault', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
@@ -116,7 +115,6 @@ describe('DeleteSharedVault', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     const useCase = createUseCase()
     const useCase = createUseCase()

+ 0 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts

@@ -20,7 +20,6 @@ describe('GetSharedVaultUsers', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
 
 
     sharedVaultUser = SharedVaultUser.create({
     sharedVaultUser = SharedVaultUser.create({
@@ -67,7 +66,6 @@ describe('GetSharedVaultUsers', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
 
 

+ 0 - 1
packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts

@@ -29,7 +29,6 @@ describe('GetSharedVaults', () => {
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       fileUploadBytesLimit: 123,
       fileUploadBytesLimit: 123,
       fileUploadBytesUsed: 123,
       fileUploadBytesUsed: 123,
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository.findByUuids = jest.fn().mockResolvedValue([sharedVault])
     sharedVaultRepository.findByUuids = jest.fn().mockResolvedValue([sharedVault])

+ 0 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts

@@ -21,7 +21,6 @@ describe('InviteUserToSharedVault', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
@@ -153,7 +152,6 @@ describe('InviteUserToSharedVault', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('10000000-0000-0000-0000-000000000000').getValue(),
       userUuid: Uuid.create('10000000-0000-0000-0000-000000000000').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
 
 

+ 0 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts

@@ -24,7 +24,6 @@ describe('RemoveUserFromSharedVault', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
@@ -89,7 +88,6 @@ describe('RemoveUserFromSharedVault', () => {
       fileUploadBytesUsed: 2,
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000002').getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
       timestamps: Timestamps.create(123, 123).getValue(),
-      sharedVaultItems: [],
     }).getValue()
     }).getValue()
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
     sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
 
 

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

@@ -6,6 +6,8 @@ import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryIn
 import { ItemHash } from '../../../Item/ItemHash'
 import { ItemHash } from '../../../Item/ItemHash'
 import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
 import { ContentType, Dates, Result, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
 import { Item } from '../../../Item/Item'
 import { Item } from '../../../Item/Item'
+import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
+import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
 
 
 describe('SaveNewItem', () => {
 describe('SaveNewItem', () => {
   let itemRepository: ItemRepositoryInterface
   let itemRepository: ItemRepositoryInterface
@@ -38,7 +40,7 @@ describe('SaveNewItem', () => {
     ).getValue()
     ).getValue()
 
 
     itemHash1 = ItemHash.create({
     itemHash1 = ItemHash.create({
-      uuid: '1-2-3',
+      uuid: '00000000-0000-0000-0000-000000000000',
       user_uuid: '00000000-0000-0000-0000-000000000000',
       user_uuid: '00000000-0000-0000-0000-000000000000',
       key_system_identifier: null,
       key_system_identifier: null,
       shared_vault_uuid: null,
       shared_vault_uuid: null,
@@ -282,4 +284,149 @@ describe('SaveNewItem', () => {
 
 
     mock.mockRestore()
     mock.mockRestore()
   })
   })
+
+  it('returns a failure if the item hash has an invalid uuid', async () => {
+    const useCase = createUseCase()
+
+    itemHash1 = ItemHash.create({
+      ...itemHash1.props,
+      uuid: '1-2-3',
+    }).getValue()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-00000000000',
+      sessionUuid: '00000000-0000-0000-0000-000000000001',
+      itemHash: itemHash1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  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()
+
+      itemHash1 = ItemHash.create({
+        ...itemHash1.props,
+        shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
+      }).getValue()
+
+      const result = await useCase.execute({
+        userUuid: '00000000-0000-0000-0000-000000000000',
+        sessionUuid: '00000000-0000-0000-0000-000000000001',
+        itemHash: itemHash1,
+      })
+
+      expect(result.isFailed()).toBeFalsy()
+      expect(result.getValue().props.sharedVaultAssociation?.props.lastEditedBy.value).toEqual(
+        '00000000-0000-0000-0000-000000000000',
+      )
+      expect(itemRepository.save).toHaveBeenCalled()
+    })
+
+    it('should return a failure if it fails to create a shared vault association', async () => {
+      const mock = jest.spyOn(SharedVaultAssociation, 'create')
+      mock.mockImplementation(() => {
+        return Result.fail('Oops')
+      })
+
+      const useCase = createUseCase()
+
+      itemHash1 = ItemHash.create({
+        ...itemHash1.props,
+        shared_vault_uuid: '00000000-0000-0000-0000-000000000001',
+      }).getValue()
+
+      const result = await useCase.execute({
+        userUuid: '00000000-0000-0000-0000-000000000000',
+        sessionUuid: '00000000-0000-0000-0000-000000000001',
+        itemHash: itemHash1,
+      })
+
+      expect(result.isFailed()).toBeTruthy()
+
+      mock.mockRestore()
+    })
+  })
+
+  describe('when item hash has a dedicated key system', () => {
+    it('should create a key system for the item if the item hash has information about a key system used for encryption', async () => {
+      const useCase = createUseCase()
+
+      itemHash1 = ItemHash.create({
+        ...itemHash1.props,
+        key_system_identifier: '00000000-0000-0000-0000-000000000001',
+      }).getValue()
+
+      const result = await useCase.execute({
+        userUuid: '00000000-0000-0000-0000-000000000000',
+        sessionUuid: '00000000-0000-0000-0000-000000000001',
+        itemHash: itemHash1,
+      })
+
+      expect(result.isFailed()).toBeFalsy()
+      expect(result.getValue().props.keySystemAssociation?.props.itemUuid.value).toEqual(
+        '00000000-0000-0000-0000-000000000000',
+      )
+      expect(itemRepository.save).toHaveBeenCalled()
+    })
+
+    it('should return a failure if the item hash has an invalid key system uuid', async () => {
+      const useCase = createUseCase()
+
+      itemHash1 = ItemHash.create({
+        ...itemHash1.props,
+        key_system_identifier: '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 return a failure if it fails to create a key system', async () => {
+      const mock = jest.spyOn(KeySystemAssociation, 'create')
+      mock.mockImplementation(() => {
+        return Result.fail('Oops')
+      })
+
+      const useCase = createUseCase()
+
+      itemHash1 = ItemHash.create({
+        ...itemHash1.props,
+        key_system_identifier: '00000000-0000-0000-0000-000000000001',
+      }).getValue()
+
+      const result = await useCase.execute({
+        userUuid: '00000000-0000-0000-0000-000000000000',
+        sessionUuid: '00000000-0000-0000-0000-000000000001',
+        itemHash: itemHash1,
+      })
+
+      expect(result.isFailed()).toBeTruthy()
+
+      mock.mockRestore()
+    })
+  })
 })
 })

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

@@ -14,6 +14,8 @@ import { Item } from '../../../Item/Item'
 import { SaveNewItemDTO } from './SaveNewItemDTO'
 import { SaveNewItemDTO } from './SaveNewItemDTO'
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
+import { KeySystemAssociation } from '../../../KeySystem/KeySystemAssociation'
 
 
 export class SaveNewItem implements UseCaseInterface<Item> {
 export class SaveNewItem implements UseCaseInterface<Item> {
   constructor(
   constructor(
@@ -24,6 +26,12 @@ export class SaveNewItem implements UseCaseInterface<Item> {
   ) {}
   ) {}
 
 
   async execute(dto: SaveNewItemDTO): Promise<Result<Item>> {
   async execute(dto: SaveNewItemDTO): Promise<Result<Item>> {
+    const uuidOrError = Uuid.create(dto.itemHash.props.uuid)
+    if (uuidOrError.isFailed()) {
+      return Result.fail(uuidOrError.getError())
+    }
+    const uuid = uuidOrError.getValue()
+
     let updatedWithSession = null
     let updatedWithSession = null
     if (dto.sessionUuid) {
     if (dto.sessionUuid) {
       const sessionUuidOrError = Uuid.create(dto.sessionUuid)
       const sessionUuidOrError = Uuid.create(dto.sessionUuid)
@@ -78,6 +86,51 @@ export class SaveNewItem implements UseCaseInterface<Item> {
     }
     }
     const timestamps = timestampsOrError.getValue()
     const timestamps = timestampsOrError.getValue()
 
 
+    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,
+        timestamps: Timestamps.create(
+          this.timer.getTimestampInMicroseconds(),
+          this.timer.getTimestampInMicroseconds(),
+        ).getValue(),
+        itemUuid: uuid,
+      })
+      if (sharedVaultAssociationOrError.isFailed()) {
+        return Result.fail(sharedVaultAssociationOrError.getError())
+      }
+      sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
+    }
+
+    let keySystemAssociation = undefined
+    if (dto.itemHash.hasDedicatedKeySystemAssociation()) {
+      const keySystemUuidOrError = Uuid.create(dto.itemHash.props.key_system_identifier as string)
+      if (keySystemUuidOrError.isFailed()) {
+        return Result.fail(keySystemUuidOrError.getError())
+      }
+      const keySystemUuid = keySystemUuidOrError.getValue()
+
+      const keySystemAssociationOrError = KeySystemAssociation.create({
+        itemUuid: uuid,
+        timestamps: Timestamps.create(
+          this.timer.getTimestampInMicroseconds(),
+          this.timer.getTimestampInMicroseconds(),
+        ).getValue(),
+        keySystemUuid,
+      })
+      if (keySystemAssociationOrError.isFailed()) {
+        return Result.fail(keySystemAssociationOrError.getError())
+      }
+      keySystemAssociation = keySystemAssociationOrError.getValue()
+    }
+
     const itemOrError = Item.create(
     const itemOrError = Item.create(
       {
       {
         updatedWithSession,
         updatedWithSession,
@@ -91,8 +144,10 @@ export class SaveNewItem implements UseCaseInterface<Item> {
         deleted: dto.itemHash.props.deleted ?? false,
         deleted: dto.itemHash.props.deleted ?? false,
         dates,
         dates,
         timestamps,
         timestamps,
+        keySystemAssociation,
+        sharedVaultAssociation,
       },
       },
-      new UniqueEntityId(dto.itemHash.props.uuid),
+      new UniqueEntityId(uuid.value),
     )
     )
     if (itemOrError.isFailed()) {
     if (itemOrError.isFailed()) {
       return Result.fail(itemOrError.getError())
       return Result.fail(itemOrError.getError())

+ 24 - 1
packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts

@@ -7,17 +7,40 @@ import { ItemQuery } from '../../Domain/Item/ItemQuery'
 import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
 import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
 import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
 import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
 import { TypeORMItem } from './TypeORMItem'
 import { TypeORMItem } from './TypeORMItem'
+import { KeySystemAssociationRepositoryInterface } from '../../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
+import { SharedVaultAssociationRepositoryInterface } from '../../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
 
 
 export class TypeORMItemRepository implements ItemRepositoryInterface {
 export class TypeORMItemRepository implements ItemRepositoryInterface {
-  constructor(private ormRepository: Repository<TypeORMItem>, private mapper: MapperInterface<Item, TypeORMItem>) {}
+  constructor(
+    private ormRepository: Repository<TypeORMItem>,
+    private mapper: MapperInterface<Item, TypeORMItem>,
+    private keySystemAssociationRepository: KeySystemAssociationRepositoryInterface,
+    private sharedVaultAssociationRepository: SharedVaultAssociationRepositoryInterface,
+  ) {}
 
 
   async save(item: Item): Promise<void> {
   async save(item: Item): Promise<void> {
     const persistence = this.mapper.toProjection(item)
     const persistence = this.mapper.toProjection(item)
 
 
     await this.ormRepository.save(persistence)
     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)
+    }
   }
   }
 
 
   async remove(item: Item): Promise<void> {
   async remove(item: Item): Promise<void> {
+    if (item.props.keySystemAssociation) {
+      await this.keySystemAssociationRepository.remove(item.props.keySystemAssociation)
+    }
+
+    if (item.props.sharedVaultAssociation) {
+      await this.sharedVaultAssociationRepository.remove(item.props.sharedVaultAssociation)
+    }
+
     await this.ormRepository.remove(this.mapper.toProjection(item))
     await this.ormRepository.remove(this.mapper.toProjection(item))
   }
   }
 
 

+ 33 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMKeySystemAssociation.ts

@@ -0,0 +1,33 @@
+import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity({ name: 'key_system_associations' })
+export class TypeORMKeySystemAssociation {
+  @PrimaryGeneratedColumn('uuid')
+  declare uuid: string
+
+  @Column({
+    name: 'key_system_uuid',
+    length: 36,
+  })
+  @Index('key_system_uuid_on_key_system_associations')
+  declare keySystemUuid: string
+
+  @Column({
+    name: 'item_uuid',
+    length: 36,
+  })
+  @Index('item_uuid_on_key_system_associations')
+  declare itemUuid: string
+
+  @Column({
+    name: 'created_at_timestamp',
+    type: 'bigint',
+  })
+  declare createdAtTimestamp: number
+
+  @Column({
+    name: 'updated_at_timestamp',
+    type: 'bigint',
+  })
+  declare updatedAtTimestamp: number
+}

+ 21 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMKeySystemAssociationRepository.ts

@@ -0,0 +1,21 @@
+import { Repository } from 'typeorm'
+import { MapperInterface } from '@standardnotes/domain-core'
+
+import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation'
+import { KeySystemAssociationRepositoryInterface } from '../../Domain/KeySystem/KeySystemAssociationRepositoryInterface'
+import { TypeORMKeySystemAssociation } from './TypeORMKeySystemAssociation'
+
+export class TypeORMKeySystemAssociationRepository implements KeySystemAssociationRepositoryInterface {
+  constructor(
+    private ormRepository: Repository<TypeORMKeySystemAssociation>,
+    private mapper: MapperInterface<KeySystemAssociation, TypeORMKeySystemAssociation>,
+  ) {}
+
+  async save(keySystemAssociation: KeySystemAssociation): Promise<void> {
+    await this.ormRepository.save(this.mapper.toProjection(keySystemAssociation))
+  }
+
+  async remove(keySystemAssociation: KeySystemAssociation): Promise<void> {
+    await this.ormRepository.remove(this.mapper.toProjection(keySystemAssociation))
+  }
+}

+ 5 - 10
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultItem.ts → packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultAssociation.ts

@@ -1,7 +1,7 @@
-import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
+import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
 
 
-@Entity({ name: 'shared_vault_items' })
-export class TypeORMSharedVaultItem {
+@Entity({ name: 'shared_vault_associations' })
+export class TypeORMSharedVaultAssociation {
   @PrimaryGeneratedColumn('uuid')
   @PrimaryGeneratedColumn('uuid')
   declare uuid: string
   declare uuid: string
 
 
@@ -9,21 +9,16 @@ export class TypeORMSharedVaultItem {
     name: 'shared_vault_uuid',
     name: 'shared_vault_uuid',
     length: 36,
     length: 36,
   })
   })
+  @Index('shared_vault_uuid_on_shared_vault_associations')
   declare sharedVaultUuid: string
   declare sharedVaultUuid: string
 
 
   @Column({
   @Column({
     name: 'item_uuid',
     name: 'item_uuid',
     length: 36,
     length: 36,
   })
   })
+  @Index('item_uuid_on_shared_vault_associations')
   declare itemUuid: string
   declare itemUuid: string
 
 
-  @Column({
-    name: 'key_system_identifier',
-    type: 'varchar',
-    length: 36,
-  })
-  declare keySystemIdentifier: string
-
   @Column({
   @Column({
     name: 'last_edited_by',
     name: 'last_edited_by',
     type: 'varchar',
     type: 'varchar',

+ 21 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultAssociationRepository.ts

@@ -0,0 +1,21 @@
+import { Repository } from 'typeorm'
+import { MapperInterface } from '@standardnotes/domain-core'
+
+import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation'
+import { SharedVaultAssociationRepositoryInterface } from '../../Domain/SharedVault/SharedVaultAssociationRepositoryInterface'
+import { TypeORMSharedVaultAssociation } from './TypeORMSharedVaultAssociation'
+
+export class TypeORMSharedVaultAssociationRepository implements SharedVaultAssociationRepositoryInterface {
+  constructor(
+    private ormRepository: Repository<TypeORMSharedVaultAssociation>,
+    private mapper: MapperInterface<SharedVaultAssociation, TypeORMSharedVaultAssociation>,
+  ) {}
+
+  async save(sharedVaultAssociation: SharedVaultAssociation): Promise<void> {
+    await this.ormRepository.save(this.mapper.toProjection(sharedVaultAssociation))
+  }
+
+  async remove(sharedVaultAssociation: SharedVaultAssociation): Promise<void> {
+    await this.ormRepository.remove(this.mapper.toProjection(sharedVaultAssociation))
+  }
+}

+ 0 - 30
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultItemRepository.ts

@@ -1,30 +0,0 @@
-import { Repository } from 'typeorm'
-import { MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
-
-import { SharedVaultItem } from '../../Domain/SharedVault/Item/SharedVaultItem'
-import { SharedVaultItemRepositoryInterface } from '../../Domain/SharedVault/Item/SharedVaultItemRepositoryInterface'
-import { TypeORMSharedVaultItem } from './TypeORMSharedVaultItem'
-
-export class TypeORMSharedVaultItemRepository implements SharedVaultItemRepositoryInterface {
-  constructor(
-    private ormRepository: Repository<TypeORMSharedVaultItem>,
-    private mapper: MapperInterface<SharedVaultItem, TypeORMSharedVaultItem>,
-  ) {}
-
-  async findBySharedVaultId(sharedVaultId: UniqueEntityId): Promise<SharedVaultItem[]> {
-    const persistence = await this.ormRepository
-      .createQueryBuilder('shared_vault_item')
-      .where('shared_vault_item.shared_vault_uuid = :sharedVaultUuid', {
-        sharedVaultUuid: sharedVaultId.toString(),
-      })
-      .getMany()
-
-    return persistence.map((p) => this.mapper.toDomain(p))
-  }
-
-  async save(sharedVaultItem: SharedVaultItem): Promise<void> {
-    const persistence = this.mapper.toProjection(sharedVaultItem)
-
-    await this.ormRepository.save(persistence)
-  }
-}

+ 0 - 6
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultRepository.ts

@@ -4,12 +4,10 @@ import { MapperInterface, Uuid } from '@standardnotes/domain-core'
 import { SharedVaultRepositoryInterface } from '../../Domain/SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultRepositoryInterface } from '../../Domain/SharedVault/SharedVaultRepositoryInterface'
 import { TypeORMSharedVault } from './TypeORMSharedVault'
 import { TypeORMSharedVault } from './TypeORMSharedVault'
 import { SharedVault } from '../../Domain/SharedVault/SharedVault'
 import { SharedVault } from '../../Domain/SharedVault/SharedVault'
-import { SharedVaultItemRepositoryInterface } from '../../Domain/SharedVault/Item/SharedVaultItemRepositoryInterface'
 
 
 export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterface {
 export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterface {
   constructor(
   constructor(
     private ormRepository: Repository<TypeORMSharedVault>,
     private ormRepository: Repository<TypeORMSharedVault>,
-    private sharedVaultItemRepository: SharedVaultItemRepositoryInterface,
     private mapper: MapperInterface<SharedVault, TypeORMSharedVault>,
     private mapper: MapperInterface<SharedVault, TypeORMSharedVault>,
   ) {}
   ) {}
 
 
@@ -28,10 +26,6 @@ export class TypeORMSharedVaultRepository implements SharedVaultRepositoryInterf
   }
   }
 
 
   async save(sharedVault: SharedVault): Promise<void> {
   async save(sharedVault: SharedVault): Promise<void> {
-    for (const item of sharedVault.props.sharedVaultItems) {
-      await this.sharedVaultItemRepository.save(item)
-    }
-
     const persistence = this.mapper.toProjection(sharedVault)
     const persistence = this.mapper.toProjection(sharedVault)
 
 
     await this.ormRepository.save(persistence)
     await this.ormRepository.save(persistence)

+ 10 - 0
packages/syncing-server/src/Mapping/Http/ItemHttpMapper.ts

@@ -26,6 +26,16 @@ export class ItemHttpMapper implements MapperInterface<Item, ItemHttpRepresentat
       updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
       updated_at: this.timer.convertMicrosecondsToStringDate(domain.props.timestamps.updatedAt),
       updated_at_timestamp: domain.props.timestamps.updatedAt,
       updated_at_timestamp: domain.props.timestamps.updatedAt,
       updated_with_session: domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null,
       updated_with_session: domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null,
+      key_system_identifier: domain.props.keySystemAssociation
+        ? domain.props.keySystemAssociation.props.keySystemUuid.value
+        : null,
+      shared_vault_uuid: domain.props.sharedVaultAssociation
+        ? domain.props.sharedVaultAssociation.props.sharedVaultUuid.value
+        : null,
+      user_uuid: domain.props.userUuid.value,
+      last_edited_by_uuid: domain.props.sharedVaultAssociation
+        ? domain.props.sharedVaultAssociation.props.lastEditedBy.value
+        : null,
     }
     }
   }
   }
 }
 }

+ 4 - 0
packages/syncing-server/src/Mapping/Http/ItemHttpRepresentation.ts

@@ -12,4 +12,8 @@ export interface ItemHttpRepresentation {
   updated_at: string
   updated_at: string
   updated_at_timestamp: number
   updated_at_timestamp: number
   updated_with_session: string | null
   updated_with_session: string | null
+  key_system_identifier: string | null
+  shared_vault_uuid: string | null
+  user_uuid: string | null
+  last_edited_by_uuid: string | null
 }
 }

+ 57 - 0
packages/syncing-server/src/Mapping/Persistence/KeySystemAssociationPersistenceMapper.ts

@@ -0,0 +1,57 @@
+import { MapperInterface, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
+
+import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation'
+
+import { TypeORMKeySystemAssociation } from '../../Infra/TypeORM/TypeORMKeySystemAssociation'
+
+export class KeySystemAssociationPersistenceMapper
+  implements MapperInterface<KeySystemAssociation, TypeORMKeySystemAssociation>
+{
+  toDomain(projection: TypeORMKeySystemAssociation): KeySystemAssociation {
+    const itemUuidOrError = Uuid.create(projection.itemUuid)
+    if (itemUuidOrError.isFailed()) {
+      throw new Error(`Failed to create key system from projection: ${itemUuidOrError.getError()}`)
+    }
+    const itemUuid = itemUuidOrError.getValue()
+
+    const keySystemUuidOrError = Uuid.create(projection.keySystemUuid)
+    if (keySystemUuidOrError.isFailed()) {
+      throw new Error(`Failed to create key system from projection: ${keySystemUuidOrError.getError()}`)
+    }
+    const keySystemUuid = keySystemUuidOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
+    if (timestampsOrError.isFailed()) {
+      throw new Error(`Failed to create key system from projection: ${timestampsOrError.getError()}`)
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    const keySystemOrError = KeySystemAssociation.create(
+      {
+        itemUuid,
+        timestamps,
+        keySystemUuid,
+      },
+      new UniqueEntityId(projection.uuid),
+    )
+    if (keySystemOrError.isFailed()) {
+      throw new Error(`Failed to create key system from projection: ${keySystemOrError.getError()}`)
+    }
+
+    const keySystem = keySystemOrError.getValue()
+
+    return keySystem
+  }
+
+  toProjection(domain: KeySystemAssociation): TypeORMKeySystemAssociation {
+    const typeorm = new TypeORMKeySystemAssociation()
+
+    typeorm.uuid = domain.id.toString()
+    typeorm.itemUuid = domain.props.itemUuid.value
+    typeorm.keySystemUuid = domain.props.keySystemUuid.value
+    typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
+    typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
+
+    return typeorm
+  }
+}

+ 65 - 0
packages/syncing-server/src/Mapping/Persistence/SharedVaultAssociationPersistenceMapper.ts

@@ -0,0 +1,65 @@
+import { MapperInterface, Timestamps, UniqueEntityId, Uuid } from '@standardnotes/domain-core'
+
+import { TypeORMSharedVaultAssociation } from '../../Infra/TypeORM/TypeORMSharedVaultAssociation'
+import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation'
+
+export class SharedVaultAssociationPersistenceMapper
+  implements MapperInterface<SharedVaultAssociation, TypeORMSharedVaultAssociation>
+{
+  toDomain(projection: TypeORMSharedVaultAssociation): SharedVaultAssociation {
+    const itemUuidOrError = Uuid.create(projection.itemUuid)
+    if (itemUuidOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault association from projection: ${itemUuidOrError.getError()}`)
+    }
+    const itemUuid = itemUuidOrError.getValue()
+
+    const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault association from projection: ${sharedVaultUuidOrError.getError()}`)
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const lastEditedByOrError = Uuid.create(projection.lastEditedBy)
+    if (lastEditedByOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault association from projection: ${lastEditedByOrError.getError()}`)
+    }
+    const lastEditedBy = lastEditedByOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
+    if (timestampsOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault association from projection: ${timestampsOrError.getError()}`)
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    const sharedVaultAssociationOrError = SharedVaultAssociation.create(
+      {
+        itemUuid,
+        lastEditedBy,
+        sharedVaultUuid,
+        timestamps,
+      },
+      new UniqueEntityId(projection.uuid),
+    )
+    if (sharedVaultAssociationOrError.isFailed()) {
+      throw new Error(
+        `Failed to create shared vault association from projection: ${sharedVaultAssociationOrError.getError()}`,
+      )
+    }
+    const sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
+
+    return sharedVaultAssociation
+  }
+
+  toProjection(domain: SharedVaultAssociation): TypeORMSharedVaultAssociation {
+    const typeorm = new TypeORMSharedVaultAssociation()
+
+    typeorm.uuid = domain.id.toString()
+    typeorm.sharedVaultUuid = domain.props.sharedVaultUuid.value
+    typeorm.itemUuid = domain.props.itemUuid.value
+    typeorm.lastEditedBy = domain.props.lastEditedBy.value
+    typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
+    typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
+
+    return typeorm
+  }
+}

+ 0 - 1
packages/syncing-server/src/Mapping/Persistence/SharedVaultPersistenceMapper.ts

@@ -23,7 +23,6 @@ export class SharedVaultPersistenceMapper implements MapperInterface<SharedVault
         fileUploadBytesUsed: projection.fileUploadBytesUsed,
         fileUploadBytesUsed: projection.fileUploadBytesUsed,
         fileUploadBytesLimit: projection.fileUploadBytesLimit,
         fileUploadBytesLimit: projection.fileUploadBytesLimit,
         timestamps,
         timestamps,
-        sharedVaultItems: [],
       },
       },
       new UniqueEntityId(projection.uuid),
       new UniqueEntityId(projection.uuid),
     )
     )