diff --git a/README.md b/README.md index 458a5e2de4..5bdc0ee962 100644 --- a/README.md +++ b/README.md @@ -573,6 +573,51 @@ 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 validateIf +If you want to validate that condition by object, you can use validation validateIf. + +```typescript +class MyClass { + @Min(5, { + message: 'min', + validateIf: (value, args) => { + const obj = args.object as MyClass; + return !obj.someOtherProperty || obj.someOtherProperty === 'min'; + } + }) + @Max(3, { + message: 'max', + validateIf: (value, args) => { + const obj = args.object as MyClass; + return !obj.someOtherProperty || obj.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..d335d33ddb 100644 --- a/src/decorator/ValidationOptions.ts +++ b/src/decorator/ValidationOptions.ts @@ -29,11 +29,16 @@ 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?: (value: any, validationArguments: ValidationArguments) => 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..0370864062 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?: (value: any, validationArguments: ValidationArguments) => 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 9d3d312f14..8f2ac37e0e 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, getValidationArguments()); + 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 99db80aa76..1e08b67885 100644 --- a/test/functional/validation-options.spec.ts +++ b/test/functional/validation-options.spec.ts @@ -8,8 +8,8 @@ import { ValidateNested, ValidatorConstraint, IsOptional, - IsNotEmpty, - Allow, + Min, + Max, } from '../../src/decorator/decorators'; import { Validator } from '../../src/validation/Validator'; import { @@ -1251,3 +1251,66 @@ describe('context', () => { return Promise.all([hasStopAtFirstError, hasNotStopAtFirstError]); }); }); + + +describe('validateIf', () => { + class MyClass { + @Min(5, { + message: 'min', + validateIf: (value, args) => { + const obj = args.object as MyClass; + return !obj.someOtherProperty || obj.someOtherProperty === 'min'; + } + }) + @Max(3, { + message: 'max', + validateIf: (value, args) => { + const obj = args.object as MyClass; + return !obj.someOtherProperty || obj.someOtherProperty === 'max'; + } + }) + someProperty: number; + + someOtherProperty: string; + } + + describe('should validate if validateIf return 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); + }); + }) + }); +})