Skip to content

Commit

Permalink
feat(mapped-types): add skip null properties option to partial type
Browse files Browse the repository at this point in the history
With this change, you can create a partial class which disallows null values, but allows
undefined values. Previously, every class created with PartialType allowed null values for
every property, which may be undesired if you are defining the DTO for a PATCH endpoint.
If the option is not defined, the behaviour is unchanged from the previous behaviour.
  • Loading branch information
tf3 committed Jan 11, 2024
1 parent 1c1c9c7 commit 981c4ca
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './partial-type.helper';
export * from './pick-type.helper';
export {
applyIsOptionalDecorator,
applyValidateIfDefinedDecorator,
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata,
Expand Down
20 changes: 18 additions & 2 deletions lib/partial-type.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(classRef: Type<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;
} = {},
) {
abstract class PartialClassType {
constructor() {
inheritPropertyInitializers(this, classRef);
Expand All @@ -20,7 +34,9 @@ export function PartialType<T>(classRef: Type<T>) {

if (propertyKeys) {
propertyKeys.forEach((key) => {
applyIsOptionalDecorator(PartialClassType, key);
options.skipNullProperties === false
? applyValidateIfDefinedDecorator(PartialClassType, key)
: applyIsOptionalDecorator(PartialClassType, key);
});
}

Expand Down
14 changes: 14 additions & 0 deletions lib/type-helpers.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>,
targetClass: Function,
Expand Down
40 changes: 40 additions & 0 deletions tests/partial-type.helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});

0 comments on commit 981c4ca

Please sign in to comment.