Skip to content

Commit

Permalink
feat: add skip null properties option to partial type
Browse files Browse the repository at this point in the history
  • Loading branch information
Evgeniy Novikov committed Feb 29, 2024
1 parent 7be9512 commit 1b4084e
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 4 deletions.
27 changes: 23 additions & 4 deletions lib/type-helpers/partial-type.helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Type } from '@nestjs/common';
import {
applyIsOptionalDecorator,
applyValidateIfDefinedDecorator,
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata
Expand All @@ -15,7 +16,25 @@ import { clonePluginMetadataFactory } from './mapped-types.utils';

const modelPropertiesAccessor = new ModelPropertiesAccessor();

export function PartialType<T>(classRef: Type<T>): Type<Partial<T>> {
export function PartialType<T>(
classRef: Type<T>,
/**
* 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<Partial<T>> {
const applyPartialDecoratorFn =
options.skipNullProperties === false
? applyValidateIfDefinedDecorator
: applyIsOptionalDecorator;

const fields = modelPropertiesAccessor.getModelProperties(classRef.prototype);

abstract class PartialTypeClass {
Expand All @@ -30,7 +49,7 @@ export function PartialType<T>(classRef: Type<T>): Type<Partial<T>> {
if (keysWithValidationConstraints) {
keysWithValidationConstraints
.filter((key) => !fields.includes(key))
.forEach((key) => applyIsOptionalDecorator(PartialTypeClass, key));
.forEach((key) => applyPartialDecoratorFn(PartialTypeClass, key));
}

inheritTransformationMetadata(classRef, PartialTypeClass);
Expand All @@ -48,7 +67,7 @@ export function PartialType<T>(classRef: Type<T>): Type<Partial<T>> {
PartialTypeClass[METADATA_FACTORY_NAME]()
);
pluginFields.forEach((key) =>
applyIsOptionalDecorator(PartialTypeClass, key)
applyPartialDecoratorFn(PartialTypeClass, key)
);
}

Expand All @@ -65,7 +84,7 @@ export function PartialType<T>(classRef: Type<T>): Type<Partial<T>> {
required: false
});
decoratorFactory(PartialTypeClass.prototype, key);
applyIsOptionalDecorator(PartialTypeClass, key);
applyPartialDecoratorFn(PartialTypeClass, key);
});
}
applyFields(fields);
Expand Down
35 changes: 35 additions & 0 deletions test/type-helpers/partial-type.helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down

0 comments on commit 1b4084e

Please sign in to comment.