Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions confiture-rest-api/src/audits/audit-id.decorator.ts
Original file line number Diff line number Diff line change
@@ -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) =>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai une erreur TS ici :

Type '(target: object, propertyKey: string) => void | TypedPropertyDescriptor<any>' is not assignable to type 'ParameterDecorator'.
  Types of parameters 'propertyKey' and 'propertyKey' are incompatible.
    Type 'string | symbol' is not assignable to type 'string'.
      Type 'symbol' is not assignable to type 'string'.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

je l’ai pas en local, tu as bien yarn install ?

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));
};
}
33 changes: 33 additions & 0 deletions confiture-rest-api/src/audits/audit.pipe.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
13 changes: 13 additions & 0 deletions confiture-rest-api/src/audits/audit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,19 @@ export class AuditService {
return pagesWithSlug;
}

/**
* @param editUniqueId id of the audit to look for
* @param isHidden look for hidden audits, default: false
* @returns true if the audit exists in db, false otherwise
*/
async checkIfAuditExists(editUniqueId: string, isHidden: boolean = false): Promise<boolean> {
const audit = await this.prisma.audit.findFirst({
where: { editUniqueId, isHidden },
select: { id: true }
});
return !!audit;
}

findAuditWithEditUniqueId(uniqueId: string, include?: Prisma.AuditInclude) {
return this.prisma.audit.findFirst({
where: { editUniqueId: uniqueId, isHidden: false },
Expand Down
143 changes: 26 additions & 117 deletions confiture-rest-api/src/audits/audits.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Controller,
Delete,
Get,
GoneException,
HttpStatus,
NotFoundException,
Param,
Expand All @@ -19,7 +18,6 @@ import {
import { FileInterceptor } from "@nestjs/platform-express";
import {
ApiCreatedResponse,
ApiGoneResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
Expand All @@ -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";
Expand Down Expand Up @@ -89,10 +88,10 @@ 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<AuditDto> {
const audit = await this.auditService.findAuditWithEditUniqueId(uniqueId, {
async getAudit(
@AuditId() uniqueId: string
): Promise<AuditDto> {
return this.auditService.findAuditWithEditUniqueId(uniqueId, {
environments: true,
transverseElementsPage: true,
pages: true,
Expand All @@ -107,19 +106,12 @@ export class AuditsController {
}
}
});

if (!audit) {
await this.sendAuditNotFoundStatus(uniqueId);
}

return audit;
}

@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<GetPageWithResultsDto> {
const data = await this.auditService.getPageWithResults(uniqueId, pageSlug);
Expand All @@ -135,45 +127,31 @@ 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<AuditDto> {
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. */
@Patch("/:uniqueId")
@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<void> {
const audit = await this.auditService.patchAudit(uniqueId, body);

if (!audit) {
await this.sendAuditNotFoundStatus(uniqueId);
}
await this.auditService.patchAudit(uniqueId, body);
}

@Post("/:uniqueId/results/examples")
@UseInterceptors(FileInterceptor("image"))
@ApiCreatedResponse({ type: ExampleImageFileDto })
@ApiOperation({ deprecated: true })
async uploadExampleImage(
@Param("uniqueId") uniqueId: string,
@AuditId() uniqueId: string,
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
Expand All @@ -189,12 +167,6 @@ export class AuditsController {
file: Express.Multer.File,
@Body() body: UploadImageDto
): Promise<ExampleImageFileDto> {
const audit = await this.auditService.findAuditWithEditUniqueId(uniqueId);

if (!audit) {
await this.sendAuditNotFoundStatus(uniqueId);
}

return await this.auditService.saveExampleImage(
uniqueId,
body.pageId,
Expand All @@ -208,7 +180,7 @@ export class AuditsController {
@UseInterceptors(FileInterceptor("file"))
@ApiCreatedResponse({ type: NotesFileDto })
async uploadNotesFile(
@Param("uniqueId") uniqueId: string,
@AuditId() uniqueId: string,
@UploadedFile(
new ParseFilePipeBuilder()
.addMaxSizeValidator({
Expand All @@ -220,12 +192,6 @@ export class AuditsController {
)
file: Express.Multer.File
): Promise<NotesFileDto> {
const audit = await this.auditService.getAuditWithEditUniqueId(uniqueId);

if (!audit) {
await this.sendAuditNotFoundStatus(uniqueId);
}

return await this.auditService.saveNotesFile(uniqueId, file);
}

Expand All @@ -252,7 +218,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(
Expand All @@ -266,8 +232,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(
Expand All @@ -283,72 +250,40 @@ 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<CriterionResultDto[]> {
const results =
await this.auditService.getResultsWithEditUniqueId(uniqueId);

if (!results) {
await this.sendAuditNotFoundStatus(uniqueId);
}

return results;
getAuditResults(@AuditId() uniqueId: string): Promise<CriterionResultDto[]> {
return this.auditService.getResultsWithEditUniqueId(uniqueId);
}

/** Update the compliance data of an audit. */
@Patch("/:uniqueId/results")
@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 audit = await this.auditService.findAuditWithEditUniqueId(uniqueId);

if (!audit) {
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<AuditDto> {
async publishAudit(@AuditId() uniqueId: string): Promise<AuditDto> {
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);

if (!audit) {
await this.sendAuditNotFoundStatus(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);
}

/**
Expand All @@ -363,10 +298,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<AuditDto> {
Expand All @@ -375,10 +308,6 @@ export class AuditsController {
body.procedureName
);

if (!newAudit) {
await this.sendAuditNotFoundStatus(uniqueId);
}

if (!user) {
this.mailer.sendAuditCreatedMail(newAudit).catch((err) => {
console.error(
Expand All @@ -395,27 +324,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 file = await this.auditExportService.getCsvExport(uniqueId);

if (!file) {
return this.sendAuditNotFoundStatus(uniqueId);
}

return file;
}

/**
* 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);
}
}
Loading
Loading