feat(validationPipe): Global Exception Filter basic setup using APP_FILTER and APP_PIPE

This commit is contained in:
Matthieu Haineault 2025-09-11 16:48:05 -04:00
parent ef4f6340d2
commit f9931f99c8
6 changed files with 103 additions and 28 deletions

View File

@ -876,6 +876,52 @@
]
}
},
"/shifts/upsert/{email}/{date}": {
"put": {
"operationId": "ShiftsController_upsert_by_date",
"parameters": [
{
"name": "email",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "date",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpsertShiftDto"
}
}
}
},
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"access-token": []
}
],
"tags": [
"Shifts"
]
}
},
"/shifts": {
"post": {
"operationId": "ShiftsController_create",
@ -2513,6 +2559,10 @@
}
}
},
"UpsertShiftDto": {
"type": "object",
"properties": {}
},
"CreateShiftDto": {
"type": "object",
"properties": {

View File

@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { BadRequestException, Module, ValidationPipe } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ArchivalModule } from './modules/archival/archival.module';
@ -22,6 +22,9 @@ import { ShiftsModule } from './modules/shifts/shifts.module';
import { TimesheetsModule } from './modules/timesheets/timesheets.module';
import { UsersModule } from './modules/users-management/users.module';
import { ConfigModule } from '@nestjs/config';
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { ValidationError } from 'class-validator';
@Module({
imports: [
@ -46,6 +49,29 @@ import { ConfigModule } from '@nestjs/config';
UsersModule,
],
controllers: [AppController, HealthController],
providers: [AppService, OvertimeService],
providers: [
AppService,
OvertimeService,
{
provide: APP_FILTER,
useClass: HttpExceptionFilter
},
{
provide: APP_PIPE,
useValue: new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
exceptionFactory: (errors: ValidationError[] = [])=> {
const messages = errors.flatMap((e)=> Object.values(e.constraints ?? {}));
return new BadRequestException({
statusCode: 400,
error: 'Bad Request',
message: messages.length ? messages : errors,
});
},
}),
},
],
})
export class AppModule {}

View File

@ -0,0 +1,24 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const http_context = host.switchToHttp();
const response = http_context.getResponse<Response>();
const request = http_context.getRequest<Request>();
const http_status = exception.getStatus();
const exception_response = exception.getResponse();
const normalized = typeof exception_response === 'string'
? { message: exception_response }
: (exception_response as Record<string, unknown>);
const response_body = {
statusCode: http_status,
timestamp: new Date().toISOString(),
path: request.url,
...normalized,
};
response.status(http_status).json(response_body);
}
}

View File

@ -11,7 +11,6 @@ import { ATT_TMP_DIR } from './config/attachment.config'; // log to be removed p
import { ModuleRef, NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
// import { JwtAuthGuard } from './modules/authentication/guards/jwt-auth.guard';
import { RolesGuard } from './common/guards/roles.guard';
import { OwnershipGuard } from './common/guards/ownership.guard';
@ -25,8 +24,6 @@ async function bootstrap() {
const reflector = app.get(Reflector); //setup Reflector for Roles()
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, transform: true}));
app.useGlobalGuards(
// new JwtAuthGuard(reflector), //Authentification JWT
new RolesGuard(reflector), //deny-by-default and Role-based Access Control

View File

@ -16,4 +16,3 @@ export function toDateOnlyUTC(input: string | Date): Date {
const date = new Date(input);
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
}

View File

@ -4,7 +4,6 @@ import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service";
import { ShiftPayloadDto, UpsertShiftDto } from "../dtos/upsert-shift.dto";
import { timeFromHHMMUTC, toDateOnlyUTC, weekStartMondayUTC } from "../helpers/shifts-date-time-helpers";
import { error, time } from "console";
type DayShiftResponse = {
start_time: string;
@ -16,14 +15,10 @@ type DayShiftResponse = {
type UpsertAction = 'created' | 'updated' | 'deleted';
@Injectable()
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
constructor(prisma: PrismaService) { super(prisma); }
//create/update/delete master method
async upsertShfitsByDate(email:string, date_string: string, dto: UpsertShiftDto):
Promise<{ action: UpsertAction; day: DayShiftResponse[] }> {
@ -230,7 +225,6 @@ async upsertShfitsByDate(email:string, date_string: string, dto: UpsertShiftDto)
return result;
}
private normalize_shift_payload(payload: ShiftPayloadDto) {
//normalize shift's infos
const start_time = timeFromHHMMUTC(payload.start_time);
@ -271,8 +265,6 @@ async upsertShfitsByDate(email:string, date_string: string, dto: UpsertShiftDto)
return `${hh}:${mm}`;
}
//approval methods
protected get delegate() {
@ -288,17 +280,4 @@ async upsertShfitsByDate(email:string, date_string: string, dto: UpsertShiftDto)
this.updateApprovalWithTransaction(transaction, id, is_approved),
);
}
/*
old without new = delete
new without old = post
old with new = patch old with new
*/
async upsertShift(old_shift?: UpsertShiftDto, new_shift?: UpsertShiftDto) {
}
}