diff --git a/README.md b/README.md index 886712dd76..d31fd25204 100644 --- a/README.md +++ b/README.md @@ -799,6 +799,7 @@ isBoolean(value); | **Common validation decorators** | | | `@IsDefined(value: any)` | Checks if value is defined (!== undefined, !== null). This is the only decorator that ignores skipMissingProperties option. | | `@IsOptional()` | Checks if given value is empty (=== null, === undefined) and if so, ignores all the validators on the property. | +| `@IsStrictlyOptional()` | Checks if given value is missing (=== undefined) and if so, ignores all the validators on the property. | | `@Equals(comparison: any)` | Checks if value equals ("===") comparison. | | `@NotEquals(comparison: any)` | Checks if value not equal ("!==") comparison. | | `@IsEmpty()` | Checks if given value is empty (=== '', === null, === undefined). | diff --git a/src/decorator/common/IsStrictlyOptional.ts b/src/decorator/common/IsStrictlyOptional.ts new file mode 100644 index 0000000000..5e0318043d --- /dev/null +++ b/src/decorator/common/IsStrictlyOptional.ts @@ -0,0 +1,28 @@ +import { ValidationOptions } from '../ValidationOptions'; +import { ValidationMetadataArgs } from '../../metadata/ValidationMetadataArgs'; +import { ValidationTypes } from '../../validation/ValidationTypes'; +import { ValidationMetadata } from '../../metadata/ValidationMetadata'; +import { getMetadataStorage } from '../../metadata/MetadataStorage'; + +export const IS_STRICTLY_OPTIONAL = 'isStrictlyOptional'; + +/** + * Checks if value is missing (undefined) and if so, ignores all validators. + */ +export function IsStrictlyOptional(validationOptions?: ValidationOptions): PropertyDecorator { + return function (object: object, propertyName: string): void { + const args: ValidationMetadataArgs = { + type: ValidationTypes.CONDITIONAL_VALIDATION, + name: IS_STRICTLY_OPTIONAL, + target: object.constructor, + propertyName: propertyName, + constraints: [ + (object: any, value: any): boolean => { + return object[propertyName] !== undefined; + }, + ], + validationOptions: validationOptions, + }; + getMetadataStorage().addValidationMetadata(new ValidationMetadata(args)); + }; +} diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts index d449e9301a..4ad53ed43a 100644 --- a/src/decorator/decorators.ts +++ b/src/decorator/decorators.ts @@ -9,6 +9,7 @@ export * from './common/Allow'; export * from './common/IsDefined'; export * from './common/IsOptional'; +export * from './common/IsStrictlyOptional'; export * from './common/Validate'; export * from './common/ValidateBy'; export * from './common/ValidateIf'; diff --git a/test/functional/conditional-validation.spec.ts b/test/functional/conditional-validation.spec.ts index e633763799..00789fd495 100644 --- a/test/functional/conditional-validation.spec.ts +++ b/test/functional/conditional-validation.spec.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty, ValidateIf, IsOptional, Equals } from '../../src/decorator/decorators'; +import { IsNotEmpty, ValidateIf, IsOptional, IsStrictlyOptional, Equals } from '../../src/decorator/decorators'; import { Validator } from '../../src/validation/Validator'; const validator = new Validator(); @@ -92,4 +92,36 @@ describe('conditional validation', () => { expect(errors[0].value).toEqual('bad_value'); }); }); + + it('should validate a property when value is not missing', () => { + expect.assertions(5); + + class MyClass { + @IsStrictlyOptional() + @Equals('test') + title: string | null = null; + } + + const model = new MyClass(); + return validator.validate(model).then(errors => { + expect(errors.length).toEqual(1); + expect(errors[0].target).toEqual(model); + expect(errors[0].property).toEqual('title'); + expect(errors[0].constraints).toEqual({ equals: 'title must be equal to test' }); + expect(errors[0].value).toEqual(null); + }); + }); + + it('should not validate a property when value is missing', () => { + class MyClass { + @IsStrictlyOptional() + @Equals('test') + title?: string = undefined; + } + + const model = new MyClass(); + return validator.validate(model).then(errors => { + expect(errors.length).toEqual(0); + }); + }); });