From c13a935a2eb74ebcdfa07c5d181cf16910f0fac3 Mon Sep 17 00:00:00 2001 From: ehenon Date: Thu, 18 Sep 2025 15:50:35 +0200 Subject: [PATCH 1/3] feat: add @IsActuallyOptional() decorator --- README.md | 1 + src/decorator/common/IsActuallyOptional.ts | 28 +++++++++++++++ src/decorator/decorators.ts | 1 + .../functional/conditional-validation.spec.ts | 34 ++++++++++++++++++- 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/decorator/common/IsActuallyOptional.ts diff --git a/README.md b/README.md index 886712dd76..da69c467ff 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. | +| `@IsActuallyOptional()` | 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/IsActuallyOptional.ts b/src/decorator/common/IsActuallyOptional.ts new file mode 100644 index 0000000000..3e714a001b --- /dev/null +++ b/src/decorator/common/IsActuallyOptional.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_ACTUALLY_OPTIONAL = 'isActuallyOptional'; + +/** + * Checks if value is missing (undefined) and if so, ignores all validators. + */ +export function IsActuallyOptional(validationOptions?: ValidationOptions): PropertyDecorator { + return function (object: object, propertyName: string): void { + const args: ValidationMetadataArgs = { + type: ValidationTypes.CONDITIONAL_VALIDATION, + name: IS_ACTUALLY_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..fe8bb37b1b 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/IsActuallyOptional'; 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..4df84bfee2 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, IsActuallyOptional, 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 { + @IsActuallyOptional() + @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 { + @IsActuallyOptional() + @Equals('test') + title?: string = undefined; + } + + const model = new MyClass(); + return validator.validate(model).then(errors => { + expect(errors.length).toEqual(0); + }); + }); }); From f3b5f94729c14a90eb3997b7a5b54c507501eea7 Mon Sep 17 00:00:00 2001 From: ehenon Date: Thu, 18 Sep 2025 16:09:22 +0200 Subject: [PATCH 2/3] fix: prettier issue on README file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da69c467ff..abd886f284 100644 --- a/README.md +++ b/README.md @@ -799,7 +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. | -| `@IsActuallyOptional()` | Checks if given value is missing (=== undefined) and if so, ignores all the validators on the property. | +| `@IsActuallyOptional()` | 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). | From 55f12ba205380e650f5c92239c0b464a987671cd Mon Sep 17 00:00:00 2001 From: ehenon Date: Fri, 19 Sep 2025 10:52:12 +0200 Subject: [PATCH 3/3] feat: rename @IsActuallyOptional to @IsStrictlyOptional --- README.md | 2 +- .../common/{IsActuallyOptional.ts => IsStrictlyOptional.ts} | 6 +++--- src/decorator/decorators.ts | 2 +- test/functional/conditional-validation.spec.ts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/decorator/common/{IsActuallyOptional.ts => IsStrictlyOptional.ts} (86%) diff --git a/README.md b/README.md index abd886f284..d31fd25204 100644 --- a/README.md +++ b/README.md @@ -799,7 +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. | -| `@IsActuallyOptional()` | Checks if given value is missing (=== 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/IsActuallyOptional.ts b/src/decorator/common/IsStrictlyOptional.ts similarity index 86% rename from src/decorator/common/IsActuallyOptional.ts rename to src/decorator/common/IsStrictlyOptional.ts index 3e714a001b..5e0318043d 100644 --- a/src/decorator/common/IsActuallyOptional.ts +++ b/src/decorator/common/IsStrictlyOptional.ts @@ -4,16 +4,16 @@ import { ValidationTypes } from '../../validation/ValidationTypes'; import { ValidationMetadata } from '../../metadata/ValidationMetadata'; import { getMetadataStorage } from '../../metadata/MetadataStorage'; -export const IS_ACTUALLY_OPTIONAL = 'isActuallyOptional'; +export const IS_STRICTLY_OPTIONAL = 'isStrictlyOptional'; /** * Checks if value is missing (undefined) and if so, ignores all validators. */ -export function IsActuallyOptional(validationOptions?: ValidationOptions): PropertyDecorator { +export function IsStrictlyOptional(validationOptions?: ValidationOptions): PropertyDecorator { return function (object: object, propertyName: string): void { const args: ValidationMetadataArgs = { type: ValidationTypes.CONDITIONAL_VALIDATION, - name: IS_ACTUALLY_OPTIONAL, + name: IS_STRICTLY_OPTIONAL, target: object.constructor, propertyName: propertyName, constraints: [ diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts index fe8bb37b1b..4ad53ed43a 100644 --- a/src/decorator/decorators.ts +++ b/src/decorator/decorators.ts @@ -9,7 +9,7 @@ export * from './common/Allow'; export * from './common/IsDefined'; export * from './common/IsOptional'; -export * from './common/IsActuallyOptional'; +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 4df84bfee2..00789fd495 100644 --- a/test/functional/conditional-validation.spec.ts +++ b/test/functional/conditional-validation.spec.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty, ValidateIf, IsOptional, IsActuallyOptional, 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(); @@ -97,7 +97,7 @@ describe('conditional validation', () => { expect.assertions(5); class MyClass { - @IsActuallyOptional() + @IsStrictlyOptional() @Equals('test') title: string | null = null; } @@ -114,7 +114,7 @@ describe('conditional validation', () => { it('should not validate a property when value is missing', () => { class MyClass { - @IsActuallyOptional() + @IsStrictlyOptional() @Equals('test') title?: string = undefined; }