From 7fbfa8a68aedd6c5368e1e6a0553620626a878b5 Mon Sep 17 00:00:00 2001 From: "OMAR.A" <58332033+civilcoder55@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:04:03 +0200 Subject: [PATCH] feat(common): add new flatten options to validation pipe --- packages/common/pipes/validation.pipe.ts | 33 ++- .../common/test/pipes/validation.pipe.spec.ts | 200 +++++++++++++++++- 2 files changed, 226 insertions(+), 7 deletions(-) diff --git a/packages/common/pipes/validation.pipe.ts b/packages/common/pipes/validation.pipe.ts index a801fa64ce7..882066a5fa9 100644 --- a/packages/common/pipes/validation.pipe.ts +++ b/packages/common/pipes/validation.pipe.ts @@ -26,9 +26,11 @@ import { isNil, isUndefined } from '../utils/shared.utils'; export interface ValidationPipeOptions extends ValidatorOptions { transform?: boolean; disableErrorMessages?: boolean; + disableFlattenErrorMessages?: boolean; + flatExceptionFactoryMessage?: boolean; transformOptions?: ClassTransformOptions; errorHttpStatusCode?: ErrorHttpStatusCode; - exceptionFactory?: (errors: ValidationError[]) => any; + exceptionFactory?: (errors: ValidationError[] | string[]) => any; validateCustomDecorators?: boolean; expectedType?: Type; validatorPackage?: ValidatorPackage; @@ -47,18 +49,23 @@ let classTransformer: TransformerPackage = {} as any; export class ValidationPipe implements PipeTransform { protected isTransformEnabled: boolean; protected isDetailedOutputDisabled?: boolean; + protected isFlattenErrorMessagesDisabled?: boolean; + protected isFlattenExceptionFactoryErrorsEnabled?: boolean; protected validatorOptions: ValidatorOptions; protected transformOptions: ClassTransformOptions; protected errorHttpStatusCode: ErrorHttpStatusCode; protected expectedType: Type; - protected exceptionFactory: (errors: ValidationError[]) => any; + protected exceptionFactory: (errors: ValidationError[] | string[]) => any; protected validateCustomDecorators: boolean; + protected hasExceptionFactory: boolean; constructor(@Optional() options?: ValidationPipeOptions) { options = options || {}; const { transform, disableErrorMessages, + disableFlattenErrorMessages, + flatExceptionFactoryMessage, errorHttpStatusCode, expectedType, transformOptions, @@ -72,11 +79,14 @@ export class ValidationPipe implements PipeTransform { this.isTransformEnabled = !!transform; this.transformOptions = transformOptions; this.isDetailedOutputDisabled = disableErrorMessages; + this.isFlattenErrorMessagesDisabled = disableFlattenErrorMessages; + this.isFlattenExceptionFactoryErrorsEnabled = flatExceptionFactoryMessage; this.validateCustomDecorators = validateCustomDecorators || false; this.errorHttpStatusCode = errorHttpStatusCode || HttpStatus.BAD_REQUEST; this.expectedType = expectedType; this.exceptionFactory = options.exceptionFactory || this.createExceptionFactory(); + this.hasExceptionFactory = !!options.exceptionFactory; classValidator = this.loadValidator(options.validatorPackage); classTransformer = this.loadTransformer(options.transformerPackage); @@ -142,7 +152,11 @@ export class ValidationPipe implements PipeTransform { const errors = await this.validate(entity, this.validatorOptions); if (errors.length > 0) { - throw await this.exceptionFactory(errors); + let validationErrors: ValidationError[] | string[] = errors; + if (this.shouldFlatErrors()) { + validationErrors = this.flattenValidationErrors(errors); + } + throw await this.exceptionFactory(validationErrors); } if (isPrimitive) { // if the value is a primitive value and the validation process has been successfully completed @@ -166,12 +180,11 @@ export class ValidationPipe implements PipeTransform { } public createExceptionFactory() { - return (validationErrors: ValidationError[] = []) => { + return (validationErrors: ValidationError[] | string[] = []) => { if (this.isDetailedOutputDisabled) { return new HttpErrorByCode[this.errorHttpStatusCode](); } - const errors = this.flattenValidationErrors(validationErrors); - return new HttpErrorByCode[this.errorHttpStatusCode](errors); + return new HttpErrorByCode[this.errorHttpStatusCode](validationErrors); }; } @@ -312,4 +325,12 @@ export class ValidationPipe implements PipeTransform { constraints, }; } + + protected shouldFlatErrors(): boolean { + if (this.hasExceptionFactory) { + return this.isFlattenExceptionFactoryErrorsEnabled; + } + + return !this.isFlattenErrorMessagesDisabled; + } } diff --git a/packages/common/test/pipes/validation.pipe.spec.ts b/packages/common/test/pipes/validation.pipe.spec.ts index 43b819c581e..30700e8e891 100644 --- a/packages/common/test/pipes/validation.pipe.spec.ts +++ b/packages/common/test/pipes/validation.pipe.spec.ts @@ -9,13 +9,24 @@ import { IsOptional, IsString, ValidateNested, + ValidationError, } from 'class-validator'; import { HttpStatus } from '../../enums'; -import { UnprocessableEntityException } from '../../exceptions'; +import { + BadRequestException, + HttpException, + UnprocessableEntityException, +} from '../../exceptions'; import { ArgumentMetadata } from '../../interfaces'; import { ValidationPipe } from '../../pipes/validation.pipe'; chai.use(chaiAsPromised); +class CustomTestError extends HttpException { + constructor(errors: any) { + super(errors, 418); + } +} + @Exclude() class TestModelInternal { constructor() {} @@ -585,4 +596,191 @@ describe('ValidationPipe', () => { expect(await target.transform(testObj, m)).to.deep.equal(testObj); }); }); + + describe('option: "exceptionFactory"', () => { + describe('when validation fails', () => { + beforeEach(() => { + target = new ValidationPipe({ + exceptionFactory: errors => new CustomTestError(errors), + }); + }); + it('should throw a CustomTestError exception', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + expect(err).to.be.instanceOf(CustomTestError); + } + }); + }); + }); + + describe('option: "disableFlattenErrorMessages"', () => { + describe('when disableFlattenErrorMessages is true', () => { + beforeEach(() => { + target = new ValidationPipe({ + disableFlattenErrorMessages: true, + }); + }); + it('should throw an exception without flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + const message = err.getResponse().message + expect(err).to.be.instanceOf(BadRequestException); + expect(message).to.be.an('array'); + message.forEach((error: any) => { + expect(error).to.be.instanceOf(ValidationError); + }); + } + }); + }); + + describe('when disableFlattenErrorMessages is false', () => { + beforeEach(() => { + target = new ValidationPipe({ + disableFlattenErrorMessages: false, + }); + }); + it('should throw an exception with flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + const message = err.getResponse().message + expect(err).to.be.instanceOf(BadRequestException); + expect(message).to.be.an('array'); + message.forEach((error: any) => { + expect(error).to.be.a('string'); + }); + } + }); + }); + + describe('when disableFlattenErrorMessages is not set', () => { + beforeEach(() => { + target = new ValidationPipe({}); + }); + it('should throw an exception with flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + const message = err.getResponse().message + expect(err).to.be.instanceOf(BadRequestException); + expect(message).to.be.an('array'); + message.forEach((error: any) => { + expect(error).to.be.a('string'); + }); + } + }); + }); + }); + + describe('option: "flatExceptionFactoryMessage"', () => { + describe('when flatExceptionFactoryMessage is true', () => { + beforeEach(() => { + target = new ValidationPipe({ + flatExceptionFactoryMessage: true, + exceptionFactory: errors => new CustomTestError(errors), + }); + }); + it('should throw a CustomTestError with flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + expect(err).to.be.instanceOf(CustomTestError); + expect(err.getResponse()).to.be.an('array'); + err.getResponse().forEach((error: any) => { + expect(error).to.be.a('string'); + }); + } + }); + }); + + describe('when flatExceptionFactoryMessage is false', () => { + beforeEach(() => { + target = new ValidationPipe({ + flatExceptionFactoryMessage: false, + exceptionFactory: errors => new CustomTestError(errors), + }); + }); + it('should throw a CustomTestError without flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + expect(err).to.be.instanceOf(CustomTestError); + expect(err.getResponse()).to.be.an('array'); + err.getResponse().forEach((error: any) => { + expect(error).to.be.instanceOf(ValidationError); + }); + } + }); + }); + + describe('when flatExceptionFactoryMessage is not set', () => { + beforeEach(() => { + target = new ValidationPipe({ + exceptionFactory: errors => new CustomTestError(errors), + }); + }); + it('should throw a CustomTestError without flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + expect(err).to.be.instanceOf(CustomTestError); + expect(err.getResponse()).to.be.an('array'); + err.getResponse().forEach((error: any) => { + expect(error).to.be.instanceOf(ValidationError); + }); + } + }); + }); + + describe('when flatExceptionFactoryMessage is false without exceptionFactory', () => { + beforeEach(() => { + target = new ValidationPipe({ + flatExceptionFactoryMessage: false, + }); + }); + it('should throw an exception with flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + const message = err.getResponse().message + expect(err).to.be.instanceOf(BadRequestException); + expect(message).to.be.an('array'); + message.forEach((error: any) => { + expect(error).to.be.a('string'); + }); + } + }); + }); + + describe('when flatExceptionFactoryMessage is true without exceptionFactory', () => { + beforeEach(() => { + target = new ValidationPipe({ + flatExceptionFactoryMessage: true, + }); + }); + it('should throw an exception with flatten errors', async () => { + const testObj = { prop1: 'value1' }; + try { + await target.transform(testObj, metadata); + } catch (err) { + const message = err.getResponse().message + expect(err).to.be.instanceOf(BadRequestException); + expect(message).to.be.an('array'); + message.forEach((error: any) => { + expect(error).to.be.a('string'); + }); + } + }); + }); + }); });