diff --git a/packages/auth/src/Domain/Event/DomainEventFactory.ts b/packages/auth/src/Domain/Event/DomainEventFactory.ts index 570c811b8..e6b64dc5e 100644 --- a/packages/auth/src/Domain/Event/DomainEventFactory.ts +++ b/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', diff --git a/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts index 228cb5c3f..55aa0f53e 100644 --- a/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/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 diff --git a/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts index 379547776..edc9b02e4 100644 --- a/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts +++ b/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 diff --git a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts b/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts index e751dd290..1b338aba2 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.spec.ts +++ b/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.roles = Promise.resolve([{ name: RoleName.NAMES.CoreUser } as jest.Mocked]) 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'], }) }) diff --git a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts b/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts index 356c69e3d..5f797b0de 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts +++ b/packages/auth/src/Domain/UseCase/DeleteAccount/DeleteAccount.ts @@ -47,6 +47,8 @@ export class DeleteAccount implements UseCaseInterface { 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 { userUuid: user.uuid, userCreatedAtTimestamp: this.timer.convertDateToMicroseconds(user.createdAt), regularSubscriptionUuid, + roleNames: roles.map((role) => role.name), }), ) diff --git a/packages/domain-events/src/Domain/Event/AccountDeletionRequestedEventPayload.ts b/packages/domain-events/src/Domain/Event/AccountDeletionRequestedEventPayload.ts index fcbbb7a70..537978d5e 100644 --- a/packages/domain-events/src/Domain/Event/AccountDeletionRequestedEventPayload.ts +++ b/packages/domain-events/src/Domain/Event/AccountDeletionRequestedEventPayload.ts @@ -1,5 +1,6 @@ export interface AccountDeletionRequestedEventPayload { userUuid: string + roleNames: string[] userCreatedAtTimestamp: number regularSubscriptionUuid: string | undefined } diff --git a/packages/domain-events/src/Domain/Event/DuplicateItemSyncedEventPayload.ts b/packages/domain-events/src/Domain/Event/DuplicateItemSyncedEventPayload.ts index b3fa9f231..bb911da51 100644 --- a/packages/domain-events/src/Domain/Event/DuplicateItemSyncedEventPayload.ts +++ b/packages/domain-events/src/Domain/Event/DuplicateItemSyncedEventPayload.ts @@ -1,4 +1,5 @@ export interface DuplicateItemSyncedEventPayload { itemUuid: string userUuid: string + roleNames: string[] } diff --git a/packages/domain-events/src/Domain/Event/ItemDumpedEventPayload.ts b/packages/domain-events/src/Domain/Event/ItemDumpedEventPayload.ts index 7c0164d1d..431be2621 100644 --- a/packages/domain-events/src/Domain/Event/ItemDumpedEventPayload.ts +++ b/packages/domain-events/src/Domain/Event/ItemDumpedEventPayload.ts @@ -1,3 +1,4 @@ export interface ItemDumpedEventPayload { fileDumpPath: string + roleNames: string[] } diff --git a/packages/domain-events/src/Domain/Event/ItemRevisionCreationRequestedEventPayload.ts b/packages/domain-events/src/Domain/Event/ItemRevisionCreationRequestedEventPayload.ts index 7854b1dfb..90a6587ff 100644 --- a/packages/domain-events/src/Domain/Event/ItemRevisionCreationRequestedEventPayload.ts +++ b/packages/domain-events/src/Domain/Event/ItemRevisionCreationRequestedEventPayload.ts @@ -1,3 +1,4 @@ export interface ItemRevisionCreationRequestedEventPayload { itemUuid: string + roleNames: string[] } diff --git a/packages/domain-events/src/Domain/Event/RevisionsCopyRequestedEventPayload.ts b/packages/domain-events/src/Domain/Event/RevisionsCopyRequestedEventPayload.ts index 09efb74f7..a6ef147ae 100644 --- a/packages/domain-events/src/Domain/Event/RevisionsCopyRequestedEventPayload.ts +++ b/packages/domain-events/src/Domain/Event/RevisionsCopyRequestedEventPayload.ts @@ -1,4 +1,5 @@ export interface RevisionsCopyRequestedEventPayload { newItemUuid: string originalItemUuid: string + roleNames: string[] } diff --git a/packages/revisions/src/Bootstrap/Container.ts b/packages/revisions/src/Bootstrap/Container.ts index b9ff3575b..6b61c3164 100644 --- a/packages/revisions/src/Bootstrap/Container.ts +++ b/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>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper) + .bind>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper) .toConstantValue(new SQLRevisionMetadataPersistenceMapper()) container - .bind>(TYPES.Revisions_SQLRevisionPersistenceMapper) + .bind>(TYPES.Revisions_SQLRevisionPersistenceMapper) .toConstantValue(new SQLRevisionPersistenceMapper()) container .bind>( @@ -122,19 +125,19 @@ export class ContainerConfigLoader { // ORM container - .bind>(TYPES.Revisions_ORMRevisionRepository) - .toDynamicValue(() => appDataSource.getRepository(TypeORMRevision)) + .bind>(TYPES.Revisions_ORMRevisionRepository) + .toDynamicValue(() => appDataSource.getRepository(SQLRevision)) // Repositories container - .bind(TYPES.Revisions_RevisionRepository) + .bind(TYPES.Revisions_SQLRevisionRepository) .toConstantValue( - new TypeORMRevisionRepository( - container.get>(TYPES.Revisions_ORMRevisionRepository), - container.get>( + new SQLRevisionRepository( + container.get>(TYPES.Revisions_ORMRevisionRepository), + container.get>( TYPES.Revisions_SQLRevisionMetadataPersistenceMapper, ), - container.get>(TYPES.Revisions_SQLRevisionPersistenceMapper), + container.get>(TYPES.Revisions_SQLRevisionPersistenceMapper), container.get(TYPES.Revisions_Logger), ), ) @@ -158,6 +161,17 @@ export class ContainerConfigLoader { ) } + container + .bind(TYPES.Revisions_RevisionRepositoryResolver) + .toConstantValue( + new TypeORMRevisionRepositoryResolver( + container.get(TYPES.Revisions_SQLRevisionRepository), + isSecondaryDatabaseEnabled + ? container.get(TYPES.Revisions_MongoDBRevisionRepository) + : null, + ), + ) + container.bind(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>(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>( + 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(TYPES.Revisions_GetRevisionsMetada) - .toDynamicValue((context: interfaces.Context) => { - return new GetRevisionsMetada(context.container.get(TYPES.Revisions_RevisionRepository)) - }) - container.bind(TYPES.Revisions_GetRevision).toDynamicValue((context: interfaces.Context) => { - return new GetRevision(context.container.get(TYPES.Revisions_RevisionRepository)) - }) - container.bind(TYPES.Revisions_DeleteRevision).toDynamicValue((context: interfaces.Context) => { - return new DeleteRevision(context.container.get(TYPES.Revisions_RevisionRepository)) - }) + .toConstantValue( + new GetRevisionsMetada( + container.get(TYPES.Revisions_RevisionRepositoryResolver), + ), + ) + container + .bind(TYPES.Revisions_GetRevision) + .toConstantValue( + new GetRevision(container.get(TYPES.Revisions_RevisionRepositoryResolver)), + ) + container + .bind(TYPES.Revisions_DeleteRevision) + .toConstantValue( + new DeleteRevision( + container.get(TYPES.Revisions_RevisionRepositoryResolver), + ), + ) + container + .bind(TYPES.Revisions_CopyRevisions) + .toConstantValue( + new CopyRevisions( + container.get(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(TYPES.Revisions_ControllerContainer) .toConstantValue(configuration?.controllerConatiner ?? new ControllerContainer()) - container - .bind(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>(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(TYPES.Revisions_CopyRevisions).toDynamicValue((context: interfaces.Context) => { - return new CopyRevisions(context.container.get(TYPES.Revisions_RevisionRepository)) - }) - // Handlers container .bind(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(TYPES.Revisions_DumpRepository), + container.get(TYPES.Revisions_RevisionRepositoryResolver), + ), + ) container .bind(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(TYPES.Revisions_RevisionRepositoryResolver), + container.get(TYPES.Revisions_Logger), + ), + ) container .bind(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(TYPES.Revisions_CopyRevisions), + container.get(TYPES.Revisions_Logger), + ), + ) const eventHandlers: Map = new Map([ ['ITEM_DUMPED', container.get(TYPES.Revisions_ItemDumpedEventHandler)], @@ -380,8 +368,12 @@ export class ContainerConfigLoader { .bind(TYPES.Revisions_BaseRevisionsController) .toConstantValue( new BaseRevisionsController( - container.get(TYPES.Revisions_RevisionsController), - container.get(TYPES.Revisions_ControllerContainer), + container.get(TYPES.Revisions_GetRevisionsMetada), + container.get(TYPES.Revisions_GetRevision), + container.get(TYPES.Revisions_DeleteRevision), + container.get(TYPES.Revisions_RevisionHttpMapper), + container.get(TYPES.Revisions_RevisionMetadataHttpMapper), + container.get(TYPES.Revisions_ControllerContainer), ), ) } diff --git a/packages/revisions/src/Bootstrap/DataSource.ts b/packages/revisions/src/Bootstrap/DataSource.ts index f782334ef..d909b27e3 100644 --- a/packages/revisions/src/Bootstrap/DataSource.ts +++ b/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: this.env.get('DB_DEBUG_LEVEL', true) ?? 'info', diff --git a/packages/revisions/src/Bootstrap/Types.ts b/packages/revisions/src/Bootstrap/Types.ts index 90f0100d6..a687435ea 100644 --- a/packages/revisions/src/Bootstrap/Types.ts +++ b/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'), diff --git a/packages/revisions/src/Controller/RevisionsController.ts b/packages/revisions/src/Controller/RevisionsController.ts deleted file mode 100644 index 92866517c..000000000 --- a/packages/revisions/src/Controller/RevisionsController.ts +++ /dev/null @@ -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> { - 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> { - 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 { - 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() }, - } - } -} diff --git a/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts b/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts index e43b07e0a..12bf06e0c 100644 --- a/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts +++ b/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 revisionRepository.removeByUserUuid = jest.fn() + revisionRepositoryResolver = {} as jest.Mocked + revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository) + logger = {} as jest.Mocked logger.info = jest.fn() logger.warn = jest.fn() + logger.error = jest.fn() event = {} as jest.Mocked 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() + }) }) diff --git a/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts b/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts index c727eb3ea..c4e959a91 100644 --- a/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts +++ b/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 { 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}`) } diff --git a/packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.spec.ts b/packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.spec.ts index 1e97eb1c0..242f62310 100644 --- a/packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.spec.ts +++ b/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 @@ -22,9 +24,13 @@ describe('ItemDumpedEventHandler', () => { revisionRepository = {} as jest.Mocked revisionRepository.save = jest.fn() + revisionRepositoryResolver = {} as jest.Mocked + revisionRepositoryResolver.resolve = jest.fn().mockReturnValue(revisionRepository) + event = {} as jest.Mocked 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) diff --git a/packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.ts b/packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.ts index 8b4994595..cdfc9fdde 100644 --- a/packages/revisions/src/Domain/Handler/ItemDumpedEventHandler.ts +++ b/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 { @@ -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) } diff --git a/packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.spec.ts b/packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.spec.ts index d729f17c7..80ba973f5 100644 --- a/packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.spec.ts +++ b/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'], } }) diff --git a/packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.ts b/packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.ts index dd907e260..dd09e05ef 100644 --- a/packages/revisions/src/Domain/Handler/RevisionsCopyRequestedEventHandler.ts +++ b/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()) { diff --git a/packages/revisions/src/Domain/Revision/RevisionRepositoryResolverInterface.ts b/packages/revisions/src/Domain/Revision/RevisionRepositoryResolverInterface.ts new file mode 100644 index 000000000..1c0c4e251 --- /dev/null +++ b/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 +} diff --git a/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.spec.ts b/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.spec.ts index 1d0f44357..e94c65573 100644 --- a/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.spec.ts +++ b/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 revisionRepository.findByItemUuid = jest.fn().mockReturnValue([{} as jest.Mocked]) revisionRepository.save = jest.fn() + + revisionRepositoryResolver = {} as jest.Mocked + 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() diff --git a/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.ts b/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.ts index 7a3b502e4..4d37f9aa9 100644 --- a/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisions.ts +++ b/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 { - constructor(private revisionRepository: RevisionRepositoryInterface) {} + constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {} async execute(dto: CopyRevisionsDTO): Promise> { const orignalItemUuidOrError = Uuid.create(dto.originalItemUuid) @@ -21,7 +21,15 @@ export class CopyRevisions implements UseCaseInterface { } 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 { const revisionCopy = revisionCopyOrError.getValue() - await this.revisionRepository.save(revisionCopy) + await revisionRepository.save(revisionCopy) } return Result.ok('Revisions copied') diff --git a/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisionsDTO.ts b/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisionsDTO.ts index 962a9b949..628cfb1fe 100644 --- a/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisionsDTO.ts +++ b/packages/revisions/src/Domain/UseCase/CopyRevisions/CopyRevisionsDTO.ts @@ -1,4 +1,5 @@ export interface CopyRevisionsDTO { originalItemUuid: string newItemUuid: string + roleNames: string[] } diff --git a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts index cd2123f46..93ebe9f4b 100644 --- a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts +++ b/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 revisionRepository.removeOneByUuid = jest.fn() + + revisionRepositoryResolver = {} as jest.Mocked + 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() diff --git a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts index 84290465a..f242ae70e 100644 --- a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts +++ b/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 { - constructor(private revisionRepository: RevisionRepositoryInterface) {} + constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {} async execute(dto: DeleteRevisionDTO): Promise> { const revisionUuidOrError = Uuid.create(dto.revisionUuid) @@ -19,7 +19,15 @@ export class DeleteRevision implements UseCaseInterface { } 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('Revision removed') } diff --git a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts index b1acf5a97..c45b0206b 100644 --- a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts +++ b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts @@ -1,4 +1,5 @@ export interface DeleteRevisionDTO { userUuid: string revisionUuid: string + roleNames: string[] } diff --git a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.spec.ts b/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.spec.ts index b6a0083ce..a24290876 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.spec.ts +++ b/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 revisionRepository.findOneByUuid = jest.fn().mockReturnValue({} as jest.Mocked) + + revisionRepositoryResolver = {} as jest.Mocked + 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() diff --git a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts b/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts index 7f435be65..9cf6fbe7f 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts +++ b/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 { - constructor(private revisionRepository: RevisionRepositoryInterface) {} + constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {} async execute(dto: GetRevisionDTO): Promise> { const revisionUuidOrError = Uuid.create(dto.revisionUuid) @@ -20,7 +20,15 @@ export class GetRevision implements UseCaseInterface { } 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(`Could not find revision with uuid: ${revisionUuid.value}`) diff --git a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevisionDTO.ts b/packages/revisions/src/Domain/UseCase/GetRevision/GetRevisionDTO.ts index 847eebc37..bb3c14118 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevisionDTO.ts +++ b/packages/revisions/src/Domain/UseCase/GetRevision/GetRevisionDTO.ts @@ -1,4 +1,5 @@ export interface GetRevisionDTO { userUuid: string revisionUuid: string + roleNames: string[] } diff --git a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts index c9ca94bf8..d6828113a 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts +++ b/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 revisionRepository.findMetadataByItemId = jest.fn().mockReturnValue([{} as jest.Mocked]) + + revisionRepositoryResolver = {} as jest.Mocked + 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() diff --git a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts index c20aa30b4..5b7735ea5 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts +++ b/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 { - constructor(private revisionRepository: RevisionRepositoryInterface) {} + constructor(private revisionRepositoryResolver: RevisionRepositoryResolverInterface) {} async execute(dto: GetRevisionsMetadaDTO): Promise> { const itemUuidOrError = Uuid.create(dto.itemUuid) @@ -19,7 +19,15 @@ export class GetRevisionsMetada implements UseCaseInterface return Result.fail(`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(), ) diff --git a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts index 265572949..1cf80681e 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts +++ b/packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts @@ -1,4 +1,5 @@ export interface GetRevisionsMetadaDTO { itemUuid: string userUuid: string + roleNames: string[] } diff --git a/packages/revisions/src/Infra/Http/Request/DeleteRevisionRequestParams.ts b/packages/revisions/src/Infra/Http/Request/DeleteRevisionRequestParams.ts deleted file mode 100644 index d31fbd286..000000000 --- a/packages/revisions/src/Infra/Http/Request/DeleteRevisionRequestParams.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface DeleteRevisionRequestParams { - revisionUuid: string - userUuid: string -} diff --git a/packages/revisions/src/Infra/Http/Request/GetRevisionRequestParams.ts b/packages/revisions/src/Infra/Http/Request/GetRevisionRequestParams.ts deleted file mode 100644 index afd1c0951..000000000 --- a/packages/revisions/src/Infra/Http/Request/GetRevisionRequestParams.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface GetRevisionRequestParams { - revisionUuid: string - userUuid: string -} diff --git a/packages/revisions/src/Infra/Http/Request/GetRevisionsMetadataRequestParams.ts b/packages/revisions/src/Infra/Http/Request/GetRevisionsMetadataRequestParams.ts deleted file mode 100644 index 22ba1cf56..000000000 --- a/packages/revisions/src/Infra/Http/Request/GetRevisionsMetadataRequestParams.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface GetRevisionsMetadataRequestParams { - itemUuid: string - userUuid: string -} diff --git a/packages/revisions/src/Infra/Http/Response/GetRevisionResponseBody.ts b/packages/revisions/src/Infra/Http/Response/GetRevisionResponseBody.ts deleted file mode 100644 index 83584a5c5..000000000 --- a/packages/revisions/src/Infra/Http/Response/GetRevisionResponseBody.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/revisions/src/Infra/Http/Response/GetRevisionsMetadataResponseBody.ts b/packages/revisions/src/Infra/Http/Response/GetRevisionsMetadataResponseBody.ts deleted file mode 100644 index 9a0ade4b0..000000000 --- a/packages/revisions/src/Infra/Http/Response/GetRevisionsMetadataResponseBody.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface GetRevisionsMetadataResponseBody { - revisions: Array<{ - uuid: string - contentType: string - createdAt: string - updatedAt: string - }> -} diff --git a/packages/revisions/src/Infra/InversifyExpress/AnnotatedRevisionsController.ts b/packages/revisions/src/Infra/InversifyExpress/AnnotatedRevisionsController.ts index d318b87b2..1caf3102a 100644 --- a/packages/revisions/src/Infra/InversifyExpress/AnnotatedRevisionsController.ts +++ b/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, + @inject(TYPES.Revisions_RevisionMetadataHttpMapper) + override revisionMetadataHttpMapper: MapperInterface, + ) { + super(getRevisionsMetadata, doGetRevision, doDeleteRevision, revisionHttpMapper, revisionMetadataHttpMapper) } @httpGet('/') - override async getRevisions(req: Request, response: Response): Promise { - return super.getRevisions(req, response) + override async getRevisions(request: Request, response: Response): Promise { + return super.getRevisions(request, response) } @httpGet('/:uuid') - override async getRevision(req: Request, response: Response): Promise { - return super.getRevision(req, response) + override async getRevision(request: Request, response: Response): Promise { + return super.getRevision(request, response) } @httpDelete('/:uuid') - override async deleteRevision(req: Request, response: Response): Promise { - return super.deleteRevision(req, response) + override async deleteRevision(request: Request, response: Response): Promise { + return super.deleteRevision(request, response) } } diff --git a/packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts b/packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts index c47839af1..034600ee6 100644 --- a/packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts +++ b/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, + protected revisionMetadataHttpMapper: MapperInterface, private controllerContainer?: ControllerContainerInterface, ) { super() @@ -18,30 +30,73 @@ export class BaseRevisionsController extends BaseHttpController { } } - async getRevisions(req: Request, response: Response): Promise { - const result = await this.revisionsController.getRevisions({ - itemUuid: req.params.itemUuid, + async getRevisions(request: Request, response: Response): Promise { + 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 { - const result = await this.revisionsController.getRevision({ - revisionUuid: req.params.uuid, + async getRevision(request: Request, response: Response): Promise { + 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 { - const result = await this.revisionsController.deleteRevision({ - revisionUuid: req.params.uuid, + async deleteRevision(request: Request, response: Response): Promise { + 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(), + }) } } diff --git a/packages/revisions/src/Infra/TypeORM/SQLRevision.ts b/packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts similarity index 97% rename from packages/revisions/src/Infra/TypeORM/SQLRevision.ts rename to packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts index ededf6ec8..b3ef58147 100644 --- a/packages/revisions/src/Infra/TypeORM/SQLRevision.ts +++ b/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 diff --git a/packages/revisions/src/Infra/TypeORM/SQLRevisionRepository.ts b/packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts similarity index 75% rename from packages/revisions/src/Infra/TypeORM/SQLRevisionRepository.ts rename to packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts index 69bac04ef..f1d76a0df 100644 --- a/packages/revisions/src/Infra/TypeORM/SQLRevisionRepository.ts +++ b/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, - private revisionMetadataMapper: MapperInterface, - private revisionMapper: MapperInterface, + private ormRepository: Repository, + private revisionMetadataMapper: MapperInterface, + private revisionMapper: MapperInterface, private logger: Logger, ) {} @@ -27,13 +27,13 @@ export class TypeORMRevisionRepository implements RevisionRepositoryInterface { } async findByItemUuid(itemUuid: Uuid): Promise { - 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 { - 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 { - const typeormRevision = this.revisionMapper.toProjection(revision) + const SQLRevision = this.revisionMapper.toProjection(revision) - await this.ormRepository.save(typeormRevision) + await this.ormRepository.save(SQLRevision) return revision } diff --git a/packages/revisions/src/Infra/TypeORM/TypeORMRevisionRepositoryResolver.ts b/packages/revisions/src/Infra/TypeORM/TypeORMRevisionRepositoryResolver.ts new file mode 100644 index 000000000..53bf58319 --- /dev/null +++ b/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 + } +} diff --git a/packages/revisions/src/Mapping/Http/RevisionHttpMapper.ts b/packages/revisions/src/Mapping/Http/RevisionHttpMapper.ts index 4a7666434..fc3271510 100644 --- a/packages/revisions/src/Mapping/Http/RevisionHttpMapper.ts +++ b/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 { + 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, diff --git a/packages/revisions/src/Mapping/Http/RevisionHttpRepresentation.ts b/packages/revisions/src/Mapping/Http/RevisionHttpRepresentation.ts new file mode 100644 index 000000000..618071349 --- /dev/null +++ b/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 +} diff --git a/packages/revisions/src/Mapping/Http/RevisionMetadataHttpMapper.ts b/packages/revisions/src/Mapping/Http/RevisionMetadataHttpMapper.ts index 3e56ba2e3..c95636429 100644 --- a/packages/revisions/src/Mapping/Http/RevisionMetadataHttpMapper.ts +++ b/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 { constructor(private getRequiredRoleToViewRevision: SyncUseCaseInterface) {} - 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, diff --git a/packages/revisions/src/Mapping/Http/RevisionMetadataHttpRepresentation.ts b/packages/revisions/src/Mapping/Http/RevisionMetadataHttpRepresentation.ts new file mode 100644 index 000000000..13533e669 --- /dev/null +++ b/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 +} diff --git a/packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper.ts b/packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper.ts index 6da97e65f..95f458ffc 100644 --- a/packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper.ts +++ b/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 { - toDomain(projection: TypeORMRevision): RevisionMetadata { +export class SQLRevisionMetadataPersistenceMapper implements MapperInterface { + 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 { - toDomain(projection: TypeORMRevision): Revision { +export class SQLRevisionPersistenceMapper implements MapperInterface { + 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(TYPES.Sync_DuplicateItemSyncedEventHandler) .toConstantValue( new DuplicateItemSyncedEventHandler( - container.get(TYPES.Sync_SQLItemRepository), - isSecondaryDatabaseEnabled ? container.get(TYPES.Sync_MongoDBItemRepository) : null, + container.get(TYPES.Sync_ItemRepositoryResolver), container.get(TYPES.Sync_DomainEventFactory), container.get(TYPES.Sync_DomainEventPublisher), container.get(TYPES.Sync_Logger), @@ -871,8 +870,7 @@ export class ContainerConfigLoader { .bind(TYPES.Sync_AccountDeletionRequestedEventHandler) .toConstantValue( new AccountDeletionRequestedEventHandler( - container.get(TYPES.Sync_SQLItemRepository), - isSecondaryDatabaseEnabled ? container.get(TYPES.Sync_MongoDBItemRepository) : null, + container.get(TYPES.Sync_ItemRepositoryResolver), container.get(TYPES.Sync_Logger), ), ) @@ -880,8 +878,7 @@ export class ContainerConfigLoader { .bind(TYPES.Sync_ItemRevisionCreationRequestedEventHandler) .toConstantValue( new ItemRevisionCreationRequestedEventHandler( - container.get(TYPES.Sync_SQLItemRepository), - isSecondaryDatabaseEnabled ? container.get(TYPES.Sync_MongoDBItemRepository) : null, + container.get(TYPES.Sync_ItemRepositoryResolver), container.get(TYPES.Sync_ItemBackupService), container.get(TYPES.Sync_DomainEventFactory), container.get(TYPES.Sync_DomainEventPublisher), diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts index ebcf9ca59..c2bae0abd 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts +++ b/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, } } diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts index 31313f644..171a8c60f 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/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 } diff --git a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts b/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts index 10f2b5ddf..ed3c15d90 100644 --- a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts +++ b/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 - primaryItemRepository.findAll = jest.fn().mockReturnValue([item]) - primaryItemRepository.deleteByUserUuid = jest.fn() + itemRepository = {} as jest.Mocked + itemRepository.findAll = jest.fn().mockReturnValue([item]) + itemRepository.deleteByUserUuid = jest.fn() + + itemRepositoryResolver = {} as jest.Mocked + itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository) logger = {} as jest.Mocked 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 - 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() }) }) diff --git a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts index 229a81146..7c40d6f1a 100644 --- a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts +++ b/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 { - 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}`) } diff --git a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts b/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts index cbfab9b38..d97b06c9d 100644 --- a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.spec.ts +++ b/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 - primaryItemRepository.findByUuidAndUserUuid = jest + itemRepository = {} as jest.Mocked + itemRepository.findByUuidAndUserUuid = jest .fn() .mockReturnValueOnce(duplicateItem) .mockReturnValueOnce(originalItem) + itemRepositoryResolver = {} as jest.Mocked + itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository) + logger = {} as jest.Mocked 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 @@ -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 - 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) diff --git a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts index 7cdefe245..b484d2391 100644 --- a/packages/syncing-server/src/Domain/Handler/DuplicateItemSyncedEventHandler.ts +++ b/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 { - 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, }), ) } diff --git a/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts b/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts index 3d4a2569d..25d9e2c30 100644 --- a/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.spec.ts +++ b/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 - primaryItemRepository.findByUuid = jest.fn().mockReturnValue(item) + itemRepository = {} as jest.Mocked + itemRepository.findByUuid = jest.fn().mockReturnValue(item) + + itemRepositoryResolver = {} as jest.Mocked + itemRepositoryResolver.resolve = jest.fn().mockReturnValue(itemRepository) event = {} as jest.Mocked 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 - 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) diff --git a/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.ts index f859d52dc..66eff2c6f 100644 --- a/packages/syncing-server/src/Domain/Handler/ItemRevisionCreationRequestedEventHandler.ts +++ b/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 { - 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, + }), ) } } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts index 08dadea03..fccc3f58e 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/SaveNewItem/SaveNewItem.ts @@ -146,16 +146,21 @@ export class SaveNewItem implements UseCaseInterface { 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, + }), ) } diff --git a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts index 2535b892f..842eb7caa 100644 --- a/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts +++ b/packages/syncing-server/src/Domain/UseCase/Syncing/UpdateExistingItem/UpdateExistingItem.ts @@ -190,20 +190,22 @@ export class UpdateExistingItem implements UseCaseInterface { [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, + }), ) }