feat(revisions): add deleting revisions

This commit is contained in:
Karol Sójko
2022-11-28 12:28:38 +01:00
parent b912e050ea
commit ac8a69f8d4
12 changed files with 164 additions and 7 deletions
@@ -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<GetRevision>(TYPES.GetRevision)
.toConstantValue(new GetRevision(container.get(TYPES.RevisionRepository)))
container
.bind<DeleteRevision>(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),
),
)
@@ -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
@@ -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<GetRevisionsMetada>
@@ -20,6 +22,9 @@ describe('RevisionsController', () => {
getRevision = {} as jest.Mocked<GetRevision>
getRevision.execute = jest.fn().mockReturnValue(Result.ok())
deleteRevision = {} as jest.Mocked<DeleteRevision>
deleteRevision.execute = jest.fn().mockReturnValue(Result.ok())
logger = {} as jest.Mocked<Logger>
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)
})
})
@@ -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<HttpResponse> {
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<HttpResponse> {
const revisionOrError = await this.doDeleteRevision.execute({
revisionUuid: params.revisionUuid,
userUuid: params.userUuid,
})
if (revisionOrError.isFailed()) {
this.logger.warn(revisionOrError.getError())
return {
status: HttpStatusCode.BadRequest,
data: {
error: {
message: 'Could not delete revision.',
},
},
}
}
return {
status: HttpStatusCode.Success,
data: { message: revisionOrError.getValue() },
}
}
}
@@ -4,6 +4,7 @@ import { Revision } from './Revision'
import { RevisionMetadata } from './RevisionMetadata'
export interface RevisionRepositoryInterface {
removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void>
findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null>
findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>>
save(revision: Revision): Promise<Revision>
@@ -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<RevisionRepositoryInterface>
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()
})
})
@@ -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<string> {
constructor(private revisionRepository: RevisionRepositoryInterface) {}
async execute(dto: DeleteRevisionDTO): Promise<Result<string>> {
const revisionUuidOrError = Uuid.create(dto.revisionUuid)
if (revisionUuidOrError.isFailed()) {
return Result.fail<string>(`Could not delete revision: ${revisionUuidOrError.getError()}`)
}
const revisionUuid = revisionUuidOrError.getValue()
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail<string>(`Could not delete revision: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
await this.revisionRepository.removeOneByUuid(revisionUuid, userUuid)
return Result.ok<string>('Revision removed')
}
}
@@ -0,0 +1,4 @@
export interface DeleteRevisionDTO {
userUuid: string
revisionUuid: string
}
@@ -16,7 +16,7 @@ export class GetRevision implements UseCaseInterface<Revision> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail<Revision>(`Could not get revisions: ${userUuidOrError.getError()}`)
return Result.fail<Revision>(`Could not get revision: ${userUuidOrError.getError()}`)
}
const userUuid = userUuidOrError.getValue()
@@ -0,0 +1,4 @@
export interface DeleteRevisionRequestParams {
revisionUuid: string
userUuid: string
}
@@ -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<results.JsonResult> {
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<results.JsonResult> {
const result = await this.revisionsController.deleteRevision({
revisionUuid: req.params.uuid,
userUuid: response.locals.user.uuid,
})
return this.json(result.data, result.status)
}
}
@@ -13,6 +13,15 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface {
private revisionMapper: MapperInterface<Revision, TypeORMRevision>,
) {}
async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.from('revisions')
.where('uuid = :revisionUuid AND user_uuid = :userUuid', { userUuid, revisionUuid })
.execute()
}
async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null> {
const typeormRevision = await this.ormRepository
.createQueryBuilder()