From e3c3fb9e814907858b0b6027f5e28ccd3fcb411c Mon Sep 17 00:00:00 2001 From: my-pc <1641622365@qq.com> Date: Fri, 11 Mar 2022 21:37:33 +0800 Subject: [PATCH 1/9] feat: `validateIf` for validation options --- README.md | 45 +++++++++++++++ src/decorator/ValidationOptions.ts | 7 ++- src/metadata/ValidationMetadata.ts | 6 ++ src/validation/ValidationExecutor.ts | 22 ++++--- test/functional/validation-options.spec.ts | 67 +++++++++++++++++++++- 5 files changed, 137 insertions(+), 10 deletions(-) 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); + }); + }) + }); +}) From 3773da39b6a671925ad07eab3694766dea6c6b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Ol=C3=A1h?= Date: Sat, 3 Dec 2022 14:16:48 +0000 Subject: [PATCH 2/9] refactor: format code with Prettier --- README.md | 23 ++++++++++---------- src/decorator/ValidationOptions.ts | 4 +++- src/validation/ValidationExecutor.ts | 2 +- test/functional/validation-options.spec.ts | 25 +++++++++++----------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 5bdc0ee962..a17cf751e8 100644 --- a/README.md +++ b/README.md @@ -574,6 +574,7 @@ There is also a special flag `always: true` in validation options that you can u 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 @@ -583,14 +584,14 @@ class MyClass { 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; @@ -598,26 +599,26 @@ class MyClass { } const model = new MyClass(); -model.someProperty = 4 +model.someProperty = 4; model.someOtherProperty = 'min'; -validator.validate(model) // this only validate min +validator.validate(model); // this only validate min const model = new MyClass(); -model.someProperty = 4 +model.someProperty = 4; model.someOtherProperty = 'max'; -validator.validate(model) // this only validate max +validator.validate(model); // this only validate max const model = new MyClass(); -model.someProperty = 4 +model.someProperty = 4; model.someOtherProperty = ''; -validator.validate(model) // this validate both +validator.validate(model); // this validate both const model = new MyClass(); -model.someProperty = 4 +model.someProperty = 4; model.someOtherProperty = 'other'; -validator.validate(model) // this validate none - +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 d335d33ddb..7ad3ad061a 100644 --- a/src/decorator/ValidationOptions.ts +++ b/src/decorator/ValidationOptions.ts @@ -40,5 +40,7 @@ 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 || 'validateIf' 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/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts index 8f2ac37e0e..e8d898a7ff 100644 --- a/src/validation/ValidationExecutor.ts +++ b/src/validation/ValidationExecutor.ts @@ -259,7 +259,7 @@ export class ValidationExecutor { constraints: metadata.constraints, }; return validationArguments; - } + }; if (metadata.validateIf) { const validateIf = metadata.validateIf(object, getValidationArguments()); if (!validateIf) return; diff --git a/test/functional/validation-options.spec.ts b/test/functional/validation-options.spec.ts index 1e08b67885..7c21eeed70 100644 --- a/test/functional/validation-options.spec.ts +++ b/test/functional/validation-options.spec.ts @@ -1252,7 +1252,6 @@ describe('context', () => { }); }); - describe('validateIf', () => { class MyClass { @Min(5, { @@ -1260,57 +1259,57 @@ describe('validateIf', () => { 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.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.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 + 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.someProperty = 4; model.someOtherProperty = 'other'; return validator.validate(model).then(errors => { expect(errors.length).toEqual(0); }); - }) + }); }); -}) +}); From 7d039a4e340118d3fda954b528f0fddbb5497f63 Mon Sep 17 00:00:00 2001 From: umi - desktop <1641622365@qq.com> Date: Mon, 15 Jan 2024 21:33:51 +0800 Subject: [PATCH 3/9] update --- README.md | 8 ++------ src/metadata/ValidationMetadata.ts | 2 +- src/validation/ValidationExecutor.ts | 2 +- test/functional/validation-options.spec.ts | 9 ++------- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7f93be8ef3..bb59410307 100644 --- a/README.md +++ b/README.md @@ -581,17 +581,13 @@ If you want to validate that condition by object, you can use validation validat class MyClass { @Min(5, { message: 'min', - validateIf: (value, args) => { - const obj = args.object as MyClass; + validateIf: (obj: MyClass, value) => { 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'; - }, + validateIf: (o: MyClass) => !o.someOtherProperty || o.someOtherProperty === 'max', }) someProperty: number; diff --git a/src/metadata/ValidationMetadata.ts b/src/metadata/ValidationMetadata.ts index 0370864062..f20654d7d2 100644 --- a/src/metadata/ValidationMetadata.ts +++ b/src/metadata/ValidationMetadata.ts @@ -67,7 +67,7 @@ export class ValidationMetadata { /** * validation will be performed while the result is true */ - validateIf?: (value: any, validationArguments: ValidationArguments) => boolean; + validateIf?: (object: object, value: any) => boolean; /** * Extra options specific to validation type. diff --git a/src/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts index 37189d6bf6..a8191d32e5 100644 --- a/src/validation/ValidationExecutor.ts +++ b/src/validation/ValidationExecutor.ts @@ -261,7 +261,7 @@ export class ValidationExecutor { return validationArguments; }; if (metadata.validateIf) { - const validateIf = metadata.validateIf(object, getValidationArguments()); + const validateIf = metadata.validateIf(object, value); if (!validateIf) return; } this.metadataStorage.getTargetValidatorConstraints(metadata.constraintCls).forEach(customConstraintMetadata => { diff --git a/test/functional/validation-options.spec.ts b/test/functional/validation-options.spec.ts index cfd47f7807..401bb8d8db 100644 --- a/test/functional/validation-options.spec.ts +++ b/test/functional/validation-options.spec.ts @@ -11,7 +11,6 @@ import { ValidatorConstraint, IsOptional, Min, - Max, } from '../../src/decorator/decorators'; import { Validator } from '../../src/validation/Validator'; import { @@ -1290,17 +1289,13 @@ describe('validateIf', () => { class MyClass { @Min(5, { message: 'min', - validateIf: (value, args) => { - const obj = args.object as MyClass; + validateIf: (obj: MyClass, value) => { 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'; - }, + validateIf: (o: MyClass) => !o.someOtherProperty || o.someOtherProperty === 'max', }) someProperty: number; From f945dd57aebc7857a5ed8b429011775a24fdd601 Mon Sep 17 00:00:00 2001 From: umi - desktop <1641622365@qq.com> Date: Mon, 15 Jan 2024 21:38:27 +0800 Subject: [PATCH 4/9] update --- src/decorator/ValidationOptions.ts | 2 +- src/metadata/ValidationMetadata.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/decorator/ValidationOptions.ts b/src/decorator/ValidationOptions.ts index 7ad3ad061a..4803afdc20 100644 --- a/src/decorator/ValidationOptions.ts +++ b/src/decorator/ValidationOptions.ts @@ -33,7 +33,7 @@ export interface ValidationOptions { /** * validation will be performed while the result is true */ - validateIf?: (value: any, validationArguments: ValidationArguments) => boolean; + validateIf?: (object: any, value: any) => boolean; } export function isValidationOptions(val: any): val is ValidationOptions { diff --git a/src/metadata/ValidationMetadata.ts b/src/metadata/ValidationMetadata.ts index f20654d7d2..93d590759e 100644 --- a/src/metadata/ValidationMetadata.ts +++ b/src/metadata/ValidationMetadata.ts @@ -67,7 +67,7 @@ export class ValidationMetadata { /** * validation will be performed while the result is true */ - validateIf?: (object: object, value: any) => boolean; + validateIf?: (object: any, value: any) => boolean; /** * Extra options specific to validation type. From df9edf98381062cc90a1a58d5bed7beabc8e33b4 Mon Sep 17 00:00:00 2001 From: Brage Sekse Aarset Date: Mon, 20 Jan 2025 07:41:27 +0200 Subject: [PATCH 5/9] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb59410307..32c3b245a6 100644 --- a/README.md +++ b/README.md @@ -573,9 +573,11 @@ 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 +## Validation option validateIf -If you want to validate that condition by object, you can use validation 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 { From 4d6b995082d2c8fa371e63a73d89b18f95342a31 Mon Sep 17 00:00:00 2001 From: Brage Sekse Aarset Date: Mon, 20 Jan 2025 07:49:07 +0200 Subject: [PATCH 6/9] fix: format readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32c3b245a6..55108135fe 100644 --- a/README.md +++ b/README.md @@ -576,8 +576,8 @@ 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. +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 { From a054218f2816db23baaa89b75a328d0f2b5989e5 Mon Sep 17 00:00:00 2001 From: umi - desktop <1641622365@qq.com> Date: Wed, 22 Jan 2025 12:46:21 +0800 Subject: [PATCH 7/9] set spec tsconfig sourceMap=true --- tsconfig.spec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, }, From 598f83c2233605540c1cb02fe280c805e275cf49 Mon Sep 17 00:00:00 2001 From: umi - desktop <1641622365@qq.com> Date: Fri, 7 Feb 2025 20:52:56 +0800 Subject: [PATCH 8/9] add test case for isValidationOptions --- test/functional/validation-options.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/functional/validation-options.spec.ts b/test/functional/validation-options.spec.ts index 401bb8d8db..a9cfe081f5 100644 --- a/test/functional/validation-options.spec.ts +++ b/test/functional/validation-options.spec.ts @@ -19,6 +19,7 @@ import { ValidationError, ValidationOptions, ValidatorConstraintInterface, + isValidationOptions, } from '../../src'; const validator = new Validator(); @@ -1303,6 +1304,15 @@ describe('validateIf', () => { } 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; From 38d9f5d985856bb287d7473f02defd2b127a0aa7 Mon Sep 17 00:00:00 2001 From: umi - desktop <1641622365@qq.com> Date: Tue, 4 Mar 2025 16:33:21 +0800 Subject: [PATCH 9/9] run lint:fix prettier:fix --- test/functional/validation-options.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/validation-options.spec.ts b/test/functional/validation-options.spec.ts index a9cfe081f5..6ec4feff9c 100644 --- a/test/functional/validation-options.spec.ts +++ b/test/functional/validation-options.spec.ts @@ -1308,7 +1308,7 @@ describe('validateIf', () => { const result = isValidationOptions({ validateIf: (obj: MyClass, value) => { return obj.someOtherProperty; - } + }, }); expect(result).toEqual(true); });