diff --git a/confiture-rest-api/src/audits/audit-id.decorator.ts b/confiture-rest-api/src/audits/audit-id.decorator.ts new file mode 100644 index 000000000..6c71ec802 --- /dev/null +++ b/confiture-rest-api/src/audits/audit-id.decorator.ts @@ -0,0 +1,35 @@ +import { Param } from "@nestjs/common"; +import { ApiGoneResponse, ApiNotFoundResponse } from "@nestjs/swagger"; +import { AuditExistsPipe } from "./audit.pipe"; + +function methodDecoratorToParamDecorator( + decorator: MethodDecorator +): ParameterDecorator { + return (target: object, propertyKey: string) => + decorator(target, propertyKey, { value: target[propertyKey] }); +} + +/** + * This decorator can be used to get the URL param corresponding to an audit unique id + * and check if the audit exists before executing the controller method. + * + * If the audit does not exist, a 404 (not found) or a 410 (gone) response is triggered. + * + * This decorator also adds descriptions to swagger’s 404 and 410 response documentation. + * + * ```typescript + * getAudit(@AuditId() id: string) { + * // here we are assured that the audit exists + * } + * ``` + * @param paramName name of the unique id param for the method + */ +export function AuditId(paramName: string = "uniqueId"): ParameterDecorator { + return (target, propertyKey, index) => { + [ + Param(paramName, AuditExistsPipe), + methodDecoratorToParamDecorator(ApiNotFoundResponse({ description: "The audit does not exist." })), + methodDecoratorToParamDecorator(ApiGoneResponse({ description: "The audit has been previously deleted" })) + ].forEach(decorator => decorator(target, propertyKey, index)); + }; +} diff --git a/confiture-rest-api/src/audits/audit.pipe.ts b/confiture-rest-api/src/audits/audit.pipe.ts new file mode 100644 index 000000000..a2fd795eb --- /dev/null +++ b/confiture-rest-api/src/audits/audit.pipe.ts @@ -0,0 +1,33 @@ +import { Injectable, PipeTransform, ArgumentMetadata, GoneException, NotFoundException } from "@nestjs/common"; +import { AuditService } from "./audit.service"; + +/** + * Asynchronously checks that the audit associated with the given editUniqueId exists. + * If it does not, return a 404 or 410 status. + */ +@Injectable() +export class AuditExistsPipe implements PipeTransform { + constructor( + private readonly auditService: AuditService + ) { } + + async transform(value: string, _metadata: ArgumentMetadata) { + const exists = await this.auditService.checkIfAuditExists(value); + if (!exists) { + await this.sendAuditNotFoundStatus(value); + } + return value; + } + + /** + * Send 404 (Not Found) status for audits that never existed + * and 410 (Gone) for audits that existed but were deleted. + */ + private async sendAuditNotFoundStatus(editUniqueId: string) { + if (await this.auditService.checkIfAuditWasDeleted(editUniqueId)) { + throw new GoneException(); + } else { + throw new NotFoundException(); + } + } +} diff --git a/confiture-rest-api/src/audits/audits.controller.ts b/confiture-rest-api/src/audits/audits.controller.ts index 9c15725bc..e3592d929 100644 --- a/confiture-rest-api/src/audits/audits.controller.ts +++ b/confiture-rest-api/src/audits/audits.controller.ts @@ -4,7 +4,6 @@ import { Controller, Delete, Get, - GoneException, HttpStatus, NotFoundException, Param, @@ -19,7 +18,6 @@ import { import { FileInterceptor } from "@nestjs/platform-express"; import { ApiCreatedResponse, - ApiGoneResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, @@ -31,6 +29,7 @@ import { AuthenticationJwtPayload } from "../auth/jwt-payloads"; import { User } from "../auth/user.decorator"; import { MailService } from "../mail/mail.service"; import { AuditExportService } from "./audit-export.service"; +import { AuditId } from "./audit-id.decorator"; import { AuditService } from "./audit.service"; import { AuditListingItemDto } from "./dto/audit-listing-item.dto"; import { AuditDto } from "./dto/entities/audit.dto"; @@ -89,23 +88,14 @@ export class AuditsController { /** Retrieve an audit from the database. */ @Get("/:uniqueId") @ApiOkResponse({ description: "The audit was found.", type: AuditDto }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) - async getAudit(@Param("uniqueId") uniqueId: string): Promise { - const audit = await this.auditService.findAudit(uniqueId); - - if (!audit) { - await this.sendAuditNotFoundStatus(uniqueId); - } - - return audit; + getAudit(@AuditId() uniqueId: string): Promise { + return this.auditService.findAudit(uniqueId); } @Get("/:uniqueId/pages/:pageSlug") - @ApiNotFoundResponse({ description: "The audit or the page does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) + @ApiNotFoundResponse({ description: "The page or the audit does not exist." }) async getAuditPageWithResults( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @Param("pageSlug") pageSlug: string ): Promise { const data = await this.auditService.getPageWithResults(uniqueId, pageSlug); @@ -121,19 +111,11 @@ export class AuditsController { description: "The audit has been successfully updated", type: AuditDto }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) async updateAudit( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @Body() body: UpdateAuditDto ): Promise { - const audit = await this.auditService.updateAudit(uniqueId, body); - - if (!audit) { - await this.sendAuditNotFoundStatus(uniqueId); - } - - return audit; + return this.auditService.updateAudit(uniqueId, body); } /** Update specific fields of an audit in the database. */ @@ -141,17 +123,11 @@ export class AuditsController { @ApiOkResponse({ description: "The audit has been successfully patched" }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) async patchAudit( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @Body() body: PatchAuditDto ): Promise { - const audit = await this.auditService.patchAudit(uniqueId, body); - - if (!audit) { - await this.sendAuditNotFoundStatus(uniqueId); - } + await this.auditService.patchAudit(uniqueId, body); } @Post("/:uniqueId/results/examples") @@ -159,7 +135,7 @@ export class AuditsController { @ApiCreatedResponse({ type: ExampleImageFileDto }) @ApiOperation({ deprecated: true }) async uploadExampleImage( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @UploadedFile( new ParseFilePipeBuilder() .addFileTypeValidator({ @@ -175,12 +151,6 @@ export class AuditsController { file: Express.Multer.File, @Body() body: UploadImageDto ): Promise { - const exists = await this.auditService.checkIfAuditExists(uniqueId); - - if (!exists) { - await this.sendAuditNotFoundStatus(uniqueId); - } - return await this.auditService.saveExampleImage( uniqueId, body.pageId, @@ -194,7 +164,7 @@ export class AuditsController { @UseInterceptors(FileInterceptor("file")) @ApiCreatedResponse({ type: NotesFileDto }) async uploadNotesFile( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @UploadedFile( new ParseFilePipeBuilder() .addMaxSizeValidator({ @@ -206,13 +176,7 @@ export class AuditsController { ) file: Express.Multer.File ): Promise { - const exists = await this.auditService.checkIfAuditExists(uniqueId); - - if (!exists) { - await this.sendAuditNotFoundStatus(uniqueId); - } - - return this.auditService.saveNotesFile(uniqueId, file); + return await this.auditService.saveNotesFile(uniqueId, file); } @Post("/editor/images") @@ -238,7 +202,7 @@ export class AuditsController { @Delete("/:uniqueId/results/examples/:exampleId") async deleteExampleImage( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @Param("exampleId", new ParseIntPipe()) exampleId: number ) { const deleted = await this.auditService.deleteExampleImage( @@ -252,8 +216,9 @@ export class AuditsController { } @Delete("/:uniqueId/notes/files/:fileId") + @ApiNotFoundResponse({ description: "The file or the audit does not exist." }) async deleteAuditFile( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @Param("fileId", new ParseIntPipe()) fileId: number ) { const deleted = await this.auditService.deleteNotesFile( @@ -269,17 +234,8 @@ export class AuditsController { /** Retrieve the results of an audit (compliance data) from the database. */ @Get("/:uniqueId/results") @ApiOkResponse({ type: [CriterionResultDto] }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) - async getAuditResults(@Param("uniqueId") uniqueId: string): Promise { - const results = - await this.auditService.getResultsWithEditUniqueId(uniqueId); - - if (!results) { - await this.sendAuditNotFoundStatus(uniqueId); - } - - return results; + getAuditResults(@AuditId() uniqueId: string): Promise { + return this.auditService.getResultsWithEditUniqueId(uniqueId); } /** Update the compliance data of an audit. */ @@ -287,55 +243,31 @@ export class AuditsController { @ApiOkResponse({ description: "The audit results have been successfully updated." }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) async updateAuditResults( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @Body() body: UpdateResultsDto ) { - const exists = await this.auditService.checkIfAuditExists(uniqueId); - - if (!exists) { - await this.sendAuditNotFoundStatus(uniqueId); - } - await this.auditService.updateResults(uniqueId, body); } /** Flag an audit as "published", completed. */ @Put("/:uniqueId/publish") @ApiOkResponse({ type: AuditDto }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) - async publishAudit(@Param("uniqueId") uniqueId: string): Promise { - const exists = await this.auditService.checkIfAuditExists(uniqueId); - if (!exists) { - await this.sendAuditNotFoundStatus(uniqueId); - } - + async publishAudit(@AuditId() uniqueId: string): Promise { const auditIsComplete = await this.auditService.isAuditComplete(uniqueId); if (!auditIsComplete) { throw new ConflictException( "Cannot publish audit if it is not complete." ); } - - const audit = await this.auditService.publishAudit(uniqueId); - - return audit; + return this.auditService.publishAudit(uniqueId); } /** Delete an audit from the database. */ @Delete("/:uniqueId") @ApiOkResponse({ description: "The audit has been successfully deleted." }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) - async deleteAudit(@Param("uniqueId") uniqueId: string) { - const deleted = await this.auditService.softDeleteAudit(uniqueId); - - if (!deleted) { - await this.sendAuditNotFoundStatus(uniqueId); - } + async deleteAudit(@AuditId() uniqueId: string) { + await this.auditService.softDeleteAudit(uniqueId); } /** @@ -350,10 +282,8 @@ export class AuditsController { description: "The audit has been successfully duplicated.", type: AuditDto }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) async duplicateAudit( - @Param("uniqueId") uniqueId: string, + @AuditId() uniqueId: string, @Body() body: DuplicateAuditDto, @User() user: AuthenticationJwtPayload ): Promise { @@ -362,10 +292,6 @@ export class AuditsController { body.procedureName ); - if (!newAudit) { - await this.sendAuditNotFoundStatus(uniqueId); - } - if (!user) { this.mailer.sendAuditCreatedMail(newAudit).catch((err) => { console.error( @@ -382,27 +308,7 @@ export class AuditsController { @ApiOkResponse({ description: "An export of the audit results using the CSV format." }) - @ApiNotFoundResponse({ description: "The audit does not exist." }) - @ApiGoneResponse({ description: "The audit has been previously deleted." }) - async getCsvExport(@Param("uniqueId") uniqueId: string) { - const exists = await this.auditService.checkIfAuditExists(uniqueId); - - if (!exists) { - return this.sendAuditNotFoundStatus(uniqueId); - } - - return this.auditExportService.getCsvExport(uniqueId); - } - - /** - * Send 404 (Not Found) status for audits that never existed - * and 410 (Gone) for audits that existed but were deleted. - */ - private async sendAuditNotFoundStatus(editUniqueId: string) { - if (await this.auditService.checkIfAuditWasDeleted(editUniqueId)) { - throw new GoneException(); - } else { - throw new NotFoundException(); - } + async getCsvExport(@AuditId() uniqueId: string) { + return await this.auditExportService.getCsvExport(uniqueId); } } diff --git a/confiture-rest-api/src/audits/audits.module.ts b/confiture-rest-api/src/audits/audits.module.ts index 5caa24175..6ca0ebcbe 100644 --- a/confiture-rest-api/src/audits/audits.module.ts +++ b/confiture-rest-api/src/audits/audits.module.ts @@ -3,6 +3,7 @@ import { MulterModule } from "@nestjs/platform-express"; import { MailService } from "src/mail/mail.service"; import { AuditExportService } from "./audit-export.service"; +import { AuditExistsPipe } from "./audit.pipe"; import { AuditService } from "./audit.service"; import { AuditsController } from "./audits.controller"; import { FileStorageService } from "./file-storage.service"; @@ -14,7 +15,8 @@ import { StatementsController } from "./statements.controller"; AuditService, MailService, FileStorageService, - AuditExportService + AuditExportService, + AuditExistsPipe ], controllers: [AuditsController, ReportsController, StatementsController], imports: [