Skip to content

Commit

Permalink
feat: validateIf for validation options
Browse files Browse the repository at this point in the history
  • Loading branch information
aoi-umi authored and NoNameProvided committed Dec 3, 2022
1 parent 1f4a89c commit e3c3fb9
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 10 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_:
Expand Down
7 changes: 6 additions & 1 deletion src/decorator/ValidationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
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?: (value: any, validationArguments: ValidationArguments) => 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, getValidationArguments());
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
67 changes: 65 additions & 2 deletions test/functional/validation-options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
ValidateNested,
ValidatorConstraint,
IsOptional,
IsNotEmpty,
Allow,
Min,
Max,
} from '../../src/decorator/decorators';
import { Validator } from '../../src/validation/Validator';
import {
Expand Down Expand Up @@ -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);
});
})
});
})

0 comments on commit e3c3fb9

Please sign in to comment.