From ac8a69f8d428e3cf8e4df5269db3cb31d9b118d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 28 Nov 2022 12:28:33 +0100 Subject: [PATCH] feat(revisions): add deleting revisions --- packages/revisions/src/Bootstrap/Container.ts | 5 +++ packages/revisions/src/Bootstrap/Types.ts | 1 + .../Controller/RevisionsController.spec.ts | 20 ++++++++- .../src/Controller/RevisionsController.ts | 36 ++++++++++++++-- .../Revision/RevisionRepositoryInterface.ts | 1 + .../DeleteRevision/DeleteRevision.spec.ts | 41 +++++++++++++++++++ .../UseCase/DeleteRevision/DeleteRevision.ts | 26 ++++++++++++ .../DeleteRevision/DeleteRevisionDTO.ts | 4 ++ .../Domain/UseCase/GetRevision/GetRevision.ts | 2 +- .../Infra/Http/DeleteRevisionRequestParams.ts | 4 ++ .../InversifyExpressRevisionsController.ts | 22 +++++++++- .../Infra/MySQL/MySQLRevisionRepository.ts | 9 ++++ 12 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts create mode 100644 packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts create mode 100644 packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts create mode 100644 packages/revisions/src/Infra/Http/DeleteRevisionRequestParams.ts diff --git a/packages/revisions/src/Bootstrap/Container.ts b/packages/revisions/src/Bootstrap/Container.ts index 7d1486ed7..6dba3ba38 100644 --- a/packages/revisions/src/Bootstrap/Container.ts +++ b/packages/revisions/src/Bootstrap/Container.ts @@ -37,6 +37,7 @@ import { DumpRepositoryInterface } from '../Domain/Dump/DumpRepositoryInterface' import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository' import { FSDumpRepository } from '../Infra/FS/FSDumpRepository' import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision' +import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -156,6 +157,9 @@ export class ContainerConfigLoader { container .bind(TYPES.GetRevision) .toConstantValue(new GetRevision(container.get(TYPES.RevisionRepository))) + container + .bind(TYPES.DeleteRevision) + .toConstantValue(new DeleteRevision(container.get(TYPES.RevisionRepository))) // Controller container @@ -164,6 +168,7 @@ export class ContainerConfigLoader { new RevisionsController( container.get(TYPES.GetRevisionsMetada), container.get(TYPES.GetRevision), + container.get(TYPES.DeleteRevision), container.get(TYPES.Logger), ), ) diff --git a/packages/revisions/src/Bootstrap/Types.ts b/packages/revisions/src/Bootstrap/Types.ts index b2fd3d62a..dc1b9474d 100644 --- a/packages/revisions/src/Bootstrap/Types.ts +++ b/packages/revisions/src/Bootstrap/Types.ts @@ -26,6 +26,7 @@ const TYPES = { // use cases GetRevisionsMetada: Symbol.for('GetRevisionsMetada'), GetRevision: Symbol.for('GetRevision'), + DeleteRevision: Symbol.for('DeleteRevision'), // Controller RevisionsController: Symbol.for('RevisionsController'), // Handlers diff --git a/packages/revisions/src/Controller/RevisionsController.spec.ts b/packages/revisions/src/Controller/RevisionsController.spec.ts index 15d1af67c..8538617b2 100644 --- a/packages/revisions/src/Controller/RevisionsController.spec.ts +++ b/packages/revisions/src/Controller/RevisionsController.spec.ts @@ -1,6 +1,7 @@ import { Result } from '@standardnotes/domain-core' import { Logger } from 'winston' +import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision' import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision' import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada' @@ -9,9 +10,10 @@ import { RevisionsController } from './RevisionsController' describe('RevisionsController', () => { let getRevisionsMetadata: GetRevisionsMetada let getRevision: GetRevision + let deleteRevision: DeleteRevision let logger: Logger - const createController = () => new RevisionsController(getRevisionsMetadata, getRevision, logger) + const createController = () => new RevisionsController(getRevisionsMetadata, getRevision, deleteRevision, logger) beforeEach(() => { getRevisionsMetadata = {} as jest.Mocked @@ -20,6 +22,9 @@ describe('RevisionsController', () => { getRevision = {} as jest.Mocked getRevision.execute = jest.fn().mockReturnValue(Result.ok()) + deleteRevision = {} as jest.Mocked + deleteRevision.execute = jest.fn().mockReturnValue(Result.ok()) + logger = {} as jest.Mocked logger.warn = jest.fn() }) @@ -49,4 +54,17 @@ describe('RevisionsController', () => { expect(response.status).toEqual(400) }) + + it('should delete revision', async () => { + const response = await createController().deleteRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' }) + + expect(response.status).toEqual(200) + }) + + it('should indicate failure to delete revision', async () => { + deleteRevision.execute = jest.fn().mockReturnValue(Result.fail('Oops')) + const response = await createController().deleteRevision({ revisionUuid: '1-2-3', userUuid: '1-2-3' }) + + expect(response.status).toEqual(400) + }) }) diff --git a/packages/revisions/src/Controller/RevisionsController.ts b/packages/revisions/src/Controller/RevisionsController.ts index 9f20207f0..38046456a 100644 --- a/packages/revisions/src/Controller/RevisionsController.ts +++ b/packages/revisions/src/Controller/RevisionsController.ts @@ -5,11 +5,14 @@ import { GetRevisionsMetada } from '../Domain/UseCase/GetRevisionsMetada/GetRevi import { GetRevisionsMetadataRequestParams } from '../Infra/Http/GetRevisionsMetadataRequestParams' import { GetRevisionRequestParams } from '../Infra/Http/GetRevisionRequestParams' import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision' +import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision' +import { DeleteRevisionRequestParams } from '../Infra/Http/DeleteRevisionRequestParams' export class RevisionsController { constructor( private getRevisionsMetadata: GetRevisionsMetada, private doGetRevision: GetRevision, + private doDeleteRevision: DeleteRevision, private logger: Logger, ) {} @@ -39,13 +42,13 @@ export class RevisionsController { } async getRevision(params: GetRevisionRequestParams): Promise { - const revisionMetadataOrError = await this.doGetRevision.execute({ + const revisionOrError = await this.doGetRevision.execute({ revisionUuid: params.revisionUuid, userUuid: params.userUuid, }) - if (revisionMetadataOrError.isFailed()) { - this.logger.warn(revisionMetadataOrError.getError()) + if (revisionOrError.isFailed()) { + this.logger.warn(revisionOrError.getError()) return { status: HttpStatusCode.BadRequest, @@ -59,7 +62,32 @@ export class RevisionsController { return { status: HttpStatusCode.Success, - data: { revision: revisionMetadataOrError.getValue() }, + data: { revision: 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/Revision/RevisionRepositoryInterface.ts b/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts index f7a35b647..2832c1fac 100644 --- a/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts +++ b/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts @@ -4,6 +4,7 @@ import { Revision } from './Revision' import { RevisionMetadata } from './RevisionMetadata' export interface RevisionRepositoryInterface { + removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise> save(revision: Revision): Promise diff --git a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts new file mode 100644 index 000000000..cd2123f46 --- /dev/null +++ b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.spec.ts @@ -0,0 +1,41 @@ +import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface' +import { DeleteRevision } from './DeleteRevision' + +describe('DeleteRevision', () => { + let revisionRepository: RevisionRepositoryInterface + + const createUseCase = () => new DeleteRevision(revisionRepository) + + beforeEach(() => { + revisionRepository = {} as jest.Mocked + revisionRepository.removeOneByUuid = jest.fn() + }) + + it('should delete revision', async () => { + const result = await createUseCase().execute({ + revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', + userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', + }) + + expect(result.isFailed()).toBeFalsy() + expect(result.getValue()).toEqual('Revision removed') + }) + + 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', + }) + + expect(result.isFailed()).toBeTruthy() + }) + + it('should not delete revision for a an invalid user uuid', async () => { + const result = await createUseCase().execute({ + userUuid: '1-2-3', + revisionUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d', + }) + + expect(result.isFailed()).toBeTruthy() + }) +}) diff --git a/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts new file mode 100644 index 000000000..84290465a --- /dev/null +++ b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevision.ts @@ -0,0 +1,26 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' + +import { RevisionRepositoryInterface } from '../../Revision/RevisionRepositoryInterface' +import { DeleteRevisionDTO } from './DeleteRevisionDTO' + +export class DeleteRevision implements UseCaseInterface { + constructor(private revisionRepository: RevisionRepositoryInterface) {} + + async execute(dto: DeleteRevisionDTO): Promise> { + const revisionUuidOrError = Uuid.create(dto.revisionUuid) + if (revisionUuidOrError.isFailed()) { + return Result.fail(`Could not delete revision: ${revisionUuidOrError.getError()}`) + } + const revisionUuid = revisionUuidOrError.getValue() + + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(`Could not delete revision: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + await this.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 new file mode 100644 index 000000000..b1acf5a97 --- /dev/null +++ b/packages/revisions/src/Domain/UseCase/DeleteRevision/DeleteRevisionDTO.ts @@ -0,0 +1,4 @@ +export interface DeleteRevisionDTO { + userUuid: string + revisionUuid: string +} diff --git a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts b/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts index 9936fd5f4..7f435be65 100644 --- a/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts +++ b/packages/revisions/src/Domain/UseCase/GetRevision/GetRevision.ts @@ -16,7 +16,7 @@ export class GetRevision implements UseCaseInterface { const userUuidOrError = Uuid.create(dto.userUuid) if (userUuidOrError.isFailed()) { - return Result.fail(`Could not get revisions: ${userUuidOrError.getError()}`) + return Result.fail(`Could not get revision: ${userUuidOrError.getError()}`) } const userUuid = userUuidOrError.getValue() diff --git a/packages/revisions/src/Infra/Http/DeleteRevisionRequestParams.ts b/packages/revisions/src/Infra/Http/DeleteRevisionRequestParams.ts new file mode 100644 index 000000000..d31fbd286 --- /dev/null +++ b/packages/revisions/src/Infra/Http/DeleteRevisionRequestParams.ts @@ -0,0 +1,4 @@ +export interface DeleteRevisionRequestParams { + revisionUuid: string + userUuid: string +} diff --git a/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts b/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts index 1a1ae473a..52091a16e 100644 --- a/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts +++ b/packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express' -import { BaseHttpController, controller, httpGet, results } from 'inversify-express-utils' +import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils' import { inject } from 'inversify' import TYPES from '../../Bootstrap/Types' @@ -20,4 +20,24 @@ export class InversifyExpressRevisionsController extends BaseHttpController { return this.json(result.data, result.status) } + + @httpGet('/:uuid') + public async getRevision(req: Request, response: Response): Promise { + const result = await this.revisionsController.getRevision({ + revisionUuid: req.params.uuid, + userUuid: response.locals.user.uuid, + }) + + return this.json(result.data, result.status) + } + + @httpDelete('/:uuid') + public async deleteRevision(req: Request, response: Response): Promise { + const result = await this.revisionsController.deleteRevision({ + revisionUuid: req.params.uuid, + userUuid: response.locals.user.uuid, + }) + + return this.json(result.data, result.status) + } } diff --git a/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts b/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts index aef2dde2d..694c7452c 100644 --- a/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts +++ b/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts @@ -13,6 +13,15 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface { private revisionMapper: MapperInterface, ) {} + async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise { + await this.ormRepository + .createQueryBuilder() + .delete() + .from('revisions') + .where('uuid = :revisionUuid AND user_uuid = :userUuid', { userUuid, revisionUuid }) + .execute() + } + async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise { const typeormRevision = await this.ormRepository .createQueryBuilder()