Skip to content

Commit e3c3fb9

Browse files
aoi-umiNoNameProvided
authored andcommitted
feat: validateIf for validation options
1 parent 1f4a89c commit e3c3fb9

File tree

5 files changed

+137
-10
lines changed

5 files changed

+137
-10
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,51 @@ 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 validateIf
577+
If you want to validate that condition by object, you can use validation validateIf.
578+
579+
```typescript
580+
class MyClass {
581+
@Min(5, {
582+
message: 'min',
583+
validateIf: (value, args) => {
584+
const obj = args.object as MyClass;
585+
return !obj.someOtherProperty || obj.someOtherProperty === 'min';
586+
}
587+
})
588+
@Max(3, {
589+
message: 'max',
590+
validateIf: (value, args) => {
591+
const obj = args.object as MyClass;
592+
return !obj.someOtherProperty || obj.someOtherProperty === 'max';
593+
}
594+
})
595+
someProperty: number;
596+
597+
someOtherProperty: string;
598+
}
599+
600+
const model = new MyClass();
601+
model.someProperty = 4
602+
model.someOtherProperty = 'min';
603+
validator.validate(model) // this only validate min
604+
605+
const model = new MyClass();
606+
model.someProperty = 4
607+
model.someOtherProperty = 'max';
608+
validator.validate(model) // this only validate max
609+
610+
const model = new MyClass();
611+
model.someProperty = 4
612+
model.someOtherProperty = '';
613+
validator.validate(model) // this validate both
614+
615+
const model = new MyClass();
616+
model.someProperty = 4
617+
model.someOtherProperty = 'other';
618+
validator.validate(model) // this validate none
619+
620+
```
576621
## Custom validation classes
577622

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

src/decorator/ValidationOptions.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ 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?: (value: any, validationArguments: ValidationArguments) => 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 'each' in val || 'message' in val || 'groups' in val || 'always' in val || 'context' in val || 'validateIf' in val;
3944
}

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?: (value: any, validationArguments: ValidationArguments) => 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 validateIf = metadata.validateIf(object, getValidationArguments());
265+
if (!validateIf) 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: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
ValidateNested,
99
ValidatorConstraint,
1010
IsOptional,
11-
IsNotEmpty,
12-
Allow,
11+
Min,
12+
Max,
1313
} from '../../src/decorator/decorators';
1414
import { Validator } from '../../src/validation/Validator';
1515
import {
@@ -1251,3 +1251,66 @@ describe('context', () => {
12511251
return Promise.all([hasStopAtFirstError, hasNotStopAtFirstError]);
12521252
});
12531253
});
1254+
1255+
1256+
describe('validateIf', () => {
1257+
class MyClass {
1258+
@Min(5, {
1259+
message: 'min',
1260+
validateIf: (value, args) => {
1261+
const obj = args.object as MyClass;
1262+
return !obj.someOtherProperty || obj.someOtherProperty === 'min';
1263+
}
1264+
})
1265+
@Max(3, {
1266+
message: 'max',
1267+
validateIf: (value, args) => {
1268+
const obj = args.object as MyClass;
1269+
return !obj.someOtherProperty || obj.someOtherProperty === 'max';
1270+
}
1271+
})
1272+
someProperty: number;
1273+
1274+
someOtherProperty: string;
1275+
}
1276+
1277+
describe('should validate if validateIf return true.', () => {
1278+
it('should only validate min', () => {
1279+
const model = new MyClass();
1280+
model.someProperty = 4
1281+
model.someOtherProperty = 'min';
1282+
return validator.validate(model).then(errors => {
1283+
expect(errors.length).toEqual(1);
1284+
expect(errors[0].constraints['min']).toBe('min');
1285+
expect(errors[0].constraints['max']).toBe(undefined);
1286+
});
1287+
})
1288+
it('should only validate max', () => {
1289+
const model = new MyClass();
1290+
model.someProperty = 4
1291+
model.someOtherProperty = 'max';
1292+
return validator.validate(model).then(errors => {
1293+
expect(errors.length).toEqual(1);
1294+
expect(errors[0].constraints['min']).toBe(undefined);
1295+
expect(errors[0].constraints['max']).toBe('max');
1296+
});
1297+
})
1298+
it('should validate both', () => {
1299+
const model = new MyClass();
1300+
model.someProperty = 4
1301+
return validator.validate(model).then(errors => {
1302+
expect(errors.length).toEqual(1);
1303+
expect(errors[0].constraints['min']).toBe('min');
1304+
expect(errors[0].constraints['max']).toBe('max');
1305+
});
1306+
})
1307+
it('should validate none', () => {
1308+
const model = new MyClass();
1309+
model.someProperty = 4
1310+
model.someOtherProperty = 'other';
1311+
return validator.validate(model).then(errors => {
1312+
expect(errors.length).toEqual(0);
1313+
});
1314+
})
1315+
});
1316+
})

0 commit comments

Comments
 (0)