Browse Source

feat(syncing-server): turn mysql items model into legacy (#711)

* feat(syncing-server): turn mysql items model into legacy

* fix: rename MySQL model to SQL model to include SQLite option

* fix: rename mysqlitem to sqlitem
Karol Sójko 1 year ago
parent
commit
effdfebc19

+ 18 - 18
packages/syncing-server/src/Bootstrap/Container.ts

@@ -6,7 +6,7 @@ import TYPES from './Types'
 import { AppDataSource } from './DataSource'
 import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
 import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
-import { TypeORMItemRepository } from '../Infra/TypeORM/TypeORMItemRepository'
+import { SQLLegacyItemRepository } from '../Infra/TypeORM/SQLLegacyItemRepository'
 import { MongoRepository, Repository } from 'typeorm'
 import { Item } from '../Domain/Item/Item'
 import {
@@ -61,8 +61,8 @@ import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
 import { ControllerContainer, ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
 import { BaseItemsController } from '../Infra/InversifyExpressUtils/Base/BaseItemsController'
 import { Transform } from 'stream'
-import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
-import { ItemPersistenceMapper } from '../Mapping/Persistence/ItemPersistenceMapper'
+import { SQLLegacyItem } from '../Infra/TypeORM/SQLLegacyItem'
+import { SQLLegacyItemPersistenceMapper } from '../Mapping/Persistence/SQLLegacyItemPersistenceMapper'
 import { ItemHttpRepresentation } from '../Mapping/Http/ItemHttpRepresentation'
 import { ItemHttpMapper } from '../Mapping/Http/ItemHttpMapper'
 import { SavedItemHttpRepresentation } from '../Mapping/Http/SavedItemHttpRepresentation'
@@ -303,8 +303,8 @@ export class ContainerConfigLoader {
 
     // Mapping
     container
-      .bind<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper)
-      .toConstantValue(new ItemPersistenceMapper())
+      .bind<MapperInterface<Item, SQLLegacyItem>>(TYPES.Sync_SQLLegacyItemPersistenceMapper)
+      .toConstantValue(new SQLLegacyItemPersistenceMapper())
     container
       .bind<MapperInterface<ItemHash, ItemHashHttpRepresentation>>(TYPES.Sync_ItemHashHttpMapper)
       .toConstantValue(new ItemHashHttpMapper())
@@ -360,8 +360,8 @@ export class ContainerConfigLoader {
 
     // ORM
     container
-      .bind<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository)
-      .toDynamicValue(() => appDataSource.getRepository(TypeORMItem))
+      .bind<Repository<SQLLegacyItem>>(TYPES.Sync_ORMLegacyItemRepository)
+      .toDynamicValue(() => appDataSource.getRepository(SQLLegacyItem))
     container
       .bind<Repository<TypeORMSharedVault>>(TYPES.Sync_ORMSharedVaultRepository)
       .toConstantValue(appDataSource.getRepository(TypeORMSharedVault))
@@ -401,11 +401,11 @@ export class ContainerConfigLoader {
 
     // Repositories
     container
-      .bind<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository)
+      .bind<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository)
       .toConstantValue(
-        new TypeORMItemRepository(
-          container.get<Repository<TypeORMItem>>(TYPES.Sync_ORMItemRepository),
-          container.get<MapperInterface<Item, TypeORMItem>>(TYPES.Sync_ItemPersistenceMapper),
+        new SQLLegacyItemRepository(
+          container.get<Repository<SQLLegacyItem>>(TYPES.Sync_ORMLegacyItemRepository),
+          container.get<MapperInterface<Item, SQLLegacyItem>>(TYPES.Sync_SQLLegacyItemPersistenceMapper),
           container.get<Logger>(TYPES.Sync_Logger),
         ),
       )
@@ -413,7 +413,7 @@ export class ContainerConfigLoader {
       .bind<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver)
       .toConstantValue(
         new TypeORMItemRepositoryResolver(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
+          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository),
           isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
         ),
       )
@@ -777,7 +777,7 @@ export class ContainerConfigLoader {
       )
       .toConstantValue(
         new TransitionItemsFromPrimaryToSecondaryDatabaseForUser(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
+          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository),
           isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
           container.get<TimerInterface>(TYPES.Sync_Timer),
           container.get<Logger>(TYPES.Sync_Logger),
@@ -843,7 +843,7 @@ export class ContainerConfigLoader {
       .bind<DuplicateItemSyncedEventHandler>(TYPES.Sync_DuplicateItemSyncedEventHandler)
       .toConstantValue(
         new DuplicateItemSyncedEventHandler(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
+          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository),
           isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
           container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
@@ -854,7 +854,7 @@ export class ContainerConfigLoader {
       .bind<AccountDeletionRequestedEventHandler>(TYPES.Sync_AccountDeletionRequestedEventHandler)
       .toConstantValue(
         new AccountDeletionRequestedEventHandler(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
+          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository),
           isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
           container.get<Logger>(TYPES.Sync_Logger),
         ),
@@ -863,7 +863,7 @@ export class ContainerConfigLoader {
       .bind<ItemRevisionCreationRequestedEventHandler>(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)
       .toConstantValue(
         new ItemRevisionCreationRequestedEventHandler(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
+          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository),
           isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
           container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
@@ -918,7 +918,7 @@ export class ContainerConfigLoader {
       .toConstantValue(
         new ExtensionsHttpService(
           container.get<AxiosInstance>(TYPES.Sync_HTTPClient),
-          container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
+          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository),
           isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
           container.get<ContentDecoderInterface>(TYPES.Sync_ContentDecoder),
           container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
@@ -964,7 +964,7 @@ export class ContainerConfigLoader {
         .bind<EmailBackupRequestedEventHandler>(TYPES.Sync_EmailBackupRequestedEventHandler)
         .toConstantValue(
           new EmailBackupRequestedEventHandler(
-            container.get<ItemRepositoryInterface>(TYPES.Sync_MySQLItemRepository),
+            container.get<ItemRepositoryInterface>(TYPES.Sync_SQLLegacyItemRepository),
             isSecondaryDatabaseEnabled
               ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository)
               : null,

+ 2 - 2
packages/syncing-server/src/Bootstrap/DataSource.ts

@@ -2,7 +2,7 @@ import { DataSource, EntityTarget, LoggerOptions, MongoRepository, ObjectLiteral
 import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
-import { TypeORMItem } from '../Infra/TypeORM/TypeORMItem'
+import { SQLLegacyItem } from '../Infra/TypeORM/SQLLegacyItem'
 import { TypeORMNotification } from '../Infra/TypeORM/TypeORMNotification'
 import { TypeORMSharedVault } from '../Infra/TypeORM/TypeORMSharedVault'
 import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
@@ -75,7 +75,7 @@ export class AppDataSource {
     const commonDataSourceOptions = {
       maxQueryExecutionTime,
       entities: [
-        TypeORMItem,
+        SQLLegacyItem,
         TypeORMNotification,
         TypeORMSharedVault,
         TypeORMSharedVaultUser,

+ 3 - 3
packages/syncing-server/src/Bootstrap/Types.ts

@@ -8,7 +8,7 @@ const TYPES = {
   Sync_Env: Symbol.for('Sync_Env'),
   // Repositories
   Sync_ItemRepositoryResolver: Symbol.for('Sync_ItemRepositoryResolver'),
-  Sync_MySQLItemRepository: Symbol.for('Sync_MySQLItemRepository'),
+  Sync_SQLLegacyItemRepository: Symbol.for('Sync_SQLLegacyItemRepository'),
   Sync_MongoDBItemRepository: Symbol.for('Sync_MongoDBItemRepository'),
   Sync_SharedVaultRepository: Symbol.for('Sync_SharedVaultRepository'),
   Sync_SharedVaultInviteRepository: Symbol.for('Sync_SharedVaultInviteRepository'),
@@ -16,7 +16,7 @@ const TYPES = {
   Sync_NotificationRepository: Symbol.for('Sync_NotificationRepository'),
   Sync_MessageRepository: Symbol.for('Sync_MessageRepository'),
   // ORM
-  Sync_ORMItemRepository: Symbol.for('Sync_ORMItemRepository'),
+  Sync_ORMLegacyItemRepository: Symbol.for('Sync_ORMLegacyItemRepository'),
   Sync_ORMSharedVaultRepository: Symbol.for('Sync_ORMSharedVaultRepository'),
   Sync_ORMSharedVaultInviteRepository: Symbol.for('Sync_ORMSharedVaultInviteRepository'),
   Sync_ORMSharedVaultUserRepository: Symbol.for('Sync_ORMSharedVaultUserRepository'),
@@ -131,7 +131,7 @@ const TYPES = {
   Sync_MessagePersistenceMapper: Symbol.for('Sync_MessagePersistenceMapper'),
   Sync_MessageHttpMapper: Symbol.for('Sync_MessageHttpMapper'),
   Sync_NotificationHttpMapper: Symbol.for('Sync_NotificationHttpMapper'),
-  Sync_ItemPersistenceMapper: Symbol.for('Sync_ItemPersistenceMapper'),
+  Sync_SQLLegacyItemPersistenceMapper: Symbol.for('Sync_SQLLegacyItemPersistenceMapper'),
   Sync_MongoDBItemPersistenceMapper: Symbol.for('Sync_MongoDBItemPersistenceMapper'),
   Sync_ItemHttpMapper: Symbol.for('Sync_ItemHttpMapper'),
   Sync_ItemHashHttpMapper: Symbol.for('Sync_ItemHashHttpMapper'),

+ 29 - 0
packages/syncing-server/src/Infra/TypeORM/SQLItem.ts

@@ -0,0 +1,29 @@
+import { Column, Entity } from 'typeorm'
+import { SQLLegacyItem } from './SQLLegacyItem'
+
+@Entity({ name: 'items' })
+export class SQLItem extends SQLLegacyItem {
+  @Column({
+    type: 'varchar',
+    name: 'last_edited_by',
+    length: 36,
+    nullable: true,
+  })
+  declare lastEditedBy: string | null
+
+  @Column({
+    type: 'varchar',
+    name: 'shared_vault_uuid',
+    length: 36,
+    nullable: true,
+  })
+  declare sharedVaultUuid: string | null
+
+  @Column({
+    type: 'varchar',
+    name: 'key_system_identifier',
+    length: 36,
+    nullable: true,
+  })
+  declare keySystemIdentifier: string | null
+}

+ 78 - 0
packages/syncing-server/src/Infra/TypeORM/SQLItemRepository.ts

@@ -0,0 +1,78 @@
+import { Repository, SelectQueryBuilder } from 'typeorm'
+import { MapperInterface } from '@standardnotes/domain-core'
+import { Logger } from 'winston'
+
+import { Item } from '../../Domain/Item/Item'
+import { SQLLegacyItem } from './SQLLegacyItem'
+import { SQLLegacyItemRepository } from './SQLLegacyItemRepository'
+import { ItemQuery } from '../../Domain/Item/ItemQuery'
+
+export class SQLItemRepository extends SQLLegacyItemRepository {
+  constructor(
+    protected override ormRepository: Repository<SQLLegacyItem>,
+    protected override mapper: MapperInterface<Item, SQLLegacyItem>,
+    protected override logger: Logger,
+  ) {
+    super(ormRepository, mapper, logger)
+  }
+
+  protected override createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<SQLLegacyItem> {
+    const queryBuilder = this.ormRepository.createQueryBuilder('item')
+
+    if (query.sortBy !== undefined && query.sortOrder !== undefined) {
+      queryBuilder.orderBy(`item.${query.sortBy}`, query.sortOrder)
+    }
+
+    if (query.includeSharedVaultUuids !== undefined && query.includeSharedVaultUuids.length > 0) {
+      if (query.userUuid) {
+        queryBuilder.where('(item.user_uuid = :userUuid OR item.shared_vault_uuid IN (:...includeSharedVaultUuids))', {
+          userUuid: query.userUuid,
+          includeSharedVaultUuids: query.includeSharedVaultUuids,
+        })
+      } else {
+        queryBuilder.where('item.shared_vault_uuid IN (:...includeSharedVaultUuids)', {
+          includeSharedVaultUuids: query.includeSharedVaultUuids,
+        })
+      }
+    } else if (query.exclusiveSharedVaultUuids !== undefined && query.exclusiveSharedVaultUuids.length > 0) {
+      queryBuilder.where('item.shared_vault_uuid IN (:...exclusiveSharedVaultUuids)', {
+        exclusiveSharedVaultUuids: query.exclusiveSharedVaultUuids,
+      })
+    } else if (query.userUuid !== undefined) {
+      queryBuilder.where('item.user_uuid = :userUuid', { userUuid: query.userUuid })
+    }
+
+    if (query.uuids && query.uuids.length > 0) {
+      queryBuilder.andWhere('item.uuid IN (:...uuids)', { uuids: query.uuids })
+    }
+    if (query.deleted !== undefined) {
+      queryBuilder.andWhere('item.deleted = :deleted', { deleted: query.deleted })
+    }
+    if (query.contentType) {
+      if (Array.isArray(query.contentType)) {
+        queryBuilder.andWhere('item.content_type IN (:...contentTypes)', { contentTypes: query.contentType })
+      } else {
+        queryBuilder.andWhere('item.content_type = :contentType', { contentType: query.contentType })
+      }
+    }
+    if (query.lastSyncTime && query.syncTimeComparison) {
+      queryBuilder.andWhere(`item.updated_at_timestamp ${query.syncTimeComparison} :lastSyncTime`, {
+        lastSyncTime: query.lastSyncTime,
+      })
+    }
+    if (query.createdBetween !== undefined) {
+      queryBuilder.andWhere('item.created_at >= :createdAfter AND item.created_at <= :createdBefore', {
+        createdAfter: query.createdBetween[0].toISOString(),
+        createdBefore: query.createdBetween[1].toISOString(),
+      })
+    }
+    if (query.offset !== undefined) {
+      queryBuilder.skip(query.offset)
+    }
+    if (query.limit !== undefined) {
+      queryBuilder.take(query.limit)
+    }
+
+    return queryBuilder
+  }
+}

+ 1 - 1
packages/syncing-server/src/Infra/TypeORM/TypeORMItem.ts → packages/syncing-server/src/Infra/TypeORM/SQLLegacyItem.ts

@@ -8,7 +8,7 @@ import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
   'createdAtTimestamp',
 ])
 @Index('user_uuid_and_deleted', ['userUuid', 'deleted'])
-export class TypeORMItem {
+export class SQLLegacyItem {
   @PrimaryGeneratedColumn('uuid')
   declare uuid: string
 

+ 6 - 6
packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepository.ts → packages/syncing-server/src/Infra/TypeORM/SQLLegacyItemRepository.ts

@@ -6,14 +6,14 @@ import { Item } from '../../Domain/Item/Item'
 import { ItemQuery } from '../../Domain/Item/ItemQuery'
 import { ItemRepositoryInterface } from '../../Domain/Item/ItemRepositoryInterface'
 import { ExtendedIntegrityPayload } from '../../Domain/Item/ExtendedIntegrityPayload'
-import { TypeORMItem } from './TypeORMItem'
+import { SQLLegacyItem } from './SQLLegacyItem'
 import { ItemContentSizeDescriptor } from '../../Domain/Item/ItemContentSizeDescriptor'
 
-export class TypeORMItemRepository implements ItemRepositoryInterface {
+export class SQLLegacyItemRepository implements ItemRepositoryInterface {
   constructor(
-    private ormRepository: Repository<TypeORMItem>,
-    private mapper: MapperInterface<Item, TypeORMItem>,
-    private logger: Logger,
+    protected ormRepository: Repository<SQLLegacyItem>,
+    protected mapper: MapperInterface<Item, SQLLegacyItem>,
+    protected logger: Logger,
   ) {}
 
   async save(item: Item): Promise<void> {
@@ -179,7 +179,7 @@ export class TypeORMItemRepository implements ItemRepositoryInterface {
       .execute()
   }
 
-  private createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<TypeORMItem> {
+  protected createFindAllQueryBuilder(query: ItemQuery): SelectQueryBuilder<SQLLegacyItem> {
     const queryBuilder = this.ormRepository.createQueryBuilder('item')
 
     if (query.sortBy !== undefined && query.sortOrder !== undefined) {

+ 3 - 3
packages/syncing-server/src/Infra/TypeORM/TypeORMItemRepositoryResolver.ts

@@ -5,13 +5,13 @@ import { ItemRepositoryResolverInterface } from '../../Domain/Item/ItemRepositor
 
 export class TypeORMItemRepositoryResolver implements ItemRepositoryResolverInterface {
   constructor(
-    private mysqlItemRepository: ItemRepositoryInterface,
+    private SQLItemRepository: ItemRepositoryInterface,
     private mongoDbItemRepository: ItemRepositoryInterface | null,
   ) {}
 
   resolve(roleNames: RoleNameCollection): ItemRepositoryInterface {
     if (!this.mongoDbItemRepository) {
-      return this.mysqlItemRepository
+      return this.SQLItemRepository
     }
 
     const transitionRoleName = RoleName.create(RoleName.NAMES.TransitionUser).getValue()
@@ -20,6 +20,6 @@ export class TypeORMItemRepositoryResolver implements ItemRepositoryResolverInte
       return this.mongoDbItemRepository
     }
 
-    return this.mysqlItemRepository
+    return this.SQLItemRepository
   }
 }

+ 148 - 0
packages/syncing-server/src/Mapping/Persistence/SQLItemPersistenceMapper.ts

@@ -0,0 +1,148 @@
+import { Timestamps, MapperInterface, UniqueEntityId, Uuid, ContentType, Dates } from '@standardnotes/domain-core'
+
+import { Item } from '../../Domain/Item/Item'
+
+import { SQLItem } from '../../Infra/TypeORM/SQLItem'
+import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation'
+import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation'
+
+export class SQLItemPersistenceMapper implements MapperInterface<Item, SQLItem> {
+  toDomain(projection: SQLItem): Item {
+    const uuidOrError = Uuid.create(projection.uuid)
+    if (uuidOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${uuidOrError.getError()}`)
+    }
+    const uuid = uuidOrError.getValue()
+
+    let duplicateOf = null
+    if (projection.duplicateOf) {
+      const duplicateOfOrError = Uuid.create(projection.duplicateOf)
+      if (duplicateOfOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${duplicateOfOrError.getError()}`)
+      }
+      duplicateOf = duplicateOfOrError.getValue()
+    }
+
+    const contentTypeOrError = ContentType.create(projection.contentType)
+    if (contentTypeOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${contentTypeOrError.getError()}`)
+    }
+    const contentType = contentTypeOrError.getValue()
+
+    const userUuidOrError = Uuid.create(projection.userUuid)
+    if (userUuidOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${userUuidOrError.getError()}`)
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const datesOrError = Dates.create(projection.createdAt, projection.updatedAt)
+    if (datesOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${datesOrError.getError()}`)
+    }
+    const dates = datesOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
+    if (timestampsOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${timestampsOrError.getError()}`)
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    let updatedWithSession = null
+    if (projection.updatedWithSession) {
+      const updatedWithSessionOrError = Uuid.create(projection.updatedWithSession)
+      if (updatedWithSessionOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${updatedWithSessionOrError.getError()}`)
+      }
+      updatedWithSession = updatedWithSessionOrError.getValue()
+    }
+
+    let sharedVaultAssociation: SharedVaultAssociation | undefined = undefined
+    if (projection.sharedVaultUuid && projection.lastEditedBy) {
+      const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${sharedVaultUuidOrError.getError()}`)
+      }
+      const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+      const lastEditedByOrError = Uuid.create(projection.lastEditedBy)
+      if (lastEditedByOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${lastEditedByOrError.getError()}`)
+      }
+      const lastEditedBy = lastEditedByOrError.getValue()
+
+      const sharedVaultAssociationOrError = SharedVaultAssociation.create({
+        sharedVaultUuid,
+        lastEditedBy,
+      })
+      if (sharedVaultAssociationOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${sharedVaultAssociationOrError.getError()}`)
+      }
+      sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
+    }
+
+    let keySystemAssociation: KeySystemAssociation | undefined = undefined
+    if (projection.keySystemIdentifier) {
+      const keySystemAssociationOrError = KeySystemAssociation.create(projection.keySystemIdentifier)
+      if (keySystemAssociationOrError.isFailed()) {
+        throw new Error(`Failed to create item from projection: ${keySystemAssociationOrError.getError()}`)
+      }
+      keySystemAssociation = keySystemAssociationOrError.getValue()
+    }
+
+    const itemOrError = Item.create(
+      {
+        duplicateOf,
+        itemsKeyId: projection.itemsKeyId,
+        content: projection.content,
+        contentType,
+        contentSize: projection.contentSize ?? undefined,
+        encItemKey: projection.encItemKey,
+        authHash: projection.authHash,
+        userUuid,
+        deleted: projection.deleted,
+        dates,
+        timestamps,
+        updatedWithSession,
+        sharedVaultAssociation,
+        keySystemAssociation,
+      },
+      new UniqueEntityId(uuid.value),
+    )
+    if (itemOrError.isFailed()) {
+      throw new Error(`Failed to create item from projection: ${itemOrError.getError()}`)
+    }
+
+    return itemOrError.getValue()
+  }
+
+  toProjection(domain: Item): SQLItem {
+    const typeorm = new SQLItem()
+
+    typeorm.uuid = domain.id.toString()
+    typeorm.duplicateOf = domain.props.duplicateOf ? domain.props.duplicateOf.value : null
+    typeorm.itemsKeyId = domain.props.itemsKeyId
+    typeorm.content = domain.props.content
+    typeorm.contentType = domain.props.contentType.value
+    typeorm.contentSize = domain.props.contentSize ?? null
+    typeorm.encItemKey = domain.props.encItemKey
+    typeorm.authHash = domain.props.authHash
+    typeorm.userUuid = domain.props.userUuid.value
+    typeorm.deleted = domain.props.deleted
+    typeorm.createdAt = domain.props.dates.createdAt
+    typeorm.updatedAt = domain.props.dates.updatedAt
+    typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
+    typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
+    typeorm.updatedWithSession = domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null
+    typeorm.lastEditedBy = domain.props.sharedVaultAssociation
+      ? domain.props.sharedVaultAssociation.props.lastEditedBy.value
+      : null
+    typeorm.sharedVaultUuid = domain.props.sharedVaultAssociation
+      ? domain.props.sharedVaultAssociation.props.sharedVaultUuid.value
+      : null
+    typeorm.keySystemIdentifier = domain.props.keySystemAssociation
+      ? domain.props.keySystemAssociation.props.keySystemIdentifier
+      : null
+
+    return typeorm
+  }
+}

+ 5 - 5
packages/syncing-server/src/Mapping/Persistence/ItemPersistenceMapper.ts → packages/syncing-server/src/Mapping/Persistence/SQLLegacyItemPersistenceMapper.ts

@@ -2,10 +2,10 @@ import { Timestamps, MapperInterface, UniqueEntityId, Uuid, ContentType, Dates }
 
 import { Item } from '../../Domain/Item/Item'
 
-import { TypeORMItem } from '../../Infra/TypeORM/TypeORMItem'
+import { SQLLegacyItem } from '../../Infra/TypeORM/SQLLegacyItem'
 
-export class ItemPersistenceMapper implements MapperInterface<Item, TypeORMItem> {
-  toDomain(projection: TypeORMItem): Item {
+export class SQLLegacyItemPersistenceMapper implements MapperInterface<Item, SQLLegacyItem> {
+  toDomain(projection: SQLLegacyItem): Item {
     const uuidOrError = Uuid.create(projection.uuid)
     if (uuidOrError.isFailed()) {
       throw new Error(`Failed to create item from projection: ${uuidOrError.getError()}`)
@@ -78,8 +78,8 @@ export class ItemPersistenceMapper implements MapperInterface<Item, TypeORMItem>
     return itemOrError.getValue()
   }
 
-  toProjection(domain: Item): TypeORMItem {
-    const typeorm = new TypeORMItem()
+  toProjection(domain: Item): SQLLegacyItem {
+    const typeorm = new SQLLegacyItem()
 
     typeorm.uuid = domain.id.toString()
     typeorm.duplicateOf = domain.props.duplicateOf ? domain.props.duplicateOf.value : null