Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validateIf for validation options #1579

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,48 @@ 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: (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_:
Expand Down
9 changes: 8 additions & 1 deletion src/decorator/ValidationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
6 changes: 6 additions & 0 deletions src/metadata/ValidationMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;
}
}
}
22 changes: 15 additions & 7 deletions src/validation/ValidationExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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);
Expand Down
61 changes: 59 additions & 2 deletions test/functional/validation-options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
ValidateNested,
ValidatorConstraint,
IsOptional,
IsNotEmpty,
Allow,
Min,
} from '../../src/decorator/decorators';
import { Validator } from '../../src/validation/Validator';
import {
Expand Down Expand Up @@ -1285,3 +1284,61 @@ 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 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);
});
});
});
});
Loading