Skip to content

Commit 0976795

Browse files
committed
feat(common): add new flatten options to validation pipe
1 parent 8f04235 commit 0976795

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 | undefined;
5256
protected errorHttpStatusCode: ErrorHttpStatusCode;
5357
protected expectedType: Type<any> | undefined;
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);
@@ -141,7 +151,11 @@ export class ValidationPipe implements PipeTransform<any> {
141151

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

167181
public createExceptionFactory() {
168-
return (validationErrors: ValidationError[] = []) => {
182+
return (validationErrors: ValidationError[] | string[] = []) => {
169183
if (this.isDetailedOutputDisabled) {
170184
return new HttpErrorByCode[this.errorHttpStatusCode]();
171185
}
172-
const errors = this.flattenValidationErrors(validationErrors);
173-
return new HttpErrorByCode[this.errorHttpStatusCode](errors);
186+
return new HttpErrorByCode[this.errorHttpStatusCode](validationErrors);
174187
};
175188
}
176189

@@ -317,4 +330,12 @@ export class ValidationPipe implements PipeTransform<any> {
317330
constraints,
318331
};
319332
}
333+
334+
protected shouldFlatErrors(): boolean {
335+
if (this.hasExceptionFactory) {
336+
return !!this.isFlattenExceptionFactoryErrorsEnabled;
337+
}
338+
339+
return !this.isFlattenErrorMessagesDisabled;
340+
}
320341
}

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() {}
@@ -609,4 +620,191 @@ describe('ValidationPipe', () => {
609620
expect(await target.transform(testObj, m)).to.deep.equal(testObj);
610621
});
611622
});
623+
624+
describe('option: "exceptionFactory"', () => {
625+
describe('when validation fails', () => {
626+
beforeEach(() => {
627+
target = new ValidationPipe({
628+
exceptionFactory: errors => new CustomTestError(errors),
629+
});
630+
});
631+
it('should throw a CustomTestError exception', async () => {
632+
const testObj = { prop1: 'value1' };
633+
try {
634+
await target.transform(testObj, metadata);
635+
} catch (err) {
636+
expect(err).to.be.instanceOf(CustomTestError);
637+
}
638+
});
639+
});
640+
});
641+
642+
describe('option: "disableFlattenErrorMessages"', () => {
643+
describe('when disableFlattenErrorMessages is true', () => {
644+
beforeEach(() => {
645+
target = new ValidationPipe({
646+
disableFlattenErrorMessages: true,
647+
});
648+
});
649+
it('should throw an exception without flatten errors', async () => {
650+
const testObj = { prop1: 'value1' };
651+
try {
652+
await target.transform(testObj, metadata);
653+
} catch (err) {
654+
const message = err.getResponse().message;
655+
expect(err).to.be.instanceOf(BadRequestException);
656+
expect(message).to.be.an('array');
657+
message.forEach((error: any) => {
658+
expect(error).to.be.instanceOf(ValidationError);
659+
});
660+
}
661+
});
662+
});
663+
664+
describe('when disableFlattenErrorMessages is false', () => {
665+
beforeEach(() => {
666+
target = new ValidationPipe({
667+
disableFlattenErrorMessages: false,
668+
});
669+
});
670+
it('should throw an exception with flatten errors', async () => {
671+
const testObj = { prop1: 'value1' };
672+
try {
673+
await target.transform(testObj, metadata);
674+
} catch (err) {
675+
const message = err.getResponse().message;
676+
expect(err).to.be.instanceOf(BadRequestException);
677+
expect(message).to.be.an('array');
678+
message.forEach((error: any) => {
679+
expect(error).to.be.a('string');
680+
});
681+
}
682+
});
683+
});
684+
685+
describe('when disableFlattenErrorMessages is not set', () => {
686+
beforeEach(() => {
687+
target = new ValidationPipe({});
688+
});
689+
it('should throw an exception with flatten errors', async () => {
690+
const testObj = { prop1: 'value1' };
691+
try {
692+
await target.transform(testObj, metadata);
693+
} catch (err) {
694+
const message = err.getResponse().message;
695+
expect(err).to.be.instanceOf(BadRequestException);
696+
expect(message).to.be.an('array');
697+
message.forEach((error: any) => {
698+
expect(error).to.be.a('string');
699+
});
700+
}
701+
});
702+
});
703+
});
704+
705+
describe('option: "flatExceptionFactoryMessage"', () => {
706+
describe('when flatExceptionFactoryMessage is true', () => {
707+
beforeEach(() => {
708+
target = new ValidationPipe({
709+
flatExceptionFactoryMessage: true,
710+
exceptionFactory: errors => new CustomTestError(errors),
711+
});
712+
});
713+
it('should throw a CustomTestError with flatten errors', async () => {
714+
const testObj = { prop1: 'value1' };
715+
try {
716+
await target.transform(testObj, metadata);
717+
} catch (err) {
718+
expect(err).to.be.instanceOf(CustomTestError);
719+
expect(err.getResponse()).to.be.an('array');
720+
err.getResponse().forEach((error: any) => {
721+
expect(error).to.be.a('string');
722+
});
723+
}
724+
});
725+
});
726+
727+
describe('when flatExceptionFactoryMessage is false', () => {
728+
beforeEach(() => {
729+
target = new ValidationPipe({
730+
flatExceptionFactoryMessage: false,
731+
exceptionFactory: errors => new CustomTestError(errors),
732+
});
733+
});
734+
it('should throw a CustomTestError without flatten errors', async () => {
735+
const testObj = { prop1: 'value1' };
736+
try {
737+
await target.transform(testObj, metadata);
738+
} catch (err) {
739+
expect(err).to.be.instanceOf(CustomTestError);
740+
expect(err.getResponse()).to.be.an('array');
741+
err.getResponse().forEach((error: any) => {
742+
expect(error).to.be.instanceOf(ValidationError);
743+
});
744+
}
745+
});
746+
});
747+
748+
describe('when flatExceptionFactoryMessage is not set', () => {
749+
beforeEach(() => {
750+
target = new ValidationPipe({
751+
exceptionFactory: errors => new CustomTestError(errors),
752+
});
753+
});
754+
it('should throw a CustomTestError without flatten errors', async () => {
755+
const testObj = { prop1: 'value1' };
756+
try {
757+
await target.transform(testObj, metadata);
758+
} catch (err) {
759+
expect(err).to.be.instanceOf(CustomTestError);
760+
expect(err.getResponse()).to.be.an('array');
761+
err.getResponse().forEach((error: any) => {
762+
expect(error).to.be.instanceOf(ValidationError);
763+
});
764+
}
765+
});
766+
});
767+
768+
describe('when flatExceptionFactoryMessage is false without exceptionFactory', () => {
769+
beforeEach(() => {
770+
target = new ValidationPipe({
771+
flatExceptionFactoryMessage: false,
772+
});
773+
});
774+
it('should throw an exception with flatten errors', async () => {
775+
const testObj = { prop1: 'value1' };
776+
try {
777+
await target.transform(testObj, metadata);
778+
} catch (err) {
779+
const message = err.getResponse().message;
780+
expect(err).to.be.instanceOf(BadRequestException);
781+
expect(message).to.be.an('array');
782+
message.forEach((error: any) => {
783+
expect(error).to.be.a('string');
784+
});
785+
}
786+
});
787+
});
788+
789+
describe('when flatExceptionFactoryMessage is true without exceptionFactory', () => {
790+
beforeEach(() => {
791+
target = new ValidationPipe({
792+
flatExceptionFactoryMessage: true,
793+
});
794+
});
795+
it('should throw an exception with flatten errors', async () => {
796+
const testObj = { prop1: 'value1' };
797+
try {
798+
await target.transform(testObj, metadata);
799+
} catch (err) {
800+
const message = err.getResponse().message;
801+
expect(err).to.be.instanceOf(BadRequestException);
802+
expect(message).to.be.an('array');
803+
message.forEach((error: any) => {
804+
expect(error).to.be.a('string');
805+
});
806+
}
807+
});
808+
});
809+
});
612810
});

0 commit comments

Comments
 (0)