Skip to content

Commit 042fb57

Browse files
aoi-umiNoNameProvidedbraaar
authored
feat: add validateIf to validation options (#1579)
* feat: `validateIf` for validation options * refactor: format code with Prettier * update * update * Update README.md * fix: format readme * set spec tsconfig sourceMap=true * add test case for isValidationOptions * run lint:fix prettier:fix * Rename variable validateIf to shouldValidate --------- Co-authored-by: Attila Oláh <NoNameProvided@users.noreply.github.com> Co-authored-by: Brage Sekse Aarset <brage.aarset@gmail.com> Co-authored-by: Brage Sekse Aarset <brage@bjerk.io>
1 parent 4fcad7b commit 042fb57

6 files changed

Lines changed: 143 additions & 11 deletions

File tree

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,50 @@ validate(user, {
573573
There is also a special flag `always: true` in validation options that you can use. This flag says that this validation
574574
must be applied always no matter which group is used.
575575

576+
## Validation option validateIf
577+
578+
If you want an individual validaton decorator to apply conditionally, you can you can use the option `validateIf` available to all validators.
579+
This allows more granular control than the `@ValidateIf` decorator which toggles all validators on the property, but keep in mind that
580+
with great power comes great responsibility: Take care not to create unnecessarily complex validation logic.
581+
582+
```typescript
583+
class MyClass {
584+
@Min(5, {
585+
message: 'min',
586+
validateIf: (obj: MyClass, value) => {
587+
return !obj.someOtherProperty || obj.someOtherProperty === 'min';
588+
},
589+
})
590+
@Max(3, {
591+
message: 'max',
592+
validateIf: (o: MyClass) => !o.someOtherProperty || o.someOtherProperty === 'max',
593+
})
594+
someProperty: number;
595+
596+
someOtherProperty: string;
597+
}
598+
599+
const model = new MyClass();
600+
model.someProperty = 4;
601+
model.someOtherProperty = 'min';
602+
validator.validate(model); // this only validate min
603+
604+
const model = new MyClass();
605+
model.someProperty = 4;
606+
model.someOtherProperty = 'max';
607+
validator.validate(model); // this only validate max
608+
609+
const model = new MyClass();
610+
model.someProperty = 4;
611+
model.someOtherProperty = '';
612+
validator.validate(model); // this validate both
613+
614+
const model = new MyClass();
615+
model.someProperty = 4;
616+
model.someOtherProperty = 'other';
617+
validator.validate(model); // this validate none
618+
```
619+
576620
## Custom validation classes
577621

578622
If you have custom validation logic you can create a _Constraint class_:

src/decorator/ValidationOptions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,18 @@ export interface ValidationOptions {
2929
* A transient set of data passed through to the validation result for response mapping
3030
*/
3131
context?: any;
32+
33+
/**
34+
* validation will be performed while the result is true
35+
*/
36+
validateIf?: (object: any, value: any) => boolean;
3237
}
3338

3439
export function isValidationOptions(val: any): val is ValidationOptions {
3540
if (!val) {
3641
return false;
3742
}
38-
return 'each' in val || 'message' in val || 'groups' in val || 'always' in val || 'context' in val;
43+
return (
44+
'each' in val || 'message' in val || 'groups' in val || 'always' in val || 'context' in val || 'validateIf' in val
45+
);
3946
}

src/metadata/ValidationMetadata.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export class ValidationMetadata {
6464
*/
6565
context?: any = undefined;
6666

67+
/**
68+
* validation will be performed while the result is true
69+
*/
70+
validateIf?: (object: any, value: any) => boolean;
71+
6772
/**
6873
* Extra options specific to validation type.
6974
*/
@@ -87,6 +92,7 @@ export class ValidationMetadata {
8792
this.always = args.validationOptions.always;
8893
this.each = args.validationOptions.each;
8994
this.context = args.validationOptions.context;
95+
this.validateIf = args.validationOptions.validateIf;
9096
}
9197
}
9298
}

src/validation/ValidationExecutor.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ export class ValidationExecutor {
250250

251251
private customValidations(object: object, value: any, metadatas: ValidationMetadata[], error: ValidationError): void {
252252
metadatas.forEach(metadata => {
253+
const getValidationArguments = () => {
254+
const validationArguments: ValidationArguments = {
255+
targetName: object.constructor ? (object.constructor as any).name : undefined,
256+
property: metadata.propertyName,
257+
object: object,
258+
value: value,
259+
constraints: metadata.constraints,
260+
};
261+
return validationArguments;
262+
};
263+
if (metadata.validateIf) {
264+
const shouldValidate = metadata.validateIf(object, value);
265+
if (!shouldValidate) return;
266+
}
253267
this.metadataStorage.getTargetValidatorConstraints(metadata.constraintCls).forEach(customConstraintMetadata => {
254268
if (customConstraintMetadata.async && this.ignoreAsyncValidations) return;
255269
if (
@@ -259,13 +273,7 @@ export class ValidationExecutor {
259273
)
260274
return;
261275

262-
const validationArguments: ValidationArguments = {
263-
targetName: object.constructor ? (object.constructor as any).name : undefined,
264-
property: metadata.propertyName,
265-
object: object,
266-
value: value,
267-
constraints: metadata.constraints,
268-
};
276+
const validationArguments = getValidationArguments();
269277

270278
if (!metadata.each || !(Array.isArray(value) || value instanceof Set || value instanceof Map)) {
271279
const validatedValue = customConstraintMetadata.instance.validate(value, validationArguments);

test/functional/validation-options.spec.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {
1010
ValidateNested,
1111
ValidatorConstraint,
1212
IsOptional,
13-
IsNotEmpty,
14-
Allow,
13+
Min,
1514
} from '../../src/decorator/decorators';
1615
import { Validator } from '../../src/validation/Validator';
1716
import {
@@ -20,6 +19,7 @@ import {
2019
ValidationError,
2120
ValidationOptions,
2221
ValidatorConstraintInterface,
22+
isValidationOptions,
2323
} from '../../src';
2424

2525
const validator = new Validator();
@@ -1285,3 +1285,70 @@ describe('context', () => {
12851285
return Promise.all([hasStopAtFirstError, hasNotStopAtFirstError]);
12861286
});
12871287
});
1288+
1289+
describe('validateIf', () => {
1290+
class MyClass {
1291+
@Min(5, {
1292+
message: 'min',
1293+
validateIf: (obj: MyClass, value) => {
1294+
return !obj.someOtherProperty || obj.someOtherProperty === 'min';
1295+
},
1296+
})
1297+
@Max(3, {
1298+
message: 'max',
1299+
validateIf: (o: MyClass) => !o.someOtherProperty || o.someOtherProperty === 'max',
1300+
})
1301+
someProperty: number;
1302+
1303+
someOtherProperty: string;
1304+
}
1305+
1306+
describe('should validate if validateIf return true.', () => {
1307+
it('should be true', () => {
1308+
const result = isValidationOptions({
1309+
validateIf: (obj: MyClass, value) => {
1310+
return obj.someOtherProperty;
1311+
},
1312+
});
1313+
expect(result).toEqual(true);
1314+
});
1315+
1316+
it('should only validate min', () => {
1317+
const model = new MyClass();
1318+
model.someProperty = 4;
1319+
model.someOtherProperty = 'min';
1320+
return validator.validate(model).then(errors => {
1321+
expect(errors.length).toEqual(1);
1322+
expect(errors[0].constraints['min']).toBe('min');
1323+
expect(errors[0].constraints['max']).toBe(undefined);
1324+
});
1325+
});
1326+
it('should only validate max', () => {
1327+
const model = new MyClass();
1328+
model.someProperty = 4;
1329+
model.someOtherProperty = 'max';
1330+
return validator.validate(model).then(errors => {
1331+
expect(errors.length).toEqual(1);
1332+
expect(errors[0].constraints['min']).toBe(undefined);
1333+
expect(errors[0].constraints['max']).toBe('max');
1334+
});
1335+
});
1336+
it('should validate both', () => {
1337+
const model = new MyClass();
1338+
model.someProperty = 4;
1339+
return validator.validate(model).then(errors => {
1340+
expect(errors.length).toEqual(1);
1341+
expect(errors[0].constraints['min']).toBe('min');
1342+
expect(errors[0].constraints['max']).toBe('max');
1343+
});
1344+
});
1345+
it('should validate none', () => {
1346+
const model = new MyClass();
1347+
model.someProperty = 4;
1348+
model.someOtherProperty = 'other';
1349+
return validator.validate(model).then(errors => {
1350+
expect(errors.length).toEqual(0);
1351+
});
1352+
});
1353+
});
1354+
});

tsconfig.spec.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"compilerOptions": {
44
"strict": false,
55
"strictPropertyInitialization": false,
6-
"sourceMap": false,
6+
"sourceMap": true,
77
"removeComments": true,
88
"noImplicitAny": false,
99
},

0 commit comments

Comments
 (0)