Skip to content

Commit 7fbfa8a

Browse files
committed
feat(common): add new flatten options to validation pipe
1 parent b6ea2a1 commit 7fbfa8a

File tree

2 files changed

+226
-7
lines changed

2 files changed

+226
-7
lines changed

packages/common/pipes/validation.pipe.ts

+27-6
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import { isNil, isUndefined } from '../utils/shared.utils';
2626
export interface ValidationPipeOptions extends ValidatorOptions {
2727
transform?: boolean;
2828
disableErrorMessages?: boolean;
29+
disableFlattenErrorMessages?: boolean;
30+
flatExceptionFactoryMessage?: boolean;
2931
transformOptions?: ClassTransformOptions;
3032
errorHttpStatusCode?: ErrorHttpStatusCode;
31-
exceptionFactory?: (errors: ValidationError[]) => any;
33+
exceptionFactory?: (errors: ValidationError[] | string[]) => any;
3234
validateCustomDecorators?: boolean;
3335
expectedType?: Type<any>;
3436
validatorPackage?: ValidatorPackage;
@@ -47,18 +49,23 @@ let classTransformer: TransformerPackage = {} as any;
4749
export class ValidationPipe implements PipeTransform<any> {
4850
protected isTransformEnabled: boolean;
4951
protected isDetailedOutputDisabled?: boolean;
52+
protected isFlattenErrorMessagesDisabled?: boolean;
53+
protected isFlattenExceptionFactoryErrorsEnabled?: boolean;
5054
protected validatorOptions: ValidatorOptions;
5155
protected transformOptions: ClassTransformOptions;
5256
protected errorHttpStatusCode: ErrorHttpStatusCode;
5357
protected expectedType: Type<any>;
54-
protected exceptionFactory: (errors: ValidationError[]) => any;
58+
protected exceptionFactory: (errors: ValidationError[] | string[]) => any;
5559
protected validateCustomDecorators: boolean;
60+
protected hasExceptionFactory: boolean;
5661

5762
constructor(@Optional() options?: ValidationPipeOptions) {
5863
options = options || {};
5964
const {
6065
transform,
6166
disableErrorMessages,
67+
disableFlattenErrorMessages,
68+
flatExceptionFactoryMessage,
6269
errorHttpStatusCode,
6370
expectedType,
6471
transformOptions,
@@ -72,11 +79,14 @@ export class ValidationPipe implements PipeTransform<any> {
7279
this.isTransformEnabled = !!transform;
7380
this.transformOptions = transformOptions;
7481
this.isDetailedOutputDisabled = disableErrorMessages;
82+
this.isFlattenErrorMessagesDisabled = disableFlattenErrorMessages;
83+
this.isFlattenExceptionFactoryErrorsEnabled = flatExceptionFactoryMessage;
7584
this.validateCustomDecorators = validateCustomDecorators || false;
7685
this.errorHttpStatusCode = errorHttpStatusCode || HttpStatus.BAD_REQUEST;
7786
this.expectedType = expectedType;
7887
this.exceptionFactory =
7988
options.exceptionFactory || this.createExceptionFactory();
89+
this.hasExceptionFactory = !!options.exceptionFactory;
8090

8191
classValidator = this.loadValidator(options.validatorPackage);
8292
classTransformer = this.loadTransformer(options.transformerPackage);
@@ -142,7 +152,11 @@ export class ValidationPipe implements PipeTransform<any> {
142152

143153
const errors = await this.validate(entity, this.validatorOptions);
144154
if (errors.length > 0) {
145-
throw await this.exceptionFactory(errors);
155+
let validationErrors: ValidationError[] | string[] = errors;
156+
if (this.shouldFlatErrors()) {
157+
validationErrors = this.flattenValidationErrors(errors);
158+
}
159+
throw await this.exceptionFactory(validationErrors);
146160
}
147161
if (isPrimitive) {
148162
// if the value is a primitive value and the validation process has been successfully completed
@@ -166,12 +180,11 @@ export class ValidationPipe implements PipeTransform<any> {
166180
}
167181

168182
public createExceptionFactory() {
169-
return (validationErrors: ValidationError[] = []) => {
183+
return (validationErrors: ValidationError[] | string[] = []) => {
170184
if (this.isDetailedOutputDisabled) {
171185
return new HttpErrorByCode[this.errorHttpStatusCode]();
172186
}
173-
const errors = this.flattenValidationErrors(validationErrors);
174-
return new HttpErrorByCode[this.errorHttpStatusCode](errors);
187+
return new HttpErrorByCode[this.errorHttpStatusCode](validationErrors);
175188
};
176189
}
177190

@@ -312,4 +325,12 @@ export class ValidationPipe implements PipeTransform<any> {
312325
constraints,
313326
};
314327
}
328+
329+
protected shouldFlatErrors(): boolean {
330+
if (this.hasExceptionFactory) {
331+
return this.isFlattenExceptionFactoryErrorsEnabled;
332+
}
333+
334+
return !this.isFlattenErrorMessagesDisabled;
335+
}
315336
}

packages/common/test/pipes/validation.pipe.spec.ts

+199-1
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@ import {
99
IsOptional,
1010
IsString,
1111
ValidateNested,
12+
ValidationError,
1213
} from 'class-validator';
1314
import { HttpStatus } from '../../enums';
14-
import { UnprocessableEntityException } from '../../exceptions';
15+
import {
16+
BadRequestException,
17+
HttpException,
18+
UnprocessableEntityException,
19+
} from '../../exceptions';
1520
import { ArgumentMetadata } from '../../interfaces';
1621
import { ValidationPipe } from '../../pipes/validation.pipe';
1722
chai.use(chaiAsPromised);
1823

24+
class CustomTestError extends HttpException {
25+
constructor(errors: any) {
26+
super(errors, 418);
27+
}
28+
}
29+
1930
@Exclude()
2031
class TestModelInternal {
2132
constructor() {}
@@ -585,4 +596,191 @@ describe('ValidationPipe', () => {
585596
expect(await target.transform(testObj, m)).to.deep.equal(testObj);
586597
});
587598
});
599+
600+
describe('option: "exceptionFactory"', () => {
601+
describe('when validation fails', () => {
602+
beforeEach(() => {
603+
target = new ValidationPipe({
604+
exceptionFactory: errors => new CustomTestError(errors),
605+
});
606+
});
607+
it('should throw a CustomTestError exception', async () => {
608+
const testObj = { prop1: 'value1' };
609+
try {
610+
await target.transform(testObj, metadata);
611+
} catch (err) {
612+
expect(err).to.be.instanceOf(CustomTestError);
613+
}
614+
});
615+
});
616+
});
617+
618+
describe('option: "disableFlattenErrorMessages"', () => {
619+
describe('when disableFlattenErrorMessages is true', () => {
620+
beforeEach(() => {
621+
target = new ValidationPipe({
622+
disableFlattenErrorMessages: true,
623+
});
624+
});
625+
it('should throw an exception without flatten errors', async () => {
626+
const testObj = { prop1: 'value1' };
627+
try {
628+
await target.transform(testObj, metadata);
629+
} catch (err) {
630+
const message = err.getResponse().message
631+
expect(err).to.be.instanceOf(BadRequestException);
632+
expect(message).to.be.an('array');
633+
message.forEach((error: any) => {
634+
expect(error).to.be.instanceOf(ValidationError);
635+
});
636+
}
637+
});
638+
});
639+
640+
describe('when disableFlattenErrorMessages is false', () => {
641+
beforeEach(() => {
642+
target = new ValidationPipe({
643+
disableFlattenErrorMessages: false,
644+
});
645+
});
646+
it('should throw an exception with flatten errors', async () => {
647+
const testObj = { prop1: 'value1' };
648+
try {
649+
await target.transform(testObj, metadata);
650+
} catch (err) {
651+
const message = err.getResponse().message
652+
expect(err).to.be.instanceOf(BadRequestException);
653+
expect(message).to.be.an('array');
654+
message.forEach((error: any) => {
655+
expect(error).to.be.a('string');
656+
});
657+
}
658+
});
659+
});
660+
661+
describe('when disableFlattenErrorMessages is not set', () => {
662+
beforeEach(() => {
663+
target = new ValidationPipe({});
664+
});
665+
it('should throw an exception with flatten errors', async () => {
666+
const testObj = { prop1: 'value1' };
667+
try {
668+
await target.transform(testObj, metadata);
669+
} catch (err) {
670+
const message = err.getResponse().message
671+
expect(err).to.be.instanceOf(BadRequestException);
672+
expect(message).to.be.an('array');
673+
message.forEach((error: any) => {
674+
expect(error).to.be.a('string');
675+
});
676+
}
677+
});
678+
});
679+
});
680+
681+
describe('option: "flatExceptionFactoryMessage"', () => {
682+
describe('when flatExceptionFactoryMessage is true', () => {
683+
beforeEach(() => {
684+
target = new ValidationPipe({
685+
flatExceptionFactoryMessage: true,
686+
exceptionFactory: errors => new CustomTestError(errors),
687+
});
688+
});
689+
it('should throw a CustomTestError with flatten errors', async () => {
690+
const testObj = { prop1: 'value1' };
691+
try {
692+
await target.transform(testObj, metadata);
693+
} catch (err) {
694+
expect(err).to.be.instanceOf(CustomTestError);
695+
expect(err.getResponse()).to.be.an('array');
696+
err.getResponse().forEach((error: any) => {
697+
expect(error).to.be.a('string');
698+
});
699+
}
700+
});
701+
});
702+
703+
describe('when flatExceptionFactoryMessage is false', () => {
704+
beforeEach(() => {
705+
target = new ValidationPipe({
706+
flatExceptionFactoryMessage: false,
707+
exceptionFactory: errors => new CustomTestError(errors),
708+
});
709+
});
710+
it('should throw a CustomTestError without flatten errors', async () => {
711+
const testObj = { prop1: 'value1' };
712+
try {
713+
await target.transform(testObj, metadata);
714+
} catch (err) {
715+
expect(err).to.be.instanceOf(CustomTestError);
716+
expect(err.getResponse()).to.be.an('array');
717+
err.getResponse().forEach((error: any) => {
718+
expect(error).to.be.instanceOf(ValidationError);
719+
});
720+
}
721+
});
722+
});
723+
724+
describe('when flatExceptionFactoryMessage is not set', () => {
725+
beforeEach(() => {
726+
target = new ValidationPipe({
727+
exceptionFactory: errors => new CustomTestError(errors),
728+
});
729+
});
730+
it('should throw a CustomTestError without flatten errors', async () => {
731+
const testObj = { prop1: 'value1' };
732+
try {
733+
await target.transform(testObj, metadata);
734+
} catch (err) {
735+
expect(err).to.be.instanceOf(CustomTestError);
736+
expect(err.getResponse()).to.be.an('array');
737+
err.getResponse().forEach((error: any) => {
738+
expect(error).to.be.instanceOf(ValidationError);
739+
});
740+
}
741+
});
742+
});
743+
744+
describe('when flatExceptionFactoryMessage is false without exceptionFactory', () => {
745+
beforeEach(() => {
746+
target = new ValidationPipe({
747+
flatExceptionFactoryMessage: false,
748+
});
749+
});
750+
it('should throw an exception with flatten errors', async () => {
751+
const testObj = { prop1: 'value1' };
752+
try {
753+
await target.transform(testObj, metadata);
754+
} catch (err) {
755+
const message = err.getResponse().message
756+
expect(err).to.be.instanceOf(BadRequestException);
757+
expect(message).to.be.an('array');
758+
message.forEach((error: any) => {
759+
expect(error).to.be.a('string');
760+
});
761+
}
762+
});
763+
});
764+
765+
describe('when flatExceptionFactoryMessage is true without exceptionFactory', () => {
766+
beforeEach(() => {
767+
target = new ValidationPipe({
768+
flatExceptionFactoryMessage: true,
769+
});
770+
});
771+
it('should throw an exception with flatten errors', async () => {
772+
const testObj = { prop1: 'value1' };
773+
try {
774+
await target.transform(testObj, metadata);
775+
} catch (err) {
776+
const message = err.getResponse().message
777+
expect(err).to.be.instanceOf(BadRequestException);
778+
expect(message).to.be.an('array');
779+
message.forEach((error: any) => {
780+
expect(error).to.be.a('string');
781+
});
782+
}
783+
});
784+
});
785+
});
588786
});

0 commit comments

Comments
 (0)