diff --git a/lib/type-helpers/partial-type.helper.ts b/lib/type-helpers/partial-type.helper.ts index 1872b8ad1..d6e626855 100644 --- a/lib/type-helpers/partial-type.helper.ts +++ b/lib/type-helpers/partial-type.helper.ts @@ -1,6 +1,7 @@ import { Type } from '@nestjs/common'; import { applyIsOptionalDecorator, + applyValidateIfDefinedDecorator, inheritPropertyInitializers, inheritTransformationMetadata, inheritValidationMetadata @@ -15,7 +16,25 @@ import { clonePluginMetadataFactory } from './mapped-types.utils'; const modelPropertiesAccessor = new ModelPropertiesAccessor(); -export function PartialType(classRef: Type): 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; + } = {} +): Type> { + const applyPartialDecoratorFn = + options.skipNullProperties === false + ? applyValidateIfDefinedDecorator + : applyIsOptionalDecorator; + const fields = modelPropertiesAccessor.getModelProperties(classRef.prototype); abstract class PartialTypeClass { @@ -30,7 +49,7 @@ export function PartialType(classRef: Type): Type> { if (keysWithValidationConstraints) { keysWithValidationConstraints .filter((key) => !fields.includes(key)) - .forEach((key) => applyIsOptionalDecorator(PartialTypeClass, key)); + .forEach((key) => applyPartialDecoratorFn(PartialTypeClass, key)); } inheritTransformationMetadata(classRef, PartialTypeClass); @@ -48,7 +67,7 @@ export function PartialType(classRef: Type): Type> { PartialTypeClass[METADATA_FACTORY_NAME]() ); pluginFields.forEach((key) => - applyIsOptionalDecorator(PartialTypeClass, key) + applyPartialDecoratorFn(PartialTypeClass, key) ); } @@ -65,7 +84,7 @@ export function PartialType(classRef: Type): Type> { required: false }); decoratorFactory(PartialTypeClass.prototype, key); - applyIsOptionalDecorator(PartialTypeClass, key); + applyPartialDecoratorFn(PartialTypeClass, key); }); } applyFields(fields); diff --git a/test/type-helpers/partial-type.helper.spec.ts b/test/type-helpers/partial-type.helper.spec.ts index 6ff6eb23a..0865ee017 100644 --- a/test/type-helpers/partial-type.helper.spec.ts +++ b/test/type-helpers/partial-type.helper.spec.ts @@ -21,9 +21,44 @@ describe('PartialType', () => { describe('Validation metadata', () => { it('should apply @IsOptional to properties reflected by the plugin', async () => { const updateDto = new UpdateUserDto(); + updateDto.firstName = null; const validationErrors = await validate(updateDto); expect(validationErrors).toHaveLength(0); }); + + it('should apply @IsOptional to properties reflected by the plugin if option `skipNullProperties` is true', async () => { + class UpdateUserWithNullableDto extends PartialType(CreateUserDto, { + skipNullProperties: true + }) {} + const updateDto = new UpdateUserWithNullableDto(); + updateDto.firstName = null; + const validationErrors = await validate(updateDto); + expect(validationErrors).toHaveLength(0); + }); + + it('should apply @IsOptional to properties reflected by the plugin if option `skipNullProperties` is undefined', async () => { + class UpdateUserWithoutNullableDto extends PartialType( + CreateUserDto, + {} + ) {} + const updateDto = new UpdateUserWithoutNullableDto(); + updateDto.firstName = null; + const validationErrors = await validate(updateDto); + expect(validationErrors).toHaveLength(0); + }); + + it('should apply @ValidateIf to properties reflected by the plugin if option `skipNullProperties` is false', async () => { + class UpdateUserWithoutNullableDto extends PartialType(CreateUserDto, { + skipNullProperties: false + }) {} + const updateDto = new UpdateUserWithoutNullableDto(); + updateDto.firstName = null; + const validationErrors = await validate(updateDto); + expect(validationErrors).toHaveLength(1); + expect(validationErrors[0].constraints).toEqual({ + isString: 'firstName must be a string' + }); + }); }); describe('OpenAPI metadata', () => { it('should return partial class', async () => {