diff --git a/README.md b/README.md index 886712dd76..55108135fe 100644 --- a/README.md +++ b/README.md @@ -573,6 +573,50 @@ validate(user, { There is also a special flag `always: true` in validation options that you can use. This flag says that this validation must be applied always no matter which group is used. +## Validation option validateIf + +If you want an individual validaton decorator to apply conditionally, you can you can use the option `validateIf` available to all validators. +This allows more granular control than the `@ValidateIf` decorator which toggles all validators on the property, but keep in mind that +with great power comes great responsibility: Take care not to create unnecessarily complex validation logic. + +```typescript +class MyClass { + @Min(5, { + message: 'min', + validateIf: (obj: MyClass, value) => { + return !obj.someOtherProperty || obj.someOtherProperty === 'min'; + }, + }) + @Max(3, { + message: 'max', + validateIf: (o: MyClass) => !o.someOtherProperty || o.someOtherProperty === 'max', + }) + someProperty: number; + + someOtherProperty: string; +} + +const model = new MyClass(); +model.someProperty = 4; +model.someOtherProperty = 'min'; +validator.validate(model); // this only validate min + +const model = new MyClass(); +model.someProperty = 4; +model.someOtherProperty = 'max'; +validator.validate(model); // this only validate max + +const model = new MyClass(); +model.someProperty = 4; +model.someOtherProperty = ''; +validator.validate(model); // this validate both + +const model = new MyClass(); +model.someProperty = 4; +model.someOtherProperty = 'other'; +validator.validate(model); // this validate none +``` + ## Custom validation classes If you have custom validation logic you can create a _Constraint class_: diff --git a/src/decorator/ValidationOptions.ts b/src/decorator/ValidationOptions.ts index 60059a5fa6..4803afdc20 100644 --- a/src/decorator/ValidationOptions.ts +++ b/src/decorator/ValidationOptions.ts @@ -29,11 +29,18 @@ export interface ValidationOptions { * A transient set of data passed through to the validation result for response mapping */ context?: any; + + /** + * validation will be performed while the result is true + */ + validateIf?: (object: any, value: any) => boolean; } export function isValidationOptions(val: any): val is ValidationOptions { if (!val) { return false; } - return 'each' in val || 'message' in val || 'groups' in val || 'always' in val || 'context' in val; + return ( + 'each' in val || 'message' in val || 'groups' in val || 'always' in val || 'context' in val || 'validateIf' in val + ); } diff --git a/src/metadata/ValidationMetadata.ts b/src/metadata/ValidationMetadata.ts index c1b1acce82..93d590759e 100644 --- a/src/metadata/ValidationMetadata.ts +++ b/src/metadata/ValidationMetadata.ts @@ -64,6 +64,11 @@ export class ValidationMetadata { */ context?: any = undefined; + /** + * validation will be performed while the result is true + */ + validateIf?: (object: any, value: any) => boolean; + /** * Extra options specific to validation type. */ @@ -87,6 +92,7 @@ export class ValidationMetadata { this.always = args.validationOptions.always; this.each = args.validationOptions.each; this.context = args.validationOptions.context; + this.validateIf = args.validationOptions.validateIf; } } } diff --git a/src/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts index 98d09fce27..a8191d32e5 100644 --- a/src/validation/ValidationExecutor.ts +++ b/src/validation/ValidationExecutor.ts @@ -250,6 +250,20 @@ export class ValidationExecutor { private customValidations(object: object, value: any, metadatas: ValidationMetadata[], error: ValidationError): void { metadatas.forEach(metadata => { + const getValidationArguments = () => { + const validationArguments: ValidationArguments = { + targetName: object.constructor ? (object.constructor as any).name : undefined, + property: metadata.propertyName, + object: object, + value: value, + constraints: metadata.constraints, + }; + return validationArguments; + }; + if (metadata.validateIf) { + const validateIf = metadata.validateIf(object, value); + if (!validateIf) return; + } this.metadataStorage.getTargetValidatorConstraints(metadata.constraintCls).forEach(customConstraintMetadata => { if (customConstraintMetadata.async && this.ignoreAsyncValidations) return; if ( @@ -259,13 +273,7 @@ export class ValidationExecutor { ) return; - const validationArguments: ValidationArguments = { - targetName: object.constructor ? (object.constructor as any).name : undefined, - property: metadata.propertyName, - object: object, - value: value, - constraints: metadata.constraints, - }; + const validationArguments = getValidationArguments(); if (!metadata.each || !(Array.isArray(value) || value instanceof Set || value instanceof Map)) { const validatedValue = customConstraintMetadata.instance.validate(value, validationArguments); diff --git a/test/functional/validation-options.spec.ts b/test/functional/validation-options.spec.ts index 515325a652..6ec4feff9c 100644 --- a/test/functional/validation-options.spec.ts +++ b/test/functional/validation-options.spec.ts @@ -10,8 +10,7 @@ import { ValidateNested, ValidatorConstraint, IsOptional, - IsNotEmpty, - Allow, + Min, } from '../../src/decorator/decorators'; import { Validator } from '../../src/validation/Validator'; import { @@ -20,6 +19,7 @@ import { ValidationError, ValidationOptions, ValidatorConstraintInterface, + isValidationOptions, } from '../../src'; const validator = new Validator(); @@ -1285,3 +1285,70 @@ describe('context', () => { return Promise.all([hasStopAtFirstError, hasNotStopAtFirstError]); }); }); + +describe('validateIf', () => { + class MyClass { + @Min(5, { + message: 'min', + validateIf: (obj: MyClass, value) => { + return !obj.someOtherProperty || obj.someOtherProperty === 'min'; + }, + }) + @Max(3, { + message: 'max', + validateIf: (o: MyClass) => !o.someOtherProperty || o.someOtherProperty === 'max', + }) + someProperty: number; + + someOtherProperty: string; + } + + describe('should validate if validateIf return true.', () => { + it('should be true', () => { + const result = isValidationOptions({ + validateIf: (obj: MyClass, value) => { + return obj.someOtherProperty; + }, + }); + expect(result).toEqual(true); + }); + + it('should only validate min', () => { + const model = new MyClass(); + model.someProperty = 4; + model.someOtherProperty = 'min'; + return validator.validate(model).then(errors => { + expect(errors.length).toEqual(1); + expect(errors[0].constraints['min']).toBe('min'); + expect(errors[0].constraints['max']).toBe(undefined); + }); + }); + it('should only validate max', () => { + const model = new MyClass(); + model.someProperty = 4; + model.someOtherProperty = 'max'; + return validator.validate(model).then(errors => { + expect(errors.length).toEqual(1); + expect(errors[0].constraints['min']).toBe(undefined); + expect(errors[0].constraints['max']).toBe('max'); + }); + }); + it('should validate both', () => { + const model = new MyClass(); + model.someProperty = 4; + return validator.validate(model).then(errors => { + expect(errors.length).toEqual(1); + expect(errors[0].constraints['min']).toBe('min'); + expect(errors[0].constraints['max']).toBe('max'); + }); + }); + it('should validate none', () => { + const model = new MyClass(); + model.someProperty = 4; + model.someOtherProperty = 'other'; + return validator.validate(model).then(errors => { + expect(errors.length).toEqual(0); + }); + }); + }); +}); diff --git a/tsconfig.spec.json b/tsconfig.spec.json index c0215c96a0..81846fc097 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,7 +3,7 @@ "compilerOptions": { "strict": false, "strictPropertyInitialization": false, - "sourceMap": false, + "sourceMap": true, "removeComments": true, "noImplicitAny": false, },