Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): add new flatten options to validation pipe #14359

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions packages/common/pipes/validation.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
validatorPackage?: ValidatorPackage;
Expand All @@ -47,18 +49,23 @@ let classTransformer: TransformerPackage = {} as any;
export class ValidationPipe implements PipeTransform<any> {
protected isTransformEnabled: boolean;
protected isDetailedOutputDisabled?: boolean;
protected isFlattenErrorMessagesDisabled?: boolean;
protected isFlattenExceptionFactoryErrorsEnabled?: boolean;
protected validatorOptions: ValidatorOptions;
protected transformOptions: ClassTransformOptions | undefined;
protected errorHttpStatusCode: ErrorHttpStatusCode;
protected expectedType: Type<any> | undefined;
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,
Expand All @@ -72,11 +79,14 @@ export class ValidationPipe implements PipeTransform<any> {
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);
Expand Down Expand Up @@ -141,7 +151,11 @@ export class ValidationPipe implements PipeTransform<any> {

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
Expand All @@ -165,12 +179,11 @@ export class ValidationPipe implements PipeTransform<any> {
}

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);
};
}

Expand Down Expand Up @@ -317,4 +330,12 @@ export class ValidationPipe implements PipeTransform<any> {
constraints,
};
}

protected shouldFlatErrors(): boolean {
if (this.hasExceptionFactory) {
return !!this.isFlattenExceptionFactoryErrorsEnabled;
}

return !this.isFlattenErrorMessagesDisabled;
}
}
200 changes: 199 additions & 1 deletion packages/common/test/pipes/validation.pipe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand Down Expand Up @@ -609,4 +620,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');
});
}
});
});
});
});