Browse Source

feat: choose primary or secondary revisions database based on transition role (#716)

* feat: choose primary or secondary revisions database based on transition role

* fix: remove redundant types

* fix: binding

* fix: specs
Karol Sójko 1 year ago
parent
commit
62d231ae41
60 changed files with 627 additions and 514 deletions
  1. 1 0
      packages/auth/src/Domain/Event/DomainEventFactory.ts
  2. 1 0
      packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts
  3. 1 0
      packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts
  4. 7 0
      packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts
  5. 3 0
      packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts
  6. 1 0
      packages/domain-events/src/Domain/Event/AccountDeletionRequestedEventPayload.ts
  7. 1 0
      packages/domain-events/src/Domain/Event/DuplicateItemSyncedEventPayload.ts
  8. 1 0
      packages/domain-events/src/Domain/Event/ItemDumpedEventPayload.ts
  9. 1 0
      packages/domain-events/src/Domain/Event/ItemRevisionCreationRequestedEventPayload.ts
  10. 1 0
      packages/domain-events/src/Domain/Event/RevisionsCopyRequestedEventPayload.ts
  11. 78 86
      packages/revisions/src/Bootstrap/Container.ts
  12. 2 2
      packages/revisions/src/Bootstrap/DataSource.ts
  13. 2 1
      packages/revisions/src/Bootstrap/Types.ts
  14. 0 129
      packages/revisions/src/Controller/RevisionsController.ts
  15. 17 1
      packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts
  16. 14 4
      packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts
  17. 16 1
      packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.spec.ts
  18. 14 3
      packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.ts
  19. 1 0
      packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.spec.ts
  20. 1 0
      packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.ts
  21. 7 0
      packages/revisions/src/Domain/Revision/RevisionRepositoryResolverInterface.ts
  22. 20 1
      packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.spec.ts
  23. 13 5
      packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.ts
  24. 1 0
      packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisionsDTO.ts
  25. 19 1
      packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts
  26. 12 4
      packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts
  27. 1 0
      packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts
  28. 20 1
      packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.spec.ts
  29. 12 4
      packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts
  30. 1 0
      packages/revisions/src/Domain/UseCase/GetRevision/GetRevisionDTO.ts
  31. 19 1
      packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts
  32. 12 4
      packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts
  33. 1 0
      packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts
  34. 0 4
      packages/revisions/src/Infra/Http/Request/DeleteRevisionRequestParams.ts
  35. 0 4
      packages/revisions/src/Infra/Http/Request/GetRevisionRequestParams.ts
  36. 0 4
      packages/revisions/src/Infra/Http/Request/GetRevisionsMetadataRequestParams.ts
  37. 0 13
      packages/revisions/src/Infra/Http/Response/GetRevisionResponseBody.ts
  38. 0 8
      packages/revisions/src/Infra/Http/Response/GetRevisionsMetadataResponseBody.ts
  39. 24 9
      packages/revisions/src/Infra/InversifyExpress/AnnotatedRevisionsController.ts
  40. 70 15
      packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts
  41. 1 1
      packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts
  42. 15 15
      packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts
  43. 25 0
      packages/revisions/src/Infra/TypeORM/TypeORMRevisionRepositoryResolver.ts
  44. 4 39
      packages/revisions/src/Mapping/Http/RevisionHttpMapper.ts
  45. 11 0
      packages/revisions/src/Mapping/Http/RevisionHttpRepresentation.ts
  46. 4 25
      packages/revisions/src/Mapping/Http/RevisionMetadataHttpMapper.ts
  47. 7 0
      packages/revisions/src/Mapping/Http/RevisionMetadataHttpRepresentation.ts
  48. 4 4
      packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper.ts
  49. 17 17
      packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts
  50. 3 6
      packages/syncing-server/src/Bootstrap/Container.ts
  51. 20 12
      packages/syncing-server/src/Domain/Event/DomainEventFactory.ts
  52. 12 4
      packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts
  53. 15 14
      packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts
  54. 12 9
      packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts
  55. 17 22
      packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts
  56. 12 6
      packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts
  57. 17 14
      packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts
  58. 16 8
      packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.ts
  59. 10 5
      packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts
  60. 10 8
      packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts

+ 1 - 0
packages/auth/src/Domain/Event/DomainEventFactory.ts

@@ -280,6 +280,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     userUuid: string
     userCreatedAtTimestamp: number
     regularSubscriptionUuid: string | undefined
+    roleNames: string[]
   }): AccountDeletionRequestedEvent {
     return {
       type: 'ACCOUNT_DELETION_REQUESTED',

+ 1 - 0
packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -45,6 +45,7 @@ export interface DomainEventFactoryInterface {
     userUuid: string
     userCreatedAtTimestamp: number
     regularSubscriptionUuid: string | undefined
+    roleNames: string[]
   }): AccountDeletionRequestedEvent
   createUserRolesChangedEvent(userUuid: string, email: string, currentRoles: string[]): UserRolesChangedEvent
   createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent

+ 1 - 0
packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts

@@ -71,6 +71,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
       userUuid: '00000000-0000-0000-0000-000000000000',
       userCreatedAtTimestamp: 1,
       regularSubscriptionUuid: '2-3-4',
+      roleNames: ['CORE_USER'],
     }
 
     logger = {} as jest.Mocked<Logger>

+ 7 - 0
packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts

@@ -9,6 +9,8 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
 import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
 import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
 import { TimerInterface } from '@standardnotes/time'
+import { RoleName } from '@standardnotes/domain-core'
+import { Role } from '../../Role/Role'
 
 describe('DeleteAccount', () => {
   let userRepository: UserRepositoryInterface
@@ -26,6 +28,7 @@ describe('DeleteAccount', () => {
     user = {
       uuid: '1-2-3',
     } as jest.Mocked<User>
+    user.roles = Promise.resolve([{ name: RoleName.NAMES.CoreUser } as jest.Mocked<Role>])
 
     regularSubscription = {
       uuid: '1-2-3',
@@ -68,6 +71,7 @@ describe('DeleteAccount', () => {
         userUuid: '1-2-3',
         userCreatedAtTimestamp: 1,
         regularSubscriptionUuid: undefined,
+        roleNames: ['CORE_USER'],
       })
     })
 
@@ -85,6 +89,7 @@ describe('DeleteAccount', () => {
         userUuid: '1-2-3',
         userCreatedAtTimestamp: 1,
         regularSubscriptionUuid: '1-2-3',
+        roleNames: ['CORE_USER'],
       })
     })
 
@@ -123,6 +128,7 @@ describe('DeleteAccount', () => {
         userUuid: '1-2-3',
         userCreatedAtTimestamp: 1,
         regularSubscriptionUuid: undefined,
+        roleNames: ['CORE_USER'],
       })
     })
 
@@ -140,6 +146,7 @@ describe('DeleteAccount', () => {
         userUuid: '1-2-3',
         userCreatedAtTimestamp: 1,
         regularSubscriptionUuid: '1-2-3',
+        roleNames: ['CORE_USER'],
       })
     })
 

+ 3 - 0
packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts

@@ -47,6 +47,8 @@ export class DeleteAccount implements UseCaseInterface<string> {
       return Result.ok('User already deleted.')
     }
 
+    const roles = await user.roles
+
     let regularSubscriptionUuid = undefined
     const { regularSubscription } = await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid)
     if (regularSubscription !== null) {
@@ -58,6 +60,7 @@ export class DeleteAccount implements UseCaseInterface<string> {
         userUuid: user.uuid,
         userCreatedAtTimestamp: this.timer.convertDateToMicroseconds(user.createdAt),
         regularSubscriptionUuid,
+        roleNames: roles.map((role) => role.name),
       }),
     )
 

+ 1 - 0
packages/domain-events/src/Domain/Event/AccountDeletionRequestedEventPayload.ts

@@ -1,5 +1,6 @@
 export interface AccountDeletionRequestedEventPayload {
   userUuid: string
+  roleNames: string[]
   userCreatedAtTimestamp: number
   regularSubscriptionUuid: string | undefined
 }

+ 1 - 0
packages/domain-events/src/Domain/Event/DuplicateItemSyncedEventPayload.ts

@@ -1,4 +1,5 @@
 export interface DuplicateItemSyncedEventPayload {
   itemUuid: string
   userUuid: string
+  roleNames: string[]
 }

+ 1 - 0
packages/domain-events/src/Domain/Event/ItemDumpedEventPayload.ts

@@ -1,3 +1,4 @@
 export interface ItemDumpedEventPayload {
   fileDumpPath: string
+  roleNames: string[]
 }

+ 1 - 0
packages/domain-events/src/Domain/Event/ItemRevisionCreationRequestedEventPayload.ts

@@ -1,3 +1,4 @@
 export interface ItemRevisionCreationRequestedEventPayload {
   itemUuid: string
+  roleNames: string[]
 }

+ 1 - 0
packages/domain-events/src/Domain/Event/RevisionsCopyRequestedEventPayload.ts

@@ -1,4 +1,5 @@
 export interface RevisionsCopyRequestedEventPayload {
   newItemUuid: string
   originalItemUuid: string
+  roleNames: string[]
 }

+ 78 - 86
packages/revisions/src/Bootstrap/Container.ts

@@ -6,15 +6,14 @@ import * as winston from 'winston'
 import { Revision } from '../Domain/Revision/Revision'
 import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
 import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
-import { TypeORMRevisionRepository } from '../Infra/TypeORM/SQLRevisionRepository'
-import { TypeORMRevision } from '../Infra/TypeORM/SQLRevision'
+import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository'
+import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
 import { AppDataSource } from './DataSource'
 import { Env } from './Env'
 import TYPES from './Types'
 import { TokenDecoderInterface, CrossServiceTokenData, TokenDecoder } from '@standardnotes/security'
 import { TimerInterface, Timer } from '@standardnotes/time'
 import { ApiGatewayAuthMiddleware } from '../Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware'
-import { RevisionsController } from '../Controller/RevisionsController'
 import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
 import { GetRequiredRoleToViewRevision } from '../Domain/UseCase/GetRequiredRoleToViewRevision/GetRequiredRoleToViewRevision'
 import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
@@ -51,6 +50,10 @@ import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevi
 import { MongoDBRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionMetadataPersistenceMapper'
 import { MongoDBRevisionPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionPersistenceMapper'
 import { RevisionHttpMapper } from '../Mapping/Http/RevisionHttpMapper'
+import { RevisionRepositoryResolverInterface } from '../Domain/Revision/RevisionRepositoryResolverInterface'
+import { TypeORMRevisionRepositoryResolver } from '../Infra/TypeORM/TypeORMRevisionRepositoryResolver'
+import { RevisionMetadataHttpRepresentation } from '../Mapping/Http/RevisionMetadataHttpRepresentation'
+import { RevisionHttpRepresentation } from '../Mapping/Http/RevisionHttpRepresentation'
 
 export class ContainerConfigLoader {
   async load(configuration?: {
@@ -106,10 +109,10 @@ export class ContainerConfigLoader {
 
     // Map
     container
-      .bind<MapperInterface<RevisionMetadata, TypeORMRevision>>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper)
+      .bind<MapperInterface<RevisionMetadata, SQLRevision>>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper)
       .toConstantValue(new SQLRevisionMetadataPersistenceMapper())
     container
-      .bind<MapperInterface<Revision, TypeORMRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper)
+      .bind<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper)
       .toConstantValue(new SQLRevisionPersistenceMapper())
     container
       .bind<MapperInterface<RevisionMetadata, MongoDBRevision>>(
@@ -122,19 +125,19 @@ export class ContainerConfigLoader {
 
     // ORM
     container
-      .bind<Repository<TypeORMRevision>>(TYPES.Revisions_ORMRevisionRepository)
-      .toDynamicValue(() => appDataSource.getRepository(TypeORMRevision))
+      .bind<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository)
+      .toDynamicValue(() => appDataSource.getRepository(SQLRevision))
 
     // Repositories
     container
-      .bind<RevisionRepositoryInterface>(TYPES.Revisions_RevisionRepository)
+      .bind<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository)
       .toConstantValue(
-        new TypeORMRevisionRepository(
-          container.get<Repository<TypeORMRevision>>(TYPES.Revisions_ORMRevisionRepository),
-          container.get<MapperInterface<RevisionMetadata, TypeORMRevision>>(
+        new SQLRevisionRepository(
+          container.get<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository),
+          container.get<MapperInterface<RevisionMetadata, SQLRevision>>(
             TYPES.Revisions_SQLRevisionMetadataPersistenceMapper,
           ),
-          container.get<MapperInterface<Revision, TypeORMRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper),
+          container.get<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper),
           container.get<winston.Logger>(TYPES.Revisions_Logger),
         ),
       )
@@ -158,6 +161,17 @@ export class ContainerConfigLoader {
         )
     }
 
+    container
+      .bind<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver)
+      .toConstantValue(
+        new TypeORMRevisionRepositoryResolver(
+          container.get<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository),
+          isSecondaryDatabaseEnabled
+            ? container.get<RevisionRepositoryInterface>(TYPES.Revisions_MongoDBRevisionRepository)
+            : null,
+        ),
+      )
+
     container.bind<TimerInterface>(TYPES.Revisions_Timer).toDynamicValue(() => new Timer())
 
     container
@@ -168,35 +182,12 @@ export class ContainerConfigLoader {
 
     // Map
     container
-      .bind<
-        MapperInterface<
-          Revision,
-          {
-            uuid: string
-            item_uuid: string
-            content: string | null
-            content_type: string
-            items_key_id: string | null
-            enc_item_key: string | null
-            auth_hash: string | null
-            created_at: string
-            updated_at: string
-          }
-        >
-      >(TYPES.Revisions_RevisionHttpMapper)
+      .bind<MapperInterface<Revision, RevisionHttpRepresentation>>(TYPES.Revisions_RevisionHttpMapper)
       .toDynamicValue(() => new RevisionHttpMapper())
     container
-      .bind<
-        MapperInterface<
-          RevisionMetadata,
-          {
-            uuid: string
-            content_type: string
-            created_at: string
-            updated_at: string
-          }
-        >
-      >(TYPES.Revisions_RevisionMetadataHttpMapper)
+      .bind<MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>>(
+        TYPES.Revisions_RevisionMetadataHttpMapper,
+      )
       .toDynamicValue((context: interfaces.Context) => {
         return new RevisionMetadataHttpMapper(context.container.get(TYPES.Revisions_GetRequiredRoleToViewRevision))
       })
@@ -204,15 +195,30 @@ export class ContainerConfigLoader {
     // use cases
     container
       .bind<GetRevisionsMetada>(TYPES.Revisions_GetRevisionsMetada)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new GetRevisionsMetada(context.container.get(TYPES.Revisions_RevisionRepository))
-      })
-    container.bind<GetRevision>(TYPES.Revisions_GetRevision).toDynamicValue((context: interfaces.Context) => {
-      return new GetRevision(context.container.get(TYPES.Revisions_RevisionRepository))
-    })
-    container.bind<DeleteRevision>(TYPES.Revisions_DeleteRevision).toDynamicValue((context: interfaces.Context) => {
-      return new DeleteRevision(context.container.get(TYPES.Revisions_RevisionRepository))
-    })
+      .toConstantValue(
+        new GetRevisionsMetada(
+          container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
+        ),
+      )
+    container
+      .bind<GetRevision>(TYPES.Revisions_GetRevision)
+      .toConstantValue(
+        new GetRevision(container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver)),
+      )
+    container
+      .bind<DeleteRevision>(TYPES.Revisions_DeleteRevision)
+      .toConstantValue(
+        new DeleteRevision(
+          container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
+        ),
+      )
+    container
+      .bind<CopyRevisions>(TYPES.Revisions_CopyRevisions)
+      .toConstantValue(
+        new CopyRevisions(
+          container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
+        ),
+      )
 
     // env vars
     container.bind(TYPES.Revisions_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
@@ -222,19 +228,6 @@ export class ContainerConfigLoader {
       .bind<ControllerContainerInterface>(TYPES.Revisions_ControllerContainer)
       .toConstantValue(configuration?.controllerConatiner ?? new ControllerContainer())
 
-    container
-      .bind<RevisionsController>(TYPES.Revisions_RevisionsController)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new RevisionsController(
-          context.container.get(TYPES.Revisions_GetRevisionsMetada),
-          context.container.get(TYPES.Revisions_GetRevision),
-          context.container.get(TYPES.Revisions_DeleteRevision),
-          context.container.get(TYPES.Revisions_RevisionHttpMapper),
-          context.container.get(TYPES.Revisions_RevisionMetadataHttpMapper),
-          context.container.get(TYPES.Revisions_Logger),
-        )
-      })
-
     container
       .bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.Revisions_CrossServiceTokenDecoder)
       .toDynamicValue((context: interfaces.Context) => {
@@ -308,36 +301,31 @@ export class ContainerConfigLoader {
           : new FSDumpRepository(container.get(TYPES.Revisions_RevisionItemStringMapper)),
       )
 
-    // use cases
-    container.bind<CopyRevisions>(TYPES.Revisions_CopyRevisions).toDynamicValue((context: interfaces.Context) => {
-      return new CopyRevisions(context.container.get(TYPES.Revisions_RevisionRepository))
-    })
-
     // Handlers
     container
       .bind<ItemDumpedEventHandler>(TYPES.Revisions_ItemDumpedEventHandler)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new ItemDumpedEventHandler(
-          context.container.get(TYPES.Revisions_DumpRepository),
-          context.container.get(TYPES.Revisions_RevisionRepository),
-        )
-      })
+      .toConstantValue(
+        new ItemDumpedEventHandler(
+          container.get<DumpRepositoryInterface>(TYPES.Revisions_DumpRepository),
+          container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
+        ),
+      )
     container
       .bind<AccountDeletionRequestedEventHandler>(TYPES.Revisions_AccountDeletionRequestedEventHandler)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new AccountDeletionRequestedEventHandler(
-          context.container.get(TYPES.Revisions_RevisionRepository),
-          context.container.get(TYPES.Revisions_Logger),
-        )
-      })
+      .toConstantValue(
+        new AccountDeletionRequestedEventHandler(
+          container.get<RevisionRepositoryResolverInterface>(TYPES.Revisions_RevisionRepositoryResolver),
+          container.get<winston.Logger>(TYPES.Revisions_Logger),
+        ),
+      )
     container
       .bind<RevisionsCopyRequestedEventHandler>(TYPES.Revisions_RevisionsCopyRequestedEventHandler)
-      .toDynamicValue((context: interfaces.Context) => {
-        return new RevisionsCopyRequestedEventHandler(
-          context.container.get(TYPES.Revisions_CopyRevisions),
-          context.container.get(TYPES.Revisions_Logger),
-        )
-      })
+      .toConstantValue(
+        new RevisionsCopyRequestedEventHandler(
+          container.get<CopyRevisions>(TYPES.Revisions_CopyRevisions),
+          container.get<winston.Logger>(TYPES.Revisions_Logger),
+        ),
+      )
 
     const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
       ['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)],
@@ -380,8 +368,12 @@ export class ContainerConfigLoader {
         .bind<BaseRevisionsController>(TYPES.Revisions_BaseRevisionsController)
         .toConstantValue(
           new BaseRevisionsController(
-            container.get(TYPES.Revisions_RevisionsController),
-            container.get(TYPES.Revisions_ControllerContainer),
+            container.get<GetRevisionsMetada>(TYPES.Revisions_GetRevisionsMetada),
+            container.get<GetRevision>(TYPES.Revisions_GetRevision),
+            container.get<DeleteRevision>(TYPES.Revisions_DeleteRevision),
+            container.get<RevisionHttpMapper>(TYPES.Revisions_RevisionHttpMapper),
+            container.get<RevisionMetadataHttpMapper>(TYPES.Revisions_RevisionMetadataHttpMapper),
+            container.get<ControllerContainerInterface>(TYPES.Revisions_ControllerContainer),
           ),
         )
     }

+ 2 - 2
packages/revisions/src/Bootstrap/DataSource.ts

@@ -1,7 +1,7 @@
 import { DataSource, EntityTarget, LoggerOptions, MongoRepository, ObjectLiteral, Repository } from 'typeorm'
 import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
 
-import { TypeORMRevision } from '../Infra/TypeORM/SQLRevision'
+import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
 
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
@@ -71,7 +71,7 @@ export class AppDataSource {
 
     const commonDataSourceOptions = {
       maxQueryExecutionTime,
-      entities: [TypeORMRevision],
+      entities: [SQLRevision],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrationsRun: true,
       logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',

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

@@ -17,9 +17,10 @@ const TYPES = {
   // Mongo
   Revisions_ORMMongoRevisionRepository: Symbol.for('Revisions_ORMMongoRevisionRepository'),
   // Repositories
-  Revisions_RevisionRepository: Symbol.for('Revisions_RevisionRepository'),
+  Revisions_SQLRevisionRepository: Symbol.for('Revisions_SQLRevisionRepository'),
   Revisions_MongoDBRevisionRepository: Symbol.for('Revisions_MongoDBRevisionRepository'),
   Revisions_DumpRepository: Symbol.for('Revisions_DumpRepository'),
+  Revisions_RevisionRepositoryResolver: Symbol.for('Revisions_RevisionRepositoryResolver'),
   // env vars
   Revisions_AUTH_JWT_SECRET: Symbol.for('Revisions_AUTH_JWT_SECRET'),
   Revisions_SQS_QUEUE_URL: Symbol.for('Revisions_SQS_QUEUE_URL'),

+ 0 - 129
packages/revisions/src/Controller/RevisionsController.ts

@@ -1,129 +0,0 @@
-import { Logger } from 'winston'
-import { HttpResponse, HttpStatusCode } from '@standardnotes/responses'
-
-import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
-import { GetRevisionsMetadataRequestParams } from '../Infra/Http/Request/GetRevisionsMetadataRequestParams'
-import { GetRevisionRequestParams } from '../Infra/Http/Request/GetRevisionRequestParams'
-import { DeleteRevisionRequestParams } from '../Infra/Http/Request/DeleteRevisionRequestParams'
-import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision'
-import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision'
-import { GetRevisionsMetadataResponseBody } from '../Infra/Http/Response/GetRevisionsMetadataResponseBody'
-import { GetRevisionResponseBody } from '../Infra/Http/Response/GetRevisionResponseBody'
-import { MapperInterface } from '@standardnotes/domain-core'
-import { Revision } from '../Domain/Revision/Revision'
-import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
-
-export class RevisionsController {
-  constructor(
-    private getRevisionsMetadata: GetRevisionsMetada,
-    private doGetRevision: GetRevision,
-    private doDeleteRevision: DeleteRevision,
-    private revisionHttpMapper: MapperInterface<
-      Revision,
-      {
-        uuid: string
-        itemUuid: string
-        content: string | null
-        contentType: string
-        itemsKeyId: string | null
-        encItemKey: string | null
-        authHash: string | null
-        createAt: string
-        updateAt: string
-      }
-    >,
-    private revisionMetadataHttpMapper: MapperInterface<
-      RevisionMetadata,
-      {
-        uuid: string
-        contentType: string
-        createdAt: string
-        updatedAt: string
-      }
-    >,
-    private logger: Logger,
-  ) {}
-
-  async getRevisions(
-    params: GetRevisionsMetadataRequestParams,
-  ): Promise<HttpResponse<GetRevisionsMetadataResponseBody>> {
-    const revisionMetadataOrError = await this.getRevisionsMetadata.execute({
-      itemUuid: params.itemUuid,
-      userUuid: params.userUuid,
-    })
-
-    if (revisionMetadataOrError.isFailed()) {
-      this.logger.warn(revisionMetadataOrError.getError())
-
-      return {
-        status: HttpStatusCode.BadRequest,
-        data: {
-          error: {
-            message: 'Could not retrieve revisions.',
-          },
-        },
-      }
-    }
-
-    const revisions = revisionMetadataOrError.getValue()
-
-    this.logger.debug(`Found ${revisions.length} revisions for item ${params.itemUuid}`)
-
-    return {
-      status: HttpStatusCode.Success,
-      data: {
-        revisions: revisions.map((revision) => this.revisionMetadataHttpMapper.toProjection(revision)),
-      },
-    }
-  }
-
-  async getRevision(params: GetRevisionRequestParams): Promise<HttpResponse<GetRevisionResponseBody>> {
-    const revisionOrError = await this.doGetRevision.execute({
-      revisionUuid: params.revisionUuid,
-      userUuid: params.userUuid,
-    })
-
-    if (revisionOrError.isFailed()) {
-      this.logger.warn(revisionOrError.getError())
-
-      return {
-        status: HttpStatusCode.BadRequest,
-        data: {
-          error: {
-            message: 'Could not retrieve revision.',
-          },
-        },
-      }
-    }
-
-    return {
-      status: HttpStatusCode.Success,
-      data: { revision: this.revisionHttpMapper.toProjection(revisionOrError.getValue()) },
-    }
-  }
-
-  async deleteRevision(params: DeleteRevisionRequestParams): Promise<HttpResponse> {
-    const revisionOrError = await this.doDeleteRevision.execute({
-      revisionUuid: params.revisionUuid,
-      userUuid: params.userUuid,
-    })
-
-    if (revisionOrError.isFailed()) {
-      this.logger.warn(revisionOrError.getError())
-
-      return {
-        status: HttpStatusCode.BadRequest,
-        data: {
-          error: {
-            message: 'Could not delete revision.',
-          },
-        },
-      }
-    }
-
-    return {
-      status: HttpStatusCode.Success,
-      data: { message: revisionOrError.getValue() },
-    }
-  }
-}

+ 17 - 1
packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts

@@ -4,21 +4,27 @@ import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
 import { Logger } from 'winston'
 import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
 import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
+import { RevisionRepositoryResolverInterface } from '../Revision/RevisionRepositoryResolverInterface'
 
 describe('AccountDeletionRequestedEventHandler', () => {
   let revisionRepository: RevisionRepositoryInterface
+  let revisionRepositoryResolver: RevisionRepositoryResolverInterface
   let logger: Logger
   let event: AccountDeletionRequestedEvent
 
-  const createHandler = () => new AccountDeletionRequestedEventHandler(revisionRepository, logger)
+  const createHandler = () => new AccountDeletionRequestedEventHandler(revisionRepositoryResolver, logger)
 
   beforeEach(() => {
     revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
     revisionRepository.removeByUserUuid = jest.fn()
 
+    revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
+    revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
+
     logger = {} as jest.Mocked<Logger>
     logger.info = jest.fn()
     logger.warn = jest.fn()
+    logger.error = jest.fn()
 
     event = {} as jest.Mocked<AccountDeletionRequestedEvent>
     event.createdAt = new Date(1)
@@ -26,6 +32,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
       userUuid: '2-3-4',
       userCreatedAtTimestamp: 1,
       regularSubscriptionUuid: '1-2-3',
+      roleNames: ['CORE_USER'],
     }
   })
 
@@ -42,4 +49,13 @@ describe('AccountDeletionRequestedEventHandler', () => {
 
     expect(revisionRepository.removeByUserUuid).not.toHaveBeenCalled()
   })
+
+  it('should do nothing if role names are not valid', async () => {
+    event.payload.userUuid = '84c0f8e8-544a-4c7e-9adf-26209303bc1d'
+    event.payload.roleNames = ['INVALID_ROLE_NAME']
+
+    await createHandler().handle(event)
+
+    expect(revisionRepository.removeByUserUuid).not.toHaveBeenCalled()
+  })
 })

+ 14 - 4
packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts

@@ -1,11 +1,11 @@
-import { Uuid } from '@standardnotes/domain-core'
+import { RoleNameCollection, Uuid } from '@standardnotes/domain-core'
 import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
 import { Logger } from 'winston'
 
-import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
+import { RevisionRepositoryResolverInterface } from '../Revision/RevisionRepositoryResolverInterface'
 
 export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
-  constructor(private revisionRepository: RevisionRepositoryInterface, private logger: Logger) {}
+  constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface, private logger: Logger) {}
 
   async handle(event: AccountDeletionRequestedEvent): Promise<void> {
     const userUuidOrError = Uuid.create(event.payload.userUuid)
@@ -16,7 +16,17 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
     }
     const userUuid = userUuidOrError.getValue()
 
-    await this.revisionRepository.removeByUserUuid(userUuid)
+    const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      this.logger.error(`Failed account cleanup: ${roleNamesOrError.getError()}`)
+
+      return
+    }
+    const roleNames = roleNamesOrError.getValue()
+
+    const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
+
+    await revisionRepository.removeByUserUuid(userUuid)
 
     this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
   }

+ 16 - 1
packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.spec.ts

@@ -3,14 +3,16 @@ import { DumpRepositoryInterface } from '../Dump/DumpRepositoryInterface'
 import { Revision } from '../Revision/Revision'
 import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
 import { ItemDumpedEventHandler } from './ItemDumpedEventHandler'
+import { RevisionRepositoryResolverInterface } from '../Revision/RevisionRepositoryResolverInterface'
 
 describe('ItemDumpedEventHandler', () => {
   let dumpRepository: DumpRepositoryInterface
   let revisionRepository: RevisionRepositoryInterface
+  let revisionRepositoryResolver: RevisionRepositoryResolverInterface
   let revision: Revision
   let event: ItemDumpedEvent
 
-  const createHandler = () => new ItemDumpedEventHandler(dumpRepository, revisionRepository)
+  const createHandler = () => new ItemDumpedEventHandler(dumpRepository, revisionRepositoryResolver)
 
   beforeEach(() => {
     revision = {} as jest.Mocked<Revision>
@@ -22,9 +24,13 @@ describe('ItemDumpedEventHandler', () => {
     revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
     revisionRepository.save = jest.fn()
 
+    revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
+    revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
+
     event = {} as jest.Mocked<ItemDumpedEvent>
     event.payload = {
       fileDumpPath: 'foobar',
+      roleNames: ['CORE_USER'],
     }
   })
 
@@ -35,6 +41,15 @@ describe('ItemDumpedEventHandler', () => {
     expect(dumpRepository.removeDump).toHaveBeenCalled()
   })
 
+  it('should do nothing if role names are not valid', async () => {
+    event.payload.roleNames = ['INVALID_ROLE_NAME']
+
+    await createHandler().handle(event)
+
+    expect(revisionRepository.save).not.toHaveBeenCalled()
+    expect(dumpRepository.removeDump).toHaveBeenCalled()
+  })
+
   it('should not save a revision if it could not be created from dump', async () => {
     dumpRepository.getRevisionFromDumpPath = jest.fn().mockReturnValue(null)
 

+ 14 - 3
packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.ts

@@ -1,12 +1,13 @@
 import { DomainEventHandlerInterface, ItemDumpedEvent } from '@standardnotes/domain-events'
 
 import { DumpRepositoryInterface } from '../Dump/DumpRepositoryInterface'
-import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
+import { RevisionRepositoryResolverInterface } from '../Revision/RevisionRepositoryResolverInterface'
+import { RoleNameCollection } from '@standardnotes/domain-core'
 
 export class ItemDumpedEventHandler implements DomainEventHandlerInterface {
   constructor(
     private dumpRepository: DumpRepositoryInterface,
-    private revisionRepository: RevisionRepositoryInterface,
+    private revisionRepositoryResolver: RevisionRepositoryResolverInterface,
   ) {}
 
   async handle(event: ItemDumpedEvent): Promise<void> {
@@ -17,7 +18,17 @@ export class ItemDumpedEventHandler implements DomainEventHandlerInterface {
       return
     }
 
-    await this.revisionRepository.save(revision)
+    const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      await this.dumpRepository.removeDump(event.payload.fileDumpPath)
+
+      return
+    }
+    const roleNames = roleNamesOrError.getValue()
+
+    const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
+
+    await revisionRepository.save(revision)
 
     await this.dumpRepository.removeDump(event.payload.fileDumpPath)
   }

+ 1 - 0
packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.spec.ts

@@ -23,6 +23,7 @@ describe('RevisionsCopyRequestedEventHandler', () => {
     event.payload = {
       newItemUuid: '1-2-3',
       originalItemUuid: '2-3-4',
+      roleNames: ['CORE_USER'],
     }
   })
 

+ 1 - 0
packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.ts

@@ -9,6 +9,7 @@ export class RevisionsCopyRequestedEventHandler implements DomainEventHandlerInt
     const result = await this.copyRevisions.execute({
       newItemUuid: event.payload.newItemUuid,
       originalItemUuid: event.payload.originalItemUuid,
+      roleNames: event.payload.roleNames,
     })
 
     if (result.isFailed()) {

+ 7 - 0
packages/revisions/src/Domain/Revision/RevisionRepositoryResolverInterface.ts

@@ -0,0 +1,7 @@
+import { RoleNameCollection } from '@standardnotes/domain-core'
+
+import { RevisionRepositoryInterface } from './RevisionRepositoryInterface'
+
+export interface RevisionRepositoryResolverInterface {
+  resolve(roleNames: RoleNameCollection): RevisionRepositoryInterface
+}

+ 20 - 1
packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.spec.ts

@@ -2,16 +2,21 @@ import { Result } from '@standardnotes/domain-core'
 import { Revision } from '../../Revision/Revision'
 import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
 import { CopyRevisions } from './CopyRevisions'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 
 describe('CopyRevisions', () => {
   let revisionRepository: RevisionRepositoryInterface
+  let revisionRepositoryResolver: RevisionRepositoryResolverInterface
 
-  const createUseCase = () => new CopyRevisions(revisionRepository)
+  const createUseCase = () => new CopyRevisions(revisionRepositoryResolver)
 
   beforeEach(() => {
     revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
     revisionRepository.findByItemUuid = jest.fn().mockReturnValue([{} as jest.Mocked<Revision>])
     revisionRepository.save = jest.fn()
+
+    revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
+    revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
   })
 
   it('should not copy revisions to new item if revision creation fails', async () => {
@@ -21,6 +26,7 @@ describe('CopyRevisions', () => {
     const result = await createUseCase().execute({
       originalItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       newItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -28,10 +34,21 @@ describe('CopyRevisions', () => {
     revisionMock.mockRestore()
   })
 
+  it('should do nothing if the role names are not valid', async () => {
+    const result = await createUseCase().execute({
+      originalItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      newItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['INVALID_ROLE_NAME'],
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
   it('should copy revisions to new item', async () => {
     const result = await createUseCase().execute({
       originalItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       newItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -43,6 +60,7 @@ describe('CopyRevisions', () => {
     const result = await createUseCase().execute({
       originalItemUuid: '1-2-3',
       newItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -52,6 +70,7 @@ describe('CopyRevisions', () => {
     const result = await createUseCase().execute({
       newItemUuid: '1-2-3',
       originalItemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()

+ 13 - 5
packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.ts

@@ -1,12 +1,12 @@
-import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 
 import { Revision } from '../../Revision/Revision'
-import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
 
 import { CopyRevisionsDTO } from './CopyRevisionsDTO'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 
 export class CopyRevisions implements UseCaseInterface<string> {
-  constructor(private revisionRepository: RevisionRepositoryInterface) {}
+  constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {}
 
   async execute(dto: CopyRevisionsDTO): Promise<Result<string>> {
     const orignalItemUuidOrError = Uuid.create(dto.originalItemUuid)
@@ -21,7 +21,15 @@ export class CopyRevisions implements UseCaseInterface<string> {
     }
     const newItemUuid = newItemUuidOrError.getValue()
 
-    const revisions = await this.revisionRepository.findByItemUuid(originalItemUuid)
+    const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return Result.fail(roleNamesOrError.getError())
+    }
+    const roleNames = roleNamesOrError.getValue()
+
+    const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
+
+    const revisions = await revisionRepository.findByItemUuid(originalItemUuid)
 
     for (const existingRevision of revisions) {
       const revisionCopyOrError = Revision.create({
@@ -35,7 +43,7 @@ export class CopyRevisions implements UseCaseInterface<string> {
 
       const revisionCopy = revisionCopyOrError.getValue()
 
-      await this.revisionRepository.save(revisionCopy)
+      await revisionRepository.save(revisionCopy)
     }
 
     return Result.ok<string>('Revisions copied')

+ 1 - 0
packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisionsDTO.ts

@@ -1,4 +1,5 @@
 export interface CopyRevisionsDTO {
   originalItemUuid: string
   newItemUuid: string
+  roleNames: string[]
 }

+ 19 - 1
packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts

@@ -1,30 +1,47 @@
 import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 import { DeleteRevision } from './DeleteRevision'
 
 describe('DeleteRevision', () => {
   let revisionRepository: RevisionRepositoryInterface
+  let revisionRepositoryResolver: RevisionRepositoryResolverInterface
 
-  const createUseCase = () => new DeleteRevision(revisionRepository)
+  const createUseCase = () => new DeleteRevision(revisionRepositoryResolver)
 
   beforeEach(() => {
     revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
     revisionRepository.removeOneByUuid = jest.fn()
+
+    revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
+    revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
   })
 
   it('should delete revision', async () => {
     const result = await createUseCase().execute({
       revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeFalsy()
     expect(result.getValue()).toEqual('Revision removed')
   })
 
+  it('should do nothing if role names are not valid', async () => {
+    const result = await createUseCase().execute({
+      revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['INVALID_ROLE_NAME'],
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
   it('should not delete revision for an invalid item uuid', async () => {
     const result = await createUseCase().execute({
       revisionUuid: '1-2-3',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -34,6 +51,7 @@ describe('DeleteRevision', () => {
     const result = await createUseCase().execute({
       userUuid: '1-2-3',
       revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()

+ 12 - 4
packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts

@@ -1,10 +1,10 @@
-import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 
-import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
 import { DeleteRevisionDTO } from './DeleteRevisionDTO'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 
 export class DeleteRevision implements UseCaseInterface<string> {
-  constructor(private revisionRepository: RevisionRepositoryInterface) {}
+  constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {}
 
   async execute(dto: DeleteRevisionDTO): Promise<Result<string>> {
     const revisionUuidOrError = Uuid.create(dto.revisionUuid)
@@ -19,7 +19,15 @@ export class DeleteRevision implements UseCaseInterface<string> {
     }
     const userUuid = userUuidOrError.getValue()
 
-    await this.revisionRepository.removeOneByUuid(revisionUuid, userUuid)
+    const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return Result.fail(roleNamesOrError.getError())
+    }
+    const roleNames = roleNamesOrError.getValue()
+
+    const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
+
+    await revisionRepository.removeOneByUuid(revisionUuid, userUuid)
 
     return Result.ok<string>('Revision removed')
   }

+ 1 - 0
packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts

@@ -1,4 +1,5 @@
 export interface DeleteRevisionDTO {
   userUuid: string
   revisionUuid: string
+  roleNames: string[]
 }

+ 20 - 1
packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.spec.ts

@@ -1,33 +1,50 @@
 import { Revision } from '../../Revision/Revision'
 import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 import { GetRevision } from './GetRevision'
 
 describe('GetRevision', () => {
   let revisionRepository: RevisionRepositoryInterface
+  let revisionRepositoryResolver: RevisionRepositoryResolverInterface
 
-  const createUseCase = () => new GetRevision(revisionRepository)
+  const createUseCase = () => new GetRevision(revisionRepositoryResolver)
 
   beforeEach(() => {
     revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
     revisionRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked<Revision>)
+
+    revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
+    revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
   })
 
   it('should return revision for a given item', async () => {
     const result = await createUseCase().execute({
       revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeFalsy()
     expect(result.getValue()).not.toBeNull()
   })
 
+  it('should do nothing if role names are not valid', async () => {
+    const result = await createUseCase().execute({
+      revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['INVALID_ROLE_NAME'],
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
   it('should not return revision for a given item if not found', async () => {
     revisionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
 
     const result = await createUseCase().execute({
       revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -37,6 +54,7 @@ describe('GetRevision', () => {
     const result = await createUseCase().execute({
       revisionUuid: '1-2-3',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -46,6 +64,7 @@ describe('GetRevision', () => {
     const result = await createUseCase().execute({
       userUuid: '1-2-3',
       revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()

+ 12 - 4
packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts

@@ -1,11 +1,11 @@
-import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 
 import { Revision } from '../../Revision/Revision'
-import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
 import { GetRevisionDTO } from './GetRevisionDTO'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 
 export class GetRevision implements UseCaseInterface<Revision> {
-  constructor(private revisionRepository: RevisionRepositoryInterface) {}
+  constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {}
 
   async execute(dto: GetRevisionDTO): Promise<Result<Revision>> {
     const revisionUuidOrError = Uuid.create(dto.revisionUuid)
@@ -20,7 +20,15 @@ export class GetRevision implements UseCaseInterface<Revision> {
     }
     const userUuid = userUuidOrError.getValue()
 
-    const revision = await this.revisionRepository.findOneByUuid(revisionUuid, userUuid)
+    const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return Result.fail(roleNamesOrError.getError())
+    }
+    const roleNames = roleNamesOrError.getValue()
+
+    const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
+
+    const revision = await revisionRepository.findOneByUuid(revisionUuid, userUuid)
 
     if (revision === null) {
       return Result.fail<Revision>(`Could not find revision with uuid: ${revisionUuid.value}`)

+ 1 - 0
packages/revisions/src/Domain/UseCase/GetRevision/GetRevisionDTO.ts

@@ -1,4 +1,5 @@
 export interface GetRevisionDTO {
   userUuid: string
   revisionUuid: string
+  roleNames: string[]
 }

+ 19 - 1
packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts

@@ -1,21 +1,27 @@
 import { RevisionMetadata } from '../../Revision/RevisionMetadata'
 import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 import { GetRevisionsMetada } from './GetRevisionsMetada'
 
 describe('GetRevisionsMetada', () => {
   let revisionRepository: RevisionRepositoryInterface
+  let revisionRepositoryResolver: RevisionRepositoryResolverInterface
 
-  const createUseCase = () => new GetRevisionsMetada(revisionRepository)
+  const createUseCase = () => new GetRevisionsMetada(revisionRepositoryResolver)
 
   beforeEach(() => {
     revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
     revisionRepository.findMetadataByItemId = jest.fn().mockReturnValue([{} as jest.Mocked<RevisionMetadata>])
+
+    revisionRepositoryResolver = {} as jest.Mocked<RevisionRepositoryResolverInterface>
+    revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository)
   })
 
   it('should return revisions metadata for a given item', async () => {
     const result = await createUseCase().execute({
       itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -26,6 +32,7 @@ describe('GetRevisionsMetada', () => {
     const result = await createUseCase().execute({
       itemUuid: '1-2-3',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -35,6 +42,17 @@ describe('GetRevisionsMetada', () => {
     const result = await createUseCase().execute({
       userUuid: '1-2-3',
       itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should do nothing if role names are not valid', async () => {
+    const result = await createUseCase().execute({
+      itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['INVALID_ROLE_NAME'],
     })
 
     expect(result.isFailed()).toBeTruthy()

+ 12 - 4
packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts

@@ -1,12 +1,12 @@
-import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { Result, RoleNameCollection, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 
 import { RevisionMetadata } from '../../Revision/RevisionMetadata'
-import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface'
 
 import { GetRevisionsMetadaDTO } from './GetRevisionsMetadaDTO'
+import { RevisionRepositoryResolverInterface } from '../../Revision/RevisionRepositoryResolverInterface'
 
 export class GetRevisionsMetada implements UseCaseInterface<RevisionMetadata[]> {
-  constructor(private revisionRepository: RevisionRepositoryInterface) {}
+  constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {}
 
   async execute(dto: GetRevisionsMetadaDTO): Promise<Result<RevisionMetadata[]>> {
     const itemUuidOrError = Uuid.create(dto.itemUuid)
@@ -19,7 +19,15 @@ export class GetRevisionsMetada implements UseCaseInterface<RevisionMetadata[]>
       return Result.fail<RevisionMetadata[]>(`Could not get revisions: ${userUuidOrError.getError()}`)
     }
 
-    const revisionsMetdata = await this.revisionRepository.findMetadataByItemId(
+    const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return Result.fail(roleNamesOrError.getError())
+    }
+    const roleNames = roleNamesOrError.getValue()
+
+    const revisionRepository = this.revisionRepositoryResolver.resolve(roleNames)
+
+    const revisionsMetdata = await revisionRepository.findMetadataByItemId(
       itemUuidOrError.getValue(),
       userUuidOrError.getValue(),
     )

+ 1 - 0
packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts

@@ -1,4 +1,5 @@
 export interface GetRevisionsMetadaDTO {
   itemUuid: string
   userUuid: string
+  roleNames: string[]
 }

+ 0 - 4
packages/revisions/src/Infra/Http/Request/DeleteRevisionRequestParams.ts

@@ -1,4 +0,0 @@
-export interface DeleteRevisionRequestParams {
-  revisionUuid: string
-  userUuid: string
-}

+ 0 - 4
packages/revisions/src/Infra/Http/Request/GetRevisionRequestParams.ts

@@ -1,4 +0,0 @@
-export interface GetRevisionRequestParams {
-  revisionUuid: string
-  userUuid: string
-}

+ 0 - 4
packages/revisions/src/Infra/Http/Request/GetRevisionsMetadataRequestParams.ts

@@ -1,4 +0,0 @@
-export interface GetRevisionsMetadataRequestParams {
-  itemUuid: string
-  userUuid: string
-}

+ 0 - 13
packages/revisions/src/Infra/Http/Response/GetRevisionResponseBody.ts

@@ -1,13 +0,0 @@
-export interface GetRevisionResponseBody {
-  revision: {
-    uuid: string
-    itemUuid: string
-    content: string | null
-    contentType: string
-    itemsKeyId: string | null
-    encItemKey: string | null
-    authHash: string | null
-    createAt: string
-    updateAt: string
-  }
-}

+ 0 - 8
packages/revisions/src/Infra/Http/Response/GetRevisionsMetadataResponseBody.ts

@@ -1,8 +0,0 @@
-export interface GetRevisionsMetadataResponseBody {
-  revisions: Array<{
-    uuid: string
-    contentType: string
-    createdAt: string
-    updatedAt: string
-  }>
-}

+ 24 - 9
packages/revisions/src/Infra/InversifyExpress/AnnotatedRevisionsController.ts

@@ -3,27 +3,42 @@ import { controller, httpDelete, httpGet, results } from 'inversify-express-util
 import { inject } from 'inversify'
 
 import TYPES from '../../Bootstrap/Types'
-import { RevisionsController } from '../../Controller/RevisionsController'
 import { BaseRevisionsController } from './Base/BaseRevisionsController'
+import { GetRevisionsMetada } from '../../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
+import { GetRevision } from '../../Domain/UseCase/GetRevision/GetRevision'
+import { DeleteRevision } from '../../Domain/UseCase/DeleteRevision/DeleteRevision'
+import { MapperInterface } from '@standardnotes/domain-core'
+import { Revision } from '../../Domain/Revision/Revision'
+import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
+import { RevisionHttpRepresentation } from '../../Mapping/Http/RevisionHttpRepresentation'
+import { RevisionMetadataHttpRepresentation } from '../../Mapping/Http/RevisionMetadataHttpRepresentation'
 
 @controller('/items/:itemUuid/revisions', TYPES.Revisions_ApiGatewayAuthMiddleware)
 export class AnnotatedRevisionsController extends BaseRevisionsController {
-  constructor(@inject(TYPES.Revisions_RevisionsController) override revisionsController: RevisionsController) {
-    super(revisionsController)
+  constructor(
+    @inject(TYPES.Revisions_GetRevisionsMetada) override getRevisionsMetadata: GetRevisionsMetada,
+    @inject(TYPES.Revisions_GetRevision) override doGetRevision: GetRevision,
+    @inject(TYPES.Revisions_DeleteRevision) override doDeleteRevision: DeleteRevision,
+    @inject(TYPES.Revisions_RevisionHttpMapper)
+    override revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>,
+    @inject(TYPES.Revisions_RevisionMetadataHttpMapper)
+    override revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>,
+  ) {
+    super(getRevisionsMetadata, doGetRevision, doDeleteRevision, revisionHttpMapper, revisionMetadataHttpMapper)
   }
 
   @httpGet('/')
-  override async getRevisions(req: Request, response: Response): Promise<results.JsonResult> {
-    return super.getRevisions(req, response)
+  override async getRevisions(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getRevisions(request, response)
   }
 
   @httpGet('/:uuid')
-  override async getRevision(req: Request, response: Response): Promise<results.JsonResult> {
-    return super.getRevision(req, response)
+  override async getRevision(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getRevision(request, response)
   }
 
   @httpDelete('/:uuid')
-  override async deleteRevision(req: Request, response: Response): Promise<results.JsonResult> {
-    return super.deleteRevision(req, response)
+  override async deleteRevision(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.deleteRevision(request, response)
   }
 }

+ 70 - 15
packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts

@@ -1,12 +1,24 @@
+import { HttpStatusCode } from '@standardnotes/responses'
+import { Role } from '@standardnotes/security'
 import { BaseHttpController, results } from 'inversify-express-utils'
 import { Request, Response } from 'express'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
 
-import { RevisionsController } from '../../../Controller/RevisionsController'
+import { Revision } from '../../../Domain/Revision/Revision'
+import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
+import { DeleteRevision } from '../../../Domain/UseCase/DeleteRevision/DeleteRevision'
+import { GetRevision } from '../../../Domain/UseCase/GetRevision/GetRevision'
+import { GetRevisionsMetada } from '../../../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada'
+import { RevisionHttpRepresentation } from '../../../Mapping/Http/RevisionHttpRepresentation'
+import { RevisionMetadataHttpRepresentation } from '../../../Mapping/Http/RevisionMetadataHttpRepresentation'
 
 export class BaseRevisionsController extends BaseHttpController {
   constructor(
-    protected revisionsController: RevisionsController,
+    protected getRevisionsMetadata: GetRevisionsMetada,
+    protected doGetRevision: GetRevision,
+    protected doDeleteRevision: DeleteRevision,
+    protected revisionHttpMapper: MapperInterface<Revision, RevisionHttpRepresentation>,
+    protected revisionMetadataHttpMapper: MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>,
     private controllerContainer?: ControllerContainerInterface,
   ) {
     super()
@@ -18,30 +30,73 @@ export class BaseRevisionsController extends BaseHttpController {
     }
   }
 
-  async getRevisions(req: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.revisionsController.getRevisions({
-      itemUuid: req.params.itemUuid,
+  async getRevisions(request: Request, response: Response): Promise<results.JsonResult> {
+    const revisionMetadataOrError = await this.getRevisionsMetadata.execute({
+      itemUuid: request.params.itemUuid,
       userUuid: response.locals.user.uuid,
+      roleNames: response.locals.roles.map((role: Role) => role.name),
     })
 
-    return this.json(result.data, result.status)
+    if (revisionMetadataOrError.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: 'Could not retrieve revisions.',
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+    const revisions = revisionMetadataOrError.getValue()
+
+    return this.json({
+      revisions: revisions.map((revision) => this.revisionMetadataHttpMapper.toProjection(revision)),
+    })
   }
 
-  async getRevision(req: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.revisionsController.getRevision({
-      revisionUuid: req.params.uuid,
+  async getRevision(request: Request, response: Response): Promise<results.JsonResult> {
+    const revisionOrError = await this.doGetRevision.execute({
+      revisionUuid: request.params.uuid,
       userUuid: response.locals.user.uuid,
+      roleNames: response.locals.roles.map((role: Role) => role.name),
     })
 
-    return this.json(result.data, result.status)
+    if (revisionOrError.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: 'Could not retrieve revision.',
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      revision: this.revisionHttpMapper.toProjection(revisionOrError.getValue()),
+    })
   }
 
-  async deleteRevision(req: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.revisionsController.deleteRevision({
-      revisionUuid: req.params.uuid,
+  async deleteRevision(request: Request, response: Response): Promise<results.JsonResult> {
+    const revisionOrError = await this.doDeleteRevision.execute({
+      revisionUuid: request.params.uuid,
       userUuid: response.locals.user.uuid,
+      roleNames: response.locals.roles.map((role: Role) => role.name),
     })
 
-    return this.json(result.data, result.status)
+    if (revisionOrError.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: 'Could not delete revision.',
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      message: revisionOrError.getValue(),
+    })
   }
 }

+ 1 - 1
packages/revisions/src/Infra/TypeORM/SQLRevision.ts → packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts

@@ -1,7 +1,7 @@
 import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
 
 @Entity({ name: 'revisions' })
-export class TypeORMRevision {
+export class SQLRevision {
   @PrimaryGeneratedColumn('uuid')
   declare uuid: string
 

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

@@ -2,16 +2,16 @@ import { MapperInterface, Uuid } from '@standardnotes/domain-core'
 import { Repository } from 'typeorm'
 import { Logger } from 'winston'
 
-import { Revision } from '../../Domain/Revision/Revision'
-import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
-import { RevisionRepositoryInterface } from '../../Domain/Revision/RevisionRepositoryInterface'
-import { TypeORMRevision } from './SQLRevision'
+import { Revision } from '../../../Domain/Revision/Revision'
+import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
+import { RevisionRepositoryInterface } from '../../../Domain/Revision/RevisionRepositoryInterface'
+import { SQLRevision } from './SQLRevision'
 
-export class TypeORMRevisionRepository implements RevisionRepositoryInterface {
+export class SQLRevisionRepository implements RevisionRepositoryInterface {
   constructor(
-    private ormRepository: Repository<TypeORMRevision>,
-    private revisionMetadataMapper: MapperInterface<RevisionMetadata, TypeORMRevision>,
-    private revisionMapper: MapperInterface<Revision, TypeORMRevision>,
+    private ormRepository: Repository<SQLRevision>,
+    private revisionMetadataMapper: MapperInterface<RevisionMetadata, SQLRevision>,
+    private revisionMapper: MapperInterface<Revision, SQLRevision>,
     private logger: Logger,
   ) {}
 
@@ -27,13 +27,13 @@ export class TypeORMRevisionRepository implements RevisionRepositoryInterface {
   }
 
   async findByItemUuid(itemUuid: Uuid): Promise<Revision[]> {
-    const typeormRevisions = await this.ormRepository
+    const SQLRevisions = await this.ormRepository
       .createQueryBuilder()
       .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
       .getMany()
 
     const revisions = []
-    for (const revision of typeormRevisions) {
+    for (const revision of SQLRevisions) {
       revisions.push(this.revisionMapper.toDomain(revision))
     }
 
@@ -62,23 +62,23 @@ export class TypeORMRevisionRepository implements RevisionRepositoryInterface {
   }
 
   async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null> {
-    const typeormRevision = await this.ormRepository
+    const SQLRevision = await this.ormRepository
       .createQueryBuilder()
       .where('uuid = :revisionUuid', { revisionUuid: revisionUuid.value })
       .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
       .getOne()
 
-    if (typeormRevision === null) {
+    if (SQLRevision === null) {
       return null
     }
 
-    return this.revisionMapper.toDomain(typeormRevision)
+    return this.revisionMapper.toDomain(SQLRevision)
   }
 
   async save(revision: Revision): Promise<Revision> {
-    const typeormRevision = this.revisionMapper.toProjection(revision)
+    const SQLRevision = this.revisionMapper.toProjection(revision)
 
-    await this.ormRepository.save(typeormRevision)
+    await this.ormRepository.save(SQLRevision)
 
     return revision
   }

+ 25 - 0
packages/revisions/src/Infra/TypeORM/TypeORMRevisionRepositoryResolver.ts

@@ -0,0 +1,25 @@
+import { RoleName, RoleNameCollection } from '@standardnotes/domain-core'
+
+import { RevisionRepositoryResolverInterface } from '../../Domain/Revision/RevisionRepositoryResolverInterface'
+import { RevisionRepositoryInterface } from '../../Domain/Revision/RevisionRepositoryInterface'
+
+export class TypeORMRevisionRepositoryResolver implements RevisionRepositoryResolverInterface {
+  constructor(
+    private sqlRevisionRepository: RevisionRepositoryInterface,
+    private mongoDbRevisionRepository: RevisionRepositoryInterface | null,
+  ) {}
+
+  resolve(roleNames: RoleNameCollection): RevisionRepositoryInterface {
+    if (!this.mongoDbRevisionRepository) {
+      return this.sqlRevisionRepository
+    }
+
+    const transitionRoleName = RoleName.create(RoleName.NAMES.TransitionUser).getValue()
+
+    if (roleNames.includes(transitionRoleName)) {
+      return this.mongoDbRevisionRepository
+    }
+
+    return this.sqlRevisionRepository
+  }
+}

+ 4 - 39
packages/revisions/src/Mapping/Http/RevisionHttpMapper.ts

@@ -1,49 +1,14 @@
 import { MapperInterface } from '@standardnotes/domain-core'
 
 import { Revision } from '../../Domain/Revision/Revision'
+import { RevisionHttpRepresentation } from './RevisionHttpRepresentation'
 
-export class RevisionHttpMapper
-  implements
-    MapperInterface<
-      Revision,
-      {
-        uuid: string
-        item_uuid: string
-        content: string | null
-        content_type: string
-        items_key_id: string | null
-        enc_item_key: string | null
-        auth_hash: string | null
-        created_at: string
-        updated_at: string
-      }
-    >
-{
-  toDomain(_projection: {
-    uuid: string
-    item_uuid: string
-    content: string | null
-    content_type: string
-    items_key_id: string | null
-    enc_item_key: string | null
-    auth_hash: string | null
-    created_at: string
-    updated_at: string
-  }): Revision {
+export class RevisionHttpMapper implements MapperInterface<Revision, RevisionHttpRepresentation> {
+  toDomain(_projection: RevisionHttpRepresentation): Revision {
     throw new Error('Method not implemented.')
   }
 
-  toProjection(domain: Revision): {
-    uuid: string
-    item_uuid: string
-    content: string | null
-    content_type: string
-    items_key_id: string | null
-    enc_item_key: string | null
-    auth_hash: string | null
-    created_at: string
-    updated_at: string
-  } {
+  toProjection(domain: Revision): RevisionHttpRepresentation {
     return {
       uuid: domain.id.toString(),
       item_uuid: domain.props.itemUuid.value,

+ 11 - 0
packages/revisions/src/Mapping/Http/RevisionHttpRepresentation.ts

@@ -0,0 +1,11 @@
+export interface RevisionHttpRepresentation {
+  uuid: string
+  item_uuid: string
+  content: string | null
+  content_type: string
+  items_key_id: string | null
+  enc_item_key: string | null
+  auth_hash: string | null
+  created_at: string
+  updated_at: string
+}

+ 4 - 25
packages/revisions/src/Mapping/Http/RevisionMetadataHttpMapper.ts

@@ -1,39 +1,18 @@
 import { MapperInterface, SyncUseCaseInterface } from '@standardnotes/domain-core'
 
 import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
+import { RevisionMetadataHttpRepresentation } from './RevisionMetadataHttpRepresentation'
 
 export class RevisionMetadataHttpMapper
-  implements
-    MapperInterface<
-      RevisionMetadata,
-      {
-        uuid: string
-        content_type: string
-        created_at: string
-        updated_at: string
-        required_role: string
-      }
-    >
+  implements MapperInterface<RevisionMetadata, RevisionMetadataHttpRepresentation>
 {
   constructor(private getRequiredRoleToViewRevision: SyncUseCaseInterface<string>) {}
 
-  toDomain(_projection: {
-    uuid: string
-    content_type: string
-    created_at: string
-    updated_at: string
-    required_role: string
-  }): RevisionMetadata {
+  toDomain(_projection: RevisionMetadataHttpRepresentation): RevisionMetadata {
     throw new Error('Method not implemented.')
   }
 
-  toProjection(domain: RevisionMetadata): {
-    uuid: string
-    content_type: string
-    created_at: string
-    updated_at: string
-    required_role: string
-  } {
+  toProjection(domain: RevisionMetadata): RevisionMetadataHttpRepresentation {
     return {
       uuid: domain.id.toString(),
       content_type: domain.props.contentType.value as string,

+ 7 - 0
packages/revisions/src/Mapping/Http/RevisionMetadataHttpRepresentation.ts

@@ -0,0 +1,7 @@
+export interface RevisionMetadataHttpRepresentation {
+  uuid: string
+  content_type: string
+  created_at: string
+  updated_at: string
+  required_role: string
+}

+ 4 - 4
packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper.ts

@@ -1,10 +1,10 @@
 import { MapperInterface, Dates, UniqueEntityId, ContentType } from '@standardnotes/domain-core'
 
 import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
-import { TypeORMRevision } from '../../../Infra/TypeORM/SQLRevision'
+import { SQLRevision } from '../../../Infra/TypeORM/SQL/SQLRevision'
 
-export class SQLRevisionMetadataPersistenceMapper implements MapperInterface<RevisionMetadata, TypeORMRevision> {
-  toDomain(projection: TypeORMRevision): RevisionMetadata {
+export class SQLRevisionMetadataPersistenceMapper implements MapperInterface<RevisionMetadata, SQLRevision> {
+  toDomain(projection: SQLRevision): RevisionMetadata {
     const contentTypeOrError = ContentType.create(projection.contentType)
     if (contentTypeOrError.isFailed()) {
       throw new Error(`Could not create content type: ${contentTypeOrError.getError()}`)
@@ -35,7 +35,7 @@ export class SQLRevisionMetadataPersistenceMapper implements MapperInterface<Rev
     return revisionMetadataOrError.getValue()
   }
 
-  toProjection(_domain: RevisionMetadata): TypeORMRevision {
+  toProjection(_domain: RevisionMetadata): SQLRevision {
     throw new Error('Method not implemented.')
   }
 }

+ 17 - 17
packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts

@@ -1,10 +1,10 @@
 import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@standardnotes/domain-core'
 
 import { Revision } from '../../../Domain/Revision/Revision'
-import { TypeORMRevision } from '../../../Infra/TypeORM/SQLRevision'
+import { SQLRevision } from '../../../Infra/TypeORM/SQL/SQLRevision'
 
-export class SQLRevisionPersistenceMapper implements MapperInterface<Revision, TypeORMRevision> {
-  toDomain(projection: TypeORMRevision): Revision {
+export class SQLRevisionPersistenceMapper implements MapperInterface<Revision, SQLRevision> {
+  toDomain(projection: SQLRevision): Revision {
     const contentTypeOrError = ContentType.create(projection.contentType)
     if (contentTypeOrError.isFailed()) {
       throw new Error(`Could not map typeorm revision to domain revision: ${contentTypeOrError.getError()}`)
@@ -53,21 +53,21 @@ export class SQLRevisionPersistenceMapper implements MapperInterface<Revision, T
     return revisionOrError.getValue()
   }
 
-  toProjection(domain: Revision): TypeORMRevision {
-    const typeormRevision = new TypeORMRevision()
+  toProjection(domain: Revision): SQLRevision {
+    const sqlRevision = new SQLRevision()
 
-    typeormRevision.authHash = domain.props.authHash
-    typeormRevision.content = domain.props.content
-    typeormRevision.contentType = domain.props.contentType.value
-    typeormRevision.createdAt = domain.props.dates.createdAt
-    typeormRevision.updatedAt = domain.props.dates.updatedAt
-    typeormRevision.creationDate = domain.props.creationDate
-    typeormRevision.encItemKey = domain.props.encItemKey
-    typeormRevision.itemUuid = domain.props.itemUuid.value
-    typeormRevision.itemsKeyId = domain.props.itemsKeyId
-    typeormRevision.userUuid = domain.props.userUuid ? domain.props.userUuid.value : null
-    typeormRevision.uuid = domain.id.toString()
+    sqlRevision.authHash = domain.props.authHash
+    sqlRevision.content = domain.props.content
+    sqlRevision.contentType = domain.props.contentType.value
+    sqlRevision.createdAt = domain.props.dates.createdAt
+    sqlRevision.updatedAt = domain.props.dates.updatedAt
+    sqlRevision.creationDate = domain.props.creationDate
+    sqlRevision.encItemKey = domain.props.encItemKey
+    sqlRevision.itemUuid = domain.props.itemUuid.value
+    sqlRevision.itemsKeyId = domain.props.itemsKeyId
+    sqlRevision.userUuid = domain.props.userUuid ? domain.props.userUuid.value : null
+    sqlRevision.uuid = domain.id.toString()
 
-    return typeormRevision
+    return sqlRevision
   }
 }

+ 3 - 6
packages/syncing-server/src/Bootstrap/Container.ts

@@ -860,8 +860,7 @@ export class ContainerConfigLoader {
       .bind<DuplicateItemSyncedEventHandler>(TYPES.Sync_DuplicateItemSyncedEventHandler)
       .toConstantValue(
         new DuplicateItemSyncedEventHandler(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
-          isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
+          container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
           container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
           container.get<Logger>(TYPES.Sync_Logger),
@@ -871,8 +870,7 @@ export class ContainerConfigLoader {
       .bind<AccountDeletionRequestedEventHandler>(TYPES.Sync_AccountDeletionRequestedEventHandler)
       .toConstantValue(
         new AccountDeletionRequestedEventHandler(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
-          isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
+          container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
           container.get<Logger>(TYPES.Sync_Logger),
         ),
       )
@@ -880,8 +878,7 @@ export class ContainerConfigLoader {
       .bind<ItemRevisionCreationRequestedEventHandler>(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)
       .toConstantValue(
         new ItemRevisionCreationRequestedEventHandler(
-          container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
-          isSecondaryDatabaseEnabled ? container.get<ItemRepositoryInterface>(TYPES.Sync_MongoDBItemRepository) : null,
+          container.get<ItemRepositoryResolverInterface>(TYPES.Sync_ItemRepositoryResolver),
           container.get<ItemBackupServiceInterface>(TYPES.Sync_ItemBackupService),
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
           container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),

+ 20 - 12
packages/syncing-server/src/Domain/Event/DomainEventFactory.ts

@@ -37,6 +37,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     dto: {
       originalItemUuid: string
       newItemUuid: string
+      roleNames: string[]
     },
   ): RevisionsCopyRequestedEvent {
     return {
@@ -53,55 +54,62 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     }
   }
 
-  createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent {
+  createItemDumpedEvent(dto: { fileDumpPath: string; userUuid: string; roleNames: string[] }): ItemDumpedEvent {
     return {
       type: 'ITEM_DUMPED',
       createdAt: this.timer.getUTCDate(),
       meta: {
         correlation: {
-          userIdentifier: userUuid,
+          userIdentifier: dto.userUuid,
           userIdentifierType: 'uuid',
         },
         origin: DomainEventService.SyncingServer,
       },
       payload: {
-        fileDumpPath,
+        fileDumpPath: dto.fileDumpPath,
+        roleNames: dto.roleNames,
       },
     }
   }
 
-  createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent {
+  createItemRevisionCreationRequested(dto: {
+    itemUuid: string
+    userUuid: string
+    roleNames: string[]
+  }): ItemRevisionCreationRequestedEvent {
     return {
       type: 'ITEM_REVISION_CREATION_REQUESTED',
       createdAt: this.timer.getUTCDate(),
       meta: {
         correlation: {
-          userIdentifier: userUuid,
+          userIdentifier: dto.userUuid,
           userIdentifierType: 'uuid',
         },
         origin: DomainEventService.SyncingServer,
       },
       payload: {
-        itemUuid,
+        itemUuid: dto.itemUuid,
+        roleNames: dto.roleNames,
       },
     }
   }
 
-  createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent {
+  createDuplicateItemSyncedEvent(dto: {
+    itemUuid: string
+    userUuid: string
+    roleNames: string[]
+  }): DuplicateItemSyncedEvent {
     return {
       type: 'DUPLICATE_ITEM_SYNCED',
       createdAt: this.timer.getUTCDate(),
       meta: {
         correlation: {
-          userIdentifier: userUuid,
+          userIdentifier: dto.userUuid,
           userIdentifierType: 'uuid',
         },
         origin: DomainEventService.SyncingServer,
       },
-      payload: {
-        itemUuid,
-        userUuid,
-      },
+      payload: dto,
     }
   }
 

+ 12 - 4
packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -26,11 +26,19 @@ export interface DomainEventFactoryInterface {
       attachmentContentType: string
     }>
   }): EmailRequestedEvent
-  createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent
-  createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent
-  createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent
+  createDuplicateItemSyncedEvent(dto: {
+    itemUuid: string
+    userUuid: string
+    roleNames: string[]
+  }): DuplicateItemSyncedEvent
+  createItemRevisionCreationRequested(dto: {
+    itemUuid: string
+    userUuid: string
+    roleNames: string[]
+  }): ItemRevisionCreationRequestedEvent
+  createItemDumpedEvent(dto: { fileDumpPath: string; userUuid: string; roleNames: string[] }): ItemDumpedEvent
   createRevisionsCopyRequestedEvent(
     userUuid: string,
-    dto: { originalItemUuid: string; newItemUuid: string },
+    dto: { originalItemUuid: string; newItemUuid: string; roleNames: string[] },
   ): RevisionsCopyRequestedEvent
 }

+ 15 - 14
packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts

@@ -6,16 +6,16 @@ import { Item } from '../Item/Item'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
 import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
+import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
 
 describe('AccountDeletionRequestedEventHandler', () => {
-  let primaryItemRepository: ItemRepositoryInterface
-  let secondaryItemRepository: ItemRepositoryInterface | null
+  let itemRepositoryResolver: ItemRepositoryResolverInterface
+  let itemRepository: ItemRepositoryInterface
   let logger: Logger
   let event: AccountDeletionRequestedEvent
   let item: Item
 
-  const createHandler = () =>
-    new AccountDeletionRequestedEventHandler(primaryItemRepository, secondaryItemRepository, logger)
+  const createHandler = () => new AccountDeletionRequestedEventHandler(itemRepositoryResolver, logger)
 
   beforeEach(() => {
     item = Item.create(
@@ -35,9 +35,12 @@ describe('AccountDeletionRequestedEventHandler', () => {
       new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
     ).getValue()
 
-    primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    primaryItemRepository.findAll = jest.fn().mockReturnValue([item])
-    primaryItemRepository.deleteByUserUuid = jest.fn()
+    itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
+    itemRepository.findAll = jest.fn().mockReturnValue([item])
+    itemRepository.deleteByUserUuid = jest.fn()
+
+    itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
+    itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
 
     logger = {} as jest.Mocked<Logger>
     logger.info = jest.fn()
@@ -48,23 +51,21 @@ describe('AccountDeletionRequestedEventHandler', () => {
       userUuid: '2-3-4',
       userCreatedAtTimestamp: 1,
       regularSubscriptionUuid: '1-2-3',
+      roleNames: ['CORE_USER'],
     }
   })
 
   it('should remove all items for a user', async () => {
     await createHandler().handle(event)
 
-    expect(primaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
+    expect(itemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
   })
 
-  it('should remove all items for a user from secondary repository', async () => {
-    secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    secondaryItemRepository.deleteByUserUuid = jest.fn()
+  it('should do nothing if role names are not valid', async () => {
+    event.payload.roleNames = ['INVALID_ROLE_NAME']
 
     await createHandler().handle(event)
 
-    expect(secondaryItemRepository.deleteByUserUuid).toHaveBeenCalledWith('2-3-4')
-
-    secondaryItemRepository = null
+    expect(itemRepository.deleteByUserUuid).not.toHaveBeenCalled()
   })
 })

+ 12 - 9
packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts

@@ -1,19 +1,22 @@
 import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
+import { RoleNameCollection } from '@standardnotes/domain-core'
 import { Logger } from 'winston'
-import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
+
+import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
 
 export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
-  constructor(
-    private primaryItemRepository: ItemRepositoryInterface,
-    private secondaryItemRepository: ItemRepositoryInterface | null,
-    private logger: Logger,
-  ) {}
+  constructor(private itemRepositoryResolver: ItemRepositoryResolverInterface, private logger: Logger) {}
 
   async handle(event: AccountDeletionRequestedEvent): Promise<void> {
-    await this.primaryItemRepository.deleteByUserUuid(event.payload.userUuid)
-    if (this.secondaryItemRepository) {
-      await this.secondaryItemRepository.deleteByUserUuid(event.payload.userUuid)
+    const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return
     }
+    const roleNames = roleNamesOrError.getValue()
+
+    const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
+
+    await itemRepository.deleteByUserUuid(event.payload.userUuid)
 
     this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
   }

+ 17 - 22
packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts

@@ -11,10 +11,11 @@ import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
 import { DuplicateItemSyncedEventHandler } from './DuplicateItemSyncedEventHandler'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
+import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
 
 describe('DuplicateItemSyncedEventHandler', () => {
-  let primaryItemRepository: ItemRepositoryInterface
-  let secondaryItemRepository: ItemRepositoryInterface | null
+  let itemRepositoryResolver: ItemRepositoryResolverInterface
+  let itemRepository: ItemRepositoryInterface
   let logger: Logger
   let duplicateItem: Item
   let originalItem: Item
@@ -23,13 +24,7 @@ describe('DuplicateItemSyncedEventHandler', () => {
   let domainEventPublisher: DomainEventPublisherInterface
 
   const createHandler = () =>
-    new DuplicateItemSyncedEventHandler(
-      primaryItemRepository,
-      secondaryItemRepository,
-      domainEventFactory,
-      domainEventPublisher,
-      logger,
-    )
+    new DuplicateItemSyncedEventHandler(itemRepositoryResolver, domainEventFactory, domainEventPublisher, logger)
 
   beforeEach(() => {
     originalItem = Item.create(
@@ -66,12 +61,15 @@ describe('DuplicateItemSyncedEventHandler', () => {
       new UniqueEntityId('00000000-0000-0000-0000-000000000001'),
     ).getValue()
 
-    primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    primaryItemRepository.findByUuidAndUserUuid = jest
+    itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
+    itemRepository.findByUuidAndUserUuid = jest
       .fn()
       .mockReturnValueOnce(duplicateItem)
       .mockReturnValueOnce(originalItem)
 
+    itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
+    itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
+
     logger = {} as jest.Mocked<Logger>
     logger.warn = jest.fn()
     logger.debug = jest.fn()
@@ -81,6 +79,7 @@ describe('DuplicateItemSyncedEventHandler', () => {
     event.payload = {
       userUuid: '1-2-3',
       itemUuid: '2-3-4',
+      roleNames: ['CORE_USER'],
     }
 
     domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
@@ -98,22 +97,17 @@ describe('DuplicateItemSyncedEventHandler', () => {
     expect(domainEventPublisher.publish).toHaveBeenCalled()
   })
 
-  it('should copy revisions from original item to the duplicate item in the secondary repository', async () => {
-    secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    secondaryItemRepository.findByUuidAndUserUuid = jest
-      .fn()
-      .mockReturnValueOnce(duplicateItem)
-      .mockReturnValueOnce(originalItem)
+  it('should do nothing if role names are not valid', async () => {
+    event.payload.roleNames = ['INVALID_ROLE_NAME']
 
     await createHandler().handle(event)
 
-    expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
-
-    secondaryItemRepository = null
+    expect(domainEventPublisher.publish).not.toHaveBeenCalled()
   })
 
   it('should not copy revisions if original item does not exist', async () => {
-    primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
+    itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(duplicateItem).mockReturnValueOnce(null)
+    itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
 
     await createHandler().handle(event)
 
@@ -121,7 +115,8 @@ describe('DuplicateItemSyncedEventHandler', () => {
   })
 
   it('should not copy revisions if duplicate item does not exist', async () => {
-    primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
+    itemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(originalItem)
+    itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
 
     await createHandler().handle(event)
 

+ 12 - 6
packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts

@@ -6,22 +6,27 @@ import {
 import { Logger } from 'winston'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
+import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
+import { RoleNameCollection } from '@standardnotes/domain-core'
 
 export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterface {
   constructor(
-    private primaryItemRepository: ItemRepositoryInterface,
-    private secondaryItemRepository: ItemRepositoryInterface | null,
+    private itemRepositoryResolver: ItemRepositoryResolverInterface,
     private domainEventFactory: DomainEventFactoryInterface,
     private domainEventPublisher: DomainEventPublisherInterface,
     private logger: Logger,
   ) {}
 
   async handle(event: DuplicateItemSyncedEvent): Promise<void> {
-    await this.requestRevisionsCopy(event, this.primaryItemRepository)
-
-    if (this.secondaryItemRepository) {
-      await this.requestRevisionsCopy(event, this.secondaryItemRepository)
+    const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return
     }
+    const roleNames = roleNamesOrError.getValue()
+
+    const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
+
+    await this.requestRevisionsCopy(event, itemRepository)
   }
 
   private async requestRevisionsCopy(
@@ -52,6 +57,7 @@ export class DuplicateItemSyncedEventHandler implements DomainEventHandlerInterf
         this.domainEventFactory.createRevisionsCopyRequestedEvent(event.payload.userUuid, {
           originalItemUuid: existingOriginalItem.id.toString(),
           newItemUuid: item.id.toString(),
+          roleNames: event.payload.roleNames,
         }),
       )
     }

+ 17 - 14
packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts

@@ -12,10 +12,11 @@ import { ItemRevisionCreationRequestedEventHandler } from './ItemRevisionCreatio
 import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
+import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
 
 describe('ItemRevisionCreationRequestedEventHandler', () => {
-  let primaryItemRepository: ItemRepositoryInterface
-  let secondaryItemRepository: ItemRepositoryInterface | null
+  let itemRepositoryResolver: ItemRepositoryResolverInterface
+  let itemRepository: ItemRepositoryInterface
   let event: ItemRevisionCreationRequestedEvent
   let item: Item
   let itemBackupService: ItemBackupServiceInterface
@@ -24,8 +25,7 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
 
   const createHandler = () =>
     new ItemRevisionCreationRequestedEventHandler(
-      primaryItemRepository,
-      secondaryItemRepository,
+      itemRepositoryResolver,
       itemBackupService,
       domainEventFactory,
       domainEventPublisher,
@@ -49,13 +49,17 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
       new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
     ).getValue()
 
-    primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    primaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
+    itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
+    itemRepository.findByUuid = jest.fn().mockReturnValue(item)
+
+    itemRepositoryResolver = {} as jest.Mocked<ItemRepositoryResolverInterface>
+    itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
 
     event = {} as jest.Mocked<ItemRevisionCreationRequestedEvent>
     event.createdAt = new Date(1)
     event.payload = {
       itemUuid: '00000000-0000-0000-0000-000000000000',
+      roleNames: ['CORE_USER'],
     }
     event.meta = {
       correlation: {
@@ -82,20 +86,19 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
     expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
   })
 
-  it('should create a revision for an item in the secondary repository', async () => {
-    secondaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
-    secondaryItemRepository.findByUuid = jest.fn().mockReturnValue(item)
+  it('should do nothing if roles names are not valid', async () => {
+    event.payload.roleNames = ['INVALID_ROLE_NAME']
 
     await createHandler().handle(event)
 
-    expect(domainEventPublisher.publish).toHaveBeenCalled()
-    expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
-
-    secondaryItemRepository = null
+    expect(domainEventPublisher.publish).not.toHaveBeenCalled()
+    expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
   })
 
   it('should not create a revision for an item that does not exist', async () => {
-    primaryItemRepository.findByUuid = jest.fn().mockReturnValue(null)
+    itemRepository.findByUuid = jest.fn().mockReturnValue(null)
+
+    itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository)
 
     await createHandler().handle(event)
 

+ 16 - 8
packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.ts

@@ -3,27 +3,31 @@ import {
   DomainEventHandlerInterface,
   DomainEventPublisherInterface,
 } from '@standardnotes/domain-events'
-import { Uuid } from '@standardnotes/domain-core'
+import { RoleNameCollection, Uuid } from '@standardnotes/domain-core'
 
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
 import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
 import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
+import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
 
 export class ItemRevisionCreationRequestedEventHandler implements DomainEventHandlerInterface {
   constructor(
-    private primaryItemRepository: ItemRepositoryInterface,
-    private secondaryItemRepository: ItemRepositoryInterface | null,
+    private itemRepositoryResolver: ItemRepositoryResolverInterface,
     private itemBackupService: ItemBackupServiceInterface,
     private domainEventFactory: DomainEventFactoryInterface,
     private domainEventPublisher: DomainEventPublisherInterface,
   ) {}
 
   async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
-    await this.createItemDump(event, this.primaryItemRepository)
-
-    if (this.secondaryItemRepository) {
-      await this.createItemDump(event, this.secondaryItemRepository)
+    const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
+    if (roleNamesOrError.isFailed()) {
+      return
     }
+    const roleNames = roleNamesOrError.getValue()
+
+    const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
+
+    await this.createItemDump(event, itemRepository)
   }
 
   private async createItemDump(
@@ -44,7 +48,11 @@ export class ItemRevisionCreationRequestedEventHandler implements DomainEventHan
     const fileDumpPath = await this.itemBackupService.dump(item)
     if (fileDumpPath) {
       await this.domainEventPublisher.publish(
-        this.domainEventFactory.createItemDumpedEvent(fileDumpPath, event.meta.correlation.userIdentifier),
+        this.domainEventFactory.createItemDumpedEvent({
+          fileDumpPath,
+          userUuid: event.meta.correlation.userIdentifier,
+          roleNames: event.payload.roleNames,
+        }),
       )
     }
   }

+ 10 - 5
packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts

@@ -146,16 +146,21 @@ export class SaveNewItem implements UseCaseInterface<Item> {
 
     if (contentType.value !== null && [ContentType.TYPES.Note, ContentType.TYPES.File].includes(contentType.value)) {
       await this.domainEventPublisher.publish(
-        this.domainEventFactory.createItemRevisionCreationRequested(
-          newItem.id.toString(),
-          newItem.props.userUuid.value,
-        ),
+        this.domainEventFactory.createItemRevisionCreationRequested({
+          itemUuid: newItem.id.toString(),
+          userUuid: newItem.props.userUuid.value,
+          roleNames: dto.roleNames,
+        }),
       )
     }
 
     if (duplicateOf) {
       await this.domainEventPublisher.publish(
-        this.domainEventFactory.createDuplicateItemSyncedEvent(newItem.id.toString(), newItem.props.userUuid.value),
+        this.domainEventFactory.createDuplicateItemSyncedEvent({
+          itemUuid: newItem.id.toString(),
+          userUuid: newItem.props.userUuid.value,
+          roleNames: dto.roleNames,
+        }),
       )
     }
 

+ 10 - 8
packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts

@@ -190,20 +190,22 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
         [ContentType.TYPES.Note, ContentType.TYPES.File].includes(dto.existingItem.props.contentType.value)
       ) {
         await this.domainEventPublisher.publish(
-          this.domainEventFactory.createItemRevisionCreationRequested(
-            dto.existingItem.id.toString(),
-            dto.existingItem.props.userUuid.value,
-          ),
+          this.domainEventFactory.createItemRevisionCreationRequested({
+            itemUuid: dto.existingItem.id.toString(),
+            userUuid: dto.existingItem.props.userUuid.value,
+            roleNames: dto.roleNames,
+          }),
         )
       }
     }
 
     if (wasMarkedAsDuplicate) {
       await this.domainEventPublisher.publish(
-        this.domainEventFactory.createDuplicateItemSyncedEvent(
-          dto.existingItem.id.toString(),
-          dto.existingItem.props.userUuid.value,
-        ),
+        this.domainEventFactory.createDuplicateItemSyncedEvent({
+          itemUuid: dto.existingItem.id.toString(),
+          userUuid: dto.existingItem.props.userUuid.value,
+          roleNames: dto.roleNames,
+        }),
       )
     }