Skip to content

Commit

Permalink
Merge pull request #3276 from bloom-alex/add-skip-nulllable-properties
Browse files Browse the repository at this point in the history
feat(@nestjs/graphql): add support for skipNullProperties in PartialType
  • Loading branch information
kamilmysliwiec authored Oct 23, 2024
2 parents a18d3b9 + a9ab004 commit dc1110e
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 4 deletions.
16 changes: 13 additions & 3 deletions packages/graphql/lib/type-helpers/partial-type.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Type } from '@nestjs/common';
import { isFunction } from '@nestjs/common/utils/shared.utils';
import {
applyIsOptionalDecorator,
applyValidateIfDefinedDecorator,
inheritPropertyInitializers,
inheritTransformationMetadata,
inheritValidationMetadata,
Expand All @@ -17,6 +18,7 @@ import { applyFieldDecorators } from './type-helpers.utils';
interface PartialTypeOptions {
decorator?: ClassDecoratorFactory;
omitDefaultValues?: boolean;
skipNullProperties?: boolean;
}

function isPartialTypeOptions(
Expand All @@ -25,7 +27,8 @@ function isPartialTypeOptions(
return (
optionsOrDecorator &&
('decorator' in optionsOrDecorator ||
'omitDefaultValues' in optionsOrDecorator)
'omitDefaultValues' in optionsOrDecorator ||
'skipNullProperties' in optionsOrDecorator)
);
}

Expand All @@ -42,13 +45,20 @@ export function PartialType<T>(

let decorator: ClassDecoratorFactory | undefined;
let omitDefaultValues = false;
let skipNullProperties = true;
if (isPartialTypeOptions(optionsOrDecorator)) {
decorator = optionsOrDecorator.decorator;
omitDefaultValues = optionsOrDecorator.omitDefaultValues;
skipNullProperties = optionsOrDecorator.skipNullProperties ?? true;
} else {
decorator = optionsOrDecorator;
}

const applyPartialDecoratorFn =
skipNullProperties === false
? applyValidateIfDefinedDecorator
: applyIsOptionalDecorator;

if (decorator) {
decorator({ isAbstract: true })(PartialObjectType);
} else {
Expand All @@ -70,7 +80,7 @@ export function PartialType<T>(
nullable: true,
defaultValue: omitDefaultValues ? undefined : item.options.defaultValue,
})(PartialObjectType.prototype, item.name);
applyIsOptionalDecorator(PartialObjectType, item.name);
applyPartialDecoratorFn(PartialObjectType, item.name);
applyFieldDecorators(PartialObjectType, item);
});
}
Expand All @@ -90,7 +100,7 @@ export function PartialType<T>(
PartialObjectType[METADATA_FACTORY_NAME](),
);
pluginFields.forEach((key) =>
applyIsOptionalDecorator(PartialObjectType, key),
applyPartialDecoratorFn(PartialObjectType, key),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { getValidationMetadataByTarget } from './type-helpers.test-utils';

@ObjectType()
class CreateUserDto extends BaseType {
@IsString()
@Field()
firstName: string;

@Field({ nullable: true })
login: string;

Expand All @@ -31,7 +35,7 @@ describe('PartialType', () => {
const prototype = Object.getPrototypeOf(UpdateUserDto);
const { fields } = getFieldsAndDecoratorForType(prototype);

expect(fields.length).toEqual(6);
expect(fields.length).toEqual(7);
expect(fields).toEqual(
expect.arrayContaining([
expect.objectContaining({
Expand All @@ -54,6 +58,10 @@ describe('PartialType', () => {
name: 'password',
options: { nullable: true },
}),
expect.objectContaining({
name: 'firstName',
options: { nullable: true },
}),
expect.objectContaining({
name: 'meta',
options: { nullable: true },
Expand All @@ -79,6 +87,7 @@ describe('PartialType', () => {
),
);
expect(Array.from(validationKeys)).toEqual([
'firstName',
'password',
'id',
'createdAt',
Expand All @@ -87,6 +96,12 @@ describe('PartialType', () => {
'meta',
]);
});
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);
});
describe('when object does not fulfil validation rules', () => {
it('"validate" should return validation errors', async () => {
const updateDto = new UpdateUserDto();
Expand Down Expand Up @@ -125,4 +140,39 @@ describe('PartialType', () => {
expect(fields[0].options.defaultValue).toEqual(undefined);
});
});

describe('skipNullProperties', () => {
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, {
skipNullProperties: undefined,
}) {}
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',
});
});
});
});

0 comments on commit dc1110e

Please sign in to comment.