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 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,50 @@ 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 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.

```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 @@

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;

Check warning on line 265 in src/validation/ValidationExecutor.ts

View check run for this annotation

Codecov / codecov/patch

src/validation/ValidationExecutor.ts#L265

Added line #L265 was not covered by tests
}
this.metadataStorage.getTargetValidatorConstraints(metadata.constraintCls).forEach(customConstraintMetadata => {
if (customConstraintMetadata.async && this.ignoreAsyncValidations) return;
if (
Expand All @@ -259,13 +273,7 @@
)
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