diff --git a/lib/index.ts b/lib/index.ts index e15beac8..e0cbee88 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -5,6 +5,7 @@ export * from './partial-type.helper'; export * from './pick-type.helper'; export { applyIsOptionalDecorator, + applyValidateIfDefinedDecorator, inheritPropertyInitializers, inheritTransformationMetadata, inheritValidationMetadata, diff --git a/lib/partial-type.helper.ts b/lib/partial-type.helper.ts index 44bee529..b734bff6 100644 --- a/lib/partial-type.helper.ts +++ b/lib/partial-type.helper.ts @@ -2,13 +2,27 @@ import { Type } from '@nestjs/common'; import { MappedType } from './mapped-type.interface'; import { applyIsOptionalDecorator, + applyValidateIfDefinedDecorator, inheritPropertyInitializers, inheritTransformationMetadata, inheritValidationMetadata, } from './type-helpers.utils'; import { RemoveFieldsWithType } from './types/remove-fields-with-type.type'; -export function PartialType(classRef: Type) { +export function PartialType( + classRef: Type, + /** + * Configuration options. + */ + options: { + /** + * If true, validations will be ignored on a property if it is either null or undefined. If + * false, validations will be ignored only if the property is undefined. + * @default true + */ + skipNullProperties?: boolean; + } = {}, +) { abstract class PartialClassType { constructor() { inheritPropertyInitializers(this, classRef); @@ -20,7 +34,9 @@ export function PartialType(classRef: Type) { if (propertyKeys) { propertyKeys.forEach((key) => { - applyIsOptionalDecorator(PartialClassType, key); + options.skipNullProperties === false + ? applyValidateIfDefinedDecorator(PartialClassType, key) + : applyIsOptionalDecorator(PartialClassType, key); }); } diff --git a/lib/type-helpers.utils.ts b/lib/type-helpers.utils.ts index 01f55ae2..ff119a6c 100644 --- a/lib/type-helpers.utils.ts +++ b/lib/type-helpers.utils.ts @@ -15,6 +15,20 @@ export function applyIsOptionalDecorator( decoratorFactory(targetClass.prototype, propertyKey); } +export function applyValidateIfDefinedDecorator( + targetClass: Function, + propertyKey: string, +) { + if (!isClassValidatorAvailable()) { + return; + } + const classValidator: typeof import('class-validator') = require('class-validator'); + const decoratorFactory = classValidator.ValidateIf( + (_, value) => value !== undefined, + ); + decoratorFactory(targetClass.prototype, propertyKey); +} + export function inheritValidationMetadata( parentClass: Type, targetClass: Function, diff --git a/tests/partial-type.helper.spec.ts b/tests/partial-type.helper.spec.ts index abd3462f..cd97c778 100644 --- a/tests/partial-type.helper.spec.ts +++ b/tests/partial-type.helper.spec.ts @@ -81,4 +81,44 @@ describe('PartialType', () => { expect(updateUserDto.login).toEqual('defaultLogin'); }); }); + + describe('Configuration options', () => { + it('should not ignore validations for null properties when `skipNullProperties` is false', async () => { + class UpdateUserDtoDisallowNull extends PartialType(CreateUserDto, { + skipNullProperties: false, + }) {} + + const updateDto = new UpdateUserDtoDisallowNull(); + updateDto.password = null as any; + + const validationErrors = await validate(updateDto); + + expect(validationErrors.length).toBe(1); + expect(validationErrors[0].constraints).toEqual({ + isString: 'password must be a string', + }); + }); + + it('should ignore validations on null properties when `skipNullProperties` is undefined', async () => { + const updateDto = new UpdateUserDto(); + updateDto.password = null as any; + + const validationErrors = await validate(updateDto); + + expect(validationErrors.length).toBe(0); + }); + + it('should ignore validations on null properties when `skipNullProperties` is true', async () => { + class UpdateUserDtoAllowNull extends PartialType(CreateUserDto, { + skipNullProperties: true, + }) {} + + const updateDto = new UpdateUserDtoAllowNull(); + updateDto.password = null as any; + + const validationErrors = await validate(updateDto); + + expect(validationErrors.length).toBe(0); + }); + }); });