From 21c697953f9257e7082836d124701b1d8d146db0 Mon Sep 17 00:00:00 2001 From: Marshall Sorenson Date: Fri, 24 May 2024 18:12:03 -0400 Subject: [PATCH] feat: support validation pipe errors --- packages/nestjs-exception/package.json | 1 + .../__fixtures__/app.controller.fixture.ts | 11 ++++++++ .../src/__fixtures__/dummy.dto.ts | 6 +++++ .../src/filters/exceptions.filter.e2e-spec.ts | 12 +++++++++ .../src/filters/exceptions.filter.ts | 25 +++++++++---------- yarn.lock | 2 +- 6 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 packages/nestjs-exception/src/__fixtures__/dummy.dto.ts diff --git a/packages/nestjs-exception/package.json b/packages/nestjs-exception/package.json index b71f14b81..0b5b25a2b 100644 --- a/packages/nestjs-exception/package.json +++ b/packages/nestjs-exception/package.json @@ -20,6 +20,7 @@ "@nestjs/core": "^9.0.0", "@nestjs/testing": "^9.0.0", "@types/supertest": "^2.0.11", + "class-validator": "^0.13.2", "supertest": "^6.1.6" } } diff --git a/packages/nestjs-exception/src/__fixtures__/app.controller.fixture.ts b/packages/nestjs-exception/src/__fixtures__/app.controller.fixture.ts index b68e736dc..fd40c5d13 100644 --- a/packages/nestjs-exception/src/__fixtures__/app.controller.fixture.ts +++ b/packages/nestjs-exception/src/__fixtures__/app.controller.fixture.ts @@ -1,11 +1,16 @@ import { BadRequestException, + Body, Controller, Get, Param, ParseIntPipe, + Post, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { DummyDto } from './dummy.dto'; import { CustomNotFoundExceptionFixture } from './exceptions/custom-not-found.exception.fixture'; /** @@ -24,6 +29,12 @@ export class AppControllerFixture { throw new BadRequestException(); } + @UsePipes(new ValidationPipe()) + @Post('/bad-validation') + getBadValidation(@Body() value: DummyDto): DummyDto { + return value; + } + @Get(':id') getErrorNotFound(@Param('id', ParseIntPipe) id: number): void { throw new CustomNotFoundExceptionFixture(id); diff --git a/packages/nestjs-exception/src/__fixtures__/dummy.dto.ts b/packages/nestjs-exception/src/__fixtures__/dummy.dto.ts new file mode 100644 index 000000000..ec6af0e81 --- /dev/null +++ b/packages/nestjs-exception/src/__fixtures__/dummy.dto.ts @@ -0,0 +1,6 @@ +import { IsNumber } from 'class-validator'; + +export class DummyDto { + @IsNumber() + id!: number; +} diff --git a/packages/nestjs-exception/src/filters/exceptions.filter.e2e-spec.ts b/packages/nestjs-exception/src/filters/exceptions.filter.e2e-spec.ts index a1fb4f654..607c6a562 100644 --- a/packages/nestjs-exception/src/filters/exceptions.filter.e2e-spec.ts +++ b/packages/nestjs-exception/src/filters/exceptions.filter.e2e-spec.ts @@ -41,6 +41,18 @@ describe('Exception (e2e)', () => { expect(response.body.message).toEqual('Bad Request'); }); + it('Should return array of validation errors', async () => { + const response = await supertest(app.getHttpServer()) + .post('/test/bad-validation') + .send({ id: 'not a number' }); + expect(response.body.statusCode).toEqual(HttpStatus.BAD_REQUEST); + expect(response.body.errorCode).toEqual('HTTP_BAD_REQUEST'); + expect(typeof response.body.message).toEqual('object'); + expect(response.body.message).toEqual([ + 'id must be a number conforming to the specified constraints', + ]); + }); + it('Should return not found', async () => { const response = await supertest(app.getHttpServer()).get('/test/123'); expect(response.body.errorCode).toEqual('CUSTOM_NOT_FOUND'); diff --git a/packages/nestjs-exception/src/filters/exceptions.filter.ts b/packages/nestjs-exception/src/filters/exceptions.filter.ts index bbafbebca..8338ca6d8 100644 --- a/packages/nestjs-exception/src/filters/exceptions.filter.ts +++ b/packages/nestjs-exception/src/filters/exceptions.filter.ts @@ -1,24 +1,17 @@ -import { - ExceptionFilter, - Catch, - ArgumentsHost, - HttpException, - HttpAdapterHost, -} from '@nestjs/common'; +import { HttpAdapterHost } from '@nestjs/core'; +import { Catch, ArgumentsHost, HttpException } from '@nestjs/common'; +import { isObject } from '@nestjs/common/utils/shared.utils'; import { ExceptionInterface } from '@concepta/ts-core'; import { ERROR_CODE_UNKNOWN } from '../constants/error-codes.constants'; import { RuntimeException } from '../exceptions/runtime.exception'; import { mapHttpStatus } from '../utils/map-http-status.util'; @Catch() -export class ExceptionsFilter implements ExceptionFilter { +export class ExceptionsFilter implements ExceptionsFilter { constructor(private readonly httpAdapterHost: HttpAdapterHost) {} catch(exception: ExceptionInterface, host: ArgumentsHost): void { - // in certain situations `httpAdapter` might not be available in the - // constructor method, thus we should resolve it here. const { httpAdapter } = this.httpAdapterHost; - const ctx = host.switchToHttp(); // error code is UNKNOWN unless it gets overridden @@ -28,7 +21,7 @@ export class ExceptionsFilter implements ExceptionFilter { let statusCode = 500; // what will this message be? - let message: string = 'Internal Server Error'; + let message: unknown = 'Internal Server Error'; // is this an http exception? if (exception instanceof HttpException) { @@ -36,8 +29,14 @@ export class ExceptionsFilter implements ExceptionFilter { statusCode = exception.getStatus(); // map the error code errorCode = mapHttpStatus(statusCode); + // get res + const res = exception.getResponse(); // set the message - message = exception.message; + if (isObject(res) && 'message' in res) { + message = res.message; + } else { + message = res; + } } else if (exception instanceof RuntimeException) { // its a runtime exception, set error code errorCode = exception.errorCode; diff --git a/yarn.lock b/yarn.lock index 33eb28b22..39474fbb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4897,7 +4897,7 @@ class-validator@*: libphonenumber-js "^1.10.14" validator "^13.7.0" -class-validator@^0.13.0: +class-validator@^0.13.0, class-validator@^0.13.2: version "0.13.2" resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.2.tgz#64b031e9f3f81a1e1dcd04a5d604734608b24143" integrity sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==