From 8c721d0e7a9e7b6c92b67b3c28b8785fdd5f3650 Mon Sep 17 00:00:00 2001 From: Andrii Zhupyk Date: Sat, 6 Feb 2021 00:36:16 +0200 Subject: [PATCH 1/2] fix: inherit directive and extensions in type helpers, compile unique directives --- .../storages/type-metadata.storage.ts | 52 ++++++++++++------- lib/type-helpers/intersection-type.helper.ts | 2 + lib/type-helpers/omit-type.helper.ts | 2 + lib/type-helpers/partial-type.helper.ts | 2 + lib/type-helpers/pick-type.helper.ts | 2 + lib/type-helpers/type-helpers.utils.ts | 16 ++++++ .../intersection-type.helper.spec.ts | 29 +++++++++-- tests/type-helpers/omit-type.helper.spec.ts | 18 +++++-- .../type-helpers/partial-type.helper.spec.ts | 18 +++++-- tests/type-helpers/pick-type.helper.spec.ts | 22 ++++++-- 10 files changed, 128 insertions(+), 35 deletions(-) create mode 100644 lib/type-helpers/type-helpers.utils.ts diff --git a/lib/schema-builder/storages/type-metadata.storage.ts b/lib/schema-builder/storages/type-metadata.storage.ts index b84b903f2..c0bf71508 100644 --- a/lib/schema-builder/storages/type-metadata.storage.ts +++ b/lib/schema-builder/storages/type-metadata.storage.ts @@ -270,7 +270,13 @@ export class TypeMetadataStorageHost { item.properties = this.getClassFieldsByPredicate(belongsToClass); } if (!item.directives) { - item.directives = this.classDirectives.filter(belongsToClass); + item.directives = this.classDirectives + .filter(belongsToClass) + .reduce( + (acc, curr) => + acc.some((item) => item.sdl === curr.sdl) ? acc : [...acc, curr], + [], + ); } if (!item.extensions) { item.extensions = this.classExtensions @@ -293,12 +299,8 @@ export class TypeMetadataStorageHost { field.methodArgs = this.params.filter( (param) => isHostEqual(param) && field.name === param.methodName, ); - field.directives = this.fieldDirectives.filter( - this.isFieldDirectiveOrExtension.bind(this, field), - ); - field.extensions = this.fieldExtensions - .filter(this.isFieldDirectiveOrExtension.bind(this, field)) - .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); + field.directives = this.getDirectives(field); + field.extensions = this.getExtensions(field); }); return fields; } @@ -312,12 +314,8 @@ export class TypeMetadataStorageHost { item.methodArgs = this.params.filter( (param) => isTypeEqual(param) && item.methodName === param.methodName, ); - item.directives = this.fieldDirectives.filter( - this.isFieldDirectiveOrExtension.bind(this, item), - ); - item.extensions = this.fieldExtensions - .filter(this.isFieldDirectiveOrExtension.bind(this, item)) - .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); + item.directives = this.getDirectives(item); + item.extensions = this.getExtensions(item); }); } @@ -326,12 +324,8 @@ export class TypeMetadataStorageHost { metadata.forEach((item) => { const belongsToClass = isTargetEqual.bind(undefined, item); - item.directives = this.fieldDirectives.filter( - this.isFieldDirectiveOrExtension.bind(this, item), - ); - item.extensions = this.fieldExtensions - .filter(this.isFieldDirectiveOrExtension.bind(this, item)) - .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); + item.directives = this.getDirectives(item); + item.extensions = this.getExtensions(item); item.objectTypeFn = item.kind === 'external' @@ -438,6 +432,26 @@ export class TypeMetadataStorageHost { }); } + private getDirectives( + field: PropertyMetadata | BaseResolverMetadata, + ): PropertyDirectiveMetadata[] { + return this.fieldDirectives + .filter(this.isFieldDirectiveOrExtension.bind(this, field)) + .reduce( + (acc, curr) => + acc.some((item) => item.sdl === curr.sdl) ? acc : [...acc, curr], + [], + ); + } + + private getExtensions( + field: PropertyMetadata | BaseResolverMetadata, + ): Record { + return this.fieldExtensions + .filter(this.isFieldDirectiveOrExtension.bind(this, field)) + .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); + } + private isFieldDirectiveOrExtension( host: Record<'target' | 'methodName' | 'name', any>, metadata: PropertyDirectiveMetadata | PropertyExtensionsMetadata, diff --git a/lib/type-helpers/intersection-type.helper.ts b/lib/type-helpers/intersection-type.helper.ts index bee42662a..2457f19d5 100644 --- a/lib/type-helpers/intersection-type.helper.ts +++ b/lib/type-helpers/intersection-type.helper.ts @@ -7,6 +7,7 @@ import { import { Field } from '../decorators'; import { ClassDecoratorFactory } from '../interfaces/class-decorator-factory.interface'; import { getFieldsAndDecoratorForType } from '../schema-builder/utils/get-fields-and-decorator.util'; +import { applyFieldDecorators } from './type-helpers.utils'; export function IntersectionType( classARef: Type, @@ -41,6 +42,7 @@ export function IntersectionType( IntersectionObjectType.prototype, item.name, ); + applyFieldDecorators(IntersectionObjectType, item); }); Object.defineProperty(IntersectionObjectType, 'name', { diff --git a/lib/type-helpers/omit-type.helper.ts b/lib/type-helpers/omit-type.helper.ts index 7f4ae543a..5d4a4f8ee 100644 --- a/lib/type-helpers/omit-type.helper.ts +++ b/lib/type-helpers/omit-type.helper.ts @@ -8,6 +8,7 @@ import { import { Field } from '../decorators'; import { ClassDecoratorFactory } from '../interfaces/class-decorator-factory.interface'; import { getFieldsAndDecoratorForType } from '../schema-builder/utils/get-fields-and-decorator.util'; +import { applyFieldDecorators } from './type-helpers.utils'; export function OmitType( classRef: Type, @@ -47,6 +48,7 @@ export function OmitType( OmitObjectType.prototype, item.name, ); + applyFieldDecorators(OmitObjectType, item); }); return OmitObjectType as Type>; } diff --git a/lib/type-helpers/partial-type.helper.ts b/lib/type-helpers/partial-type.helper.ts index 5742a33c8..814cea620 100644 --- a/lib/type-helpers/partial-type.helper.ts +++ b/lib/type-helpers/partial-type.helper.ts @@ -10,6 +10,7 @@ import { Field } from '../decorators'; import { ClassDecoratorFactory } from '../interfaces/class-decorator-factory.interface'; import { METADATA_FACTORY_NAME } from '../plugin/plugin-constants'; import { getFieldsAndDecoratorForType } from '../schema-builder/utils/get-fields-and-decorator.util'; +import { applyFieldDecorators } from './type-helpers.utils'; export function PartialType( classRef: Type, @@ -44,6 +45,7 @@ export function PartialType( item.name, ); applyIsOptionalDecorator(PartialObjectType, item.name); + applyFieldDecorators(PartialObjectType, item); }); if (PartialObjectType[METADATA_FACTORY_NAME]) { diff --git a/lib/type-helpers/pick-type.helper.ts b/lib/type-helpers/pick-type.helper.ts index d05e7148c..08ef6d430 100644 --- a/lib/type-helpers/pick-type.helper.ts +++ b/lib/type-helpers/pick-type.helper.ts @@ -8,6 +8,7 @@ import { import { Field } from '../decorators'; import { ClassDecoratorFactory } from '../interfaces/class-decorator-factory.interface'; import { getFieldsAndDecoratorForType } from '../schema-builder/utils/get-fields-and-decorator.util'; +import { applyFieldDecorators } from './type-helpers.utils'; export function PickType( classRef: Type, @@ -48,6 +49,7 @@ export function PickType( PickObjectType.prototype, item.name, ); + applyFieldDecorators(PickObjectType, item); }); return PickObjectType as Type>; } diff --git a/lib/type-helpers/type-helpers.utils.ts b/lib/type-helpers/type-helpers.utils.ts new file mode 100644 index 000000000..ee2d5aa57 --- /dev/null +++ b/lib/type-helpers/type-helpers.utils.ts @@ -0,0 +1,16 @@ +import { Directive, Extensions } from '../decorators'; +import { PropertyMetadata } from '../schema-builder/metadata'; + +export function applyFieldDecorators( + targetClass: Function, + item: PropertyMetadata, +) { + if (item.extensions) { + Extensions(item.extensions)(targetClass.prototype, item.name); + } + if (item.directives?.length) { + item.directives.map((directive) => { + Directive(directive.sdl)(targetClass.prototype, item.name); + }); + } +} diff --git a/tests/type-helpers/intersection-type.helper.spec.ts b/tests/type-helpers/intersection-type.helper.spec.ts index b9cc0da13..15274cd38 100644 --- a/tests/type-helpers/intersection-type.helper.spec.ts +++ b/tests/type-helpers/intersection-type.helper.spec.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '../../lib/decorators'; +import { Directive, Extensions, Field, ObjectType } from '../../lib/decorators'; import { METADATA_FACTORY_NAME } from '../../lib/plugin/plugin-constants'; import { getFieldsAndDecoratorForType } from '../../lib/schema-builder/utils/get-fields-and-decorator.util'; import { IntersectionType } from '../../lib/type-helpers'; @@ -7,6 +7,8 @@ describe('IntersectionType', () => { @ObjectType() class ClassA { @Field({ nullable: true }) + @Directive('@upper') + @Extensions({ extension: true }) login: string; @Field() @@ -16,6 +18,8 @@ describe('IntersectionType', () => { @ObjectType() class ClassB { @Field() + @Directive('@upper') + @Extensions({ extension: true }) lastName: string; firstName?: string; @@ -30,13 +34,30 @@ describe('IntersectionType', () => { class UpdateUserDto extends IntersectionType(ClassA, ClassB) {} it('should inherit all fields from two types', () => { - const { fields } = getFieldsAndDecoratorForType( - Object.getPrototypeOf(UpdateUserDto), - ); + const prototype = Object.getPrototypeOf(UpdateUserDto); + const { fields } = getFieldsAndDecoratorForType(prototype); expect(fields.length).toEqual(4); expect(fields[0].name).toEqual('login'); expect(fields[1].name).toEqual('password'); expect(fields[2].name).toEqual('lastName'); expect(fields[3].name).toEqual('firstName'); + expect(fields[0].directives.length).toEqual(1); + expect(fields[0].directives).toContainEqual({ + fieldName: 'login', + sdl: '@upper', + target: prototype, + }); + expect(fields[0].extensions).toEqual({ + extension: true, + }); + expect(fields[2].directives.length).toEqual(1); + expect(fields[2].directives).toContainEqual({ + fieldName: 'lastName', + sdl: '@upper', + target: prototype, + }); + expect(fields[2].extensions).toEqual({ + extension: true, + }); }); }); diff --git a/tests/type-helpers/omit-type.helper.spec.ts b/tests/type-helpers/omit-type.helper.spec.ts index ce67abc2f..587dbb8cf 100644 --- a/tests/type-helpers/omit-type.helper.spec.ts +++ b/tests/type-helpers/omit-type.helper.spec.ts @@ -1,6 +1,6 @@ import { Transform } from 'class-transformer'; import { MinLength } from 'class-validator'; -import { Field, ObjectType } from '../../lib/decorators'; +import { Directive, Extensions, Field, ObjectType } from '../../lib/decorators'; import { getFieldsAndDecoratorForType } from '../../lib/schema-builder/utils/get-fields-and-decorator.util'; import { OmitType } from '../../lib/type-helpers'; @@ -14,6 +14,8 @@ describe('OmitType', () => { @Transform((str) => str + '_transformed') @MinLength(10) @Field() + @Directive('@upper') + @Extensions({ extension: true }) password: string; @Field({ name: 'id' }) @@ -23,10 +25,18 @@ describe('OmitType', () => { class UpdateUserDto extends OmitType(CreateUserDto, ['login', '_id']) {} it('should inherit all fields except for "login" and "_id"', () => { - const { fields } = getFieldsAndDecoratorForType( - Object.getPrototypeOf(UpdateUserDto), - ); + const prototype = Object.getPrototypeOf(UpdateUserDto); + const { fields } = getFieldsAndDecoratorForType(prototype); expect(fields.length).toEqual(1); expect(fields[0].name).toEqual('password'); + expect(fields[0].directives.length).toEqual(1); + expect(fields[0].directives).toContainEqual({ + fieldName: 'password', + sdl: '@upper', + target: prototype, + }); + expect(fields[0].extensions).toEqual({ + extension: true, + }); }); }); diff --git a/tests/type-helpers/partial-type.helper.spec.ts b/tests/type-helpers/partial-type.helper.spec.ts index 644a99994..0bfc9c8ef 100644 --- a/tests/type-helpers/partial-type.helper.spec.ts +++ b/tests/type-helpers/partial-type.helper.spec.ts @@ -1,6 +1,6 @@ import { Expose, Transform } from 'class-transformer'; import { IsString } from 'class-validator'; -import { Field, ObjectType } from '../../lib/decorators'; +import { Directive, Extensions, Field, ObjectType } from '../../lib/decorators'; import { getFieldsAndDecoratorForType } from '../../lib/schema-builder/utils/get-fields-and-decorator.util'; import { PartialType } from '../../lib/type-helpers'; @@ -8,6 +8,8 @@ describe('PartialType', () => { @ObjectType({ isAbstract: true }) abstract class BaseType { @Field() + @Directive('@upper') + @Extensions({ extension: true }) id: string; @Field() @@ -32,9 +34,8 @@ describe('PartialType', () => { class UpdateUserDto extends PartialType(CreateUserDto) {} it('should inherit all fields and set "nullable" to true', () => { - const { fields } = getFieldsAndDecoratorForType( - Object.getPrototypeOf(UpdateUserDto), - ); + const prototype = Object.getPrototypeOf(UpdateUserDto); + const { fields } = getFieldsAndDecoratorForType(prototype); expect(fields.length).toEqual(5); expect(fields).toEqual( @@ -61,5 +62,14 @@ describe('PartialType', () => { }), ]), ); + expect(fields[0].directives.length).toEqual(1); + expect(fields[0].directives).toContainEqual({ + fieldName: 'id', + sdl: '@upper', + target: prototype, + }); + expect(fields[0].extensions).toEqual({ + extension: true, + }); }); }); diff --git a/tests/type-helpers/pick-type.helper.spec.ts b/tests/type-helpers/pick-type.helper.spec.ts index fd5d4407c..ee500b249 100644 --- a/tests/type-helpers/pick-type.helper.spec.ts +++ b/tests/type-helpers/pick-type.helper.spec.ts @@ -1,6 +1,12 @@ import { Transform } from 'class-transformer'; import { MinLength } from 'class-validator'; -import { ArgsType, Field, ObjectType } from '../../lib/decorators'; +import { + ArgsType, + Directive, + Extensions, + Field, + ObjectType, +} from '../../lib/decorators'; import { getFieldsAndDecoratorForType } from '../../lib/schema-builder/utils/get-fields-and-decorator.util'; import { PickType } from '../../lib/type-helpers'; @@ -10,6 +16,7 @@ describe('PickType', () => { @Transform((str) => str + '_transformed') @MinLength(10) @Field({ nullable: true }) + @Directive('@upper') login: string; @MinLength(10) @@ -17,6 +24,7 @@ describe('PickType', () => { password: string; @Field({ name: 'id' }) + @Extensions({ extension: true }) _id: string; } @@ -25,11 +33,16 @@ describe('PickType', () => { class UpdateUserWithIdDto extends PickType(CreateUserDto, ['_id']) {} it('should inherit "login" field', () => { - const { fields } = getFieldsAndDecoratorForType( - Object.getPrototypeOf(UpdateUserDto), - ); + const prototype = Object.getPrototypeOf(UpdateUserDto); + const { fields } = getFieldsAndDecoratorForType(prototype); expect(fields.length).toEqual(1); expect(fields[0].name).toEqual('login'); + expect(fields[0].directives.length).toEqual(1); + expect(fields[0].directives).toContainEqual({ + fieldName: 'login', + sdl: '@upper', + target: prototype, + }); }); it('should inherit renamed "_id" field', () => { @@ -38,6 +51,7 @@ describe('PickType', () => { ); expect(fields.length).toEqual(1); expect(fields[0].name).toEqual('_id'); + expect(fields[0].extensions).toEqual({ extension: true }); }); @ArgsType() From b97030c84c7e578388fd190f6261ee6370c9985d Mon Sep 17 00:00:00 2001 From: Andrii Zhupyk Date: Sat, 6 Feb 2021 01:14:16 +0200 Subject: [PATCH 2/2] fix: skip adding existing directive metadata --- .../storages/type-metadata.storage.ts | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/lib/schema-builder/storages/type-metadata.storage.ts b/lib/schema-builder/storages/type-metadata.storage.ts index c0bf71508..6b2f7a810 100644 --- a/lib/schema-builder/storages/type-metadata.storage.ts +++ b/lib/schema-builder/storages/type-metadata.storage.ts @@ -157,11 +157,28 @@ export class TypeMetadataStorageHost { } addDirectiveMetadata(metadata: ClassDirectiveMetadata) { - this.classDirectives.push(metadata); + const exist = this.fieldDirectives.some((directiveMetadata) => { + return ( + directiveMetadata.sdl === metadata.sdl && + directiveMetadata.target === metadata.target + ); + }); + if (!exist) { + this.classDirectives.push(metadata); + } } addDirectivePropertyMetadata(metadata: PropertyDirectiveMetadata) { - this.fieldDirectives.push(metadata); + const exist = this.fieldDirectives.some((directiveMetadata) => { + return ( + directiveMetadata.fieldName === metadata.fieldName && + directiveMetadata.sdl === metadata.sdl && + directiveMetadata.target === metadata.target + ); + }); + if (!exist) { + this.fieldDirectives.push(metadata); + } } addExtensionsMetadata(metadata: ClassExtensionsMetadata) { @@ -270,13 +287,7 @@ export class TypeMetadataStorageHost { item.properties = this.getClassFieldsByPredicate(belongsToClass); } if (!item.directives) { - item.directives = this.classDirectives - .filter(belongsToClass) - .reduce( - (acc, curr) => - acc.some((item) => item.sdl === curr.sdl) ? acc : [...acc, curr], - [], - ); + item.directives = this.classDirectives.filter(belongsToClass); } if (!item.extensions) { item.extensions = this.classExtensions @@ -299,8 +310,12 @@ export class TypeMetadataStorageHost { field.methodArgs = this.params.filter( (param) => isHostEqual(param) && field.name === param.methodName, ); - field.directives = this.getDirectives(field); - field.extensions = this.getExtensions(field); + field.directives = this.fieldDirectives.filter( + this.isFieldDirectiveOrExtension.bind(this, field), + ); + field.extensions = this.fieldExtensions + .filter(this.isFieldDirectiveOrExtension.bind(this, field)) + .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); }); return fields; } @@ -314,8 +329,12 @@ export class TypeMetadataStorageHost { item.methodArgs = this.params.filter( (param) => isTypeEqual(param) && item.methodName === param.methodName, ); - item.directives = this.getDirectives(item); - item.extensions = this.getExtensions(item); + item.directives = this.fieldDirectives.filter( + this.isFieldDirectiveOrExtension.bind(this, item), + ); + item.extensions = this.fieldExtensions + .filter(this.isFieldDirectiveOrExtension.bind(this, item)) + .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); }); } @@ -324,8 +343,12 @@ export class TypeMetadataStorageHost { metadata.forEach((item) => { const belongsToClass = isTargetEqual.bind(undefined, item); - item.directives = this.getDirectives(item); - item.extensions = this.getExtensions(item); + item.directives = this.fieldDirectives.filter( + this.isFieldDirectiveOrExtension.bind(this, item), + ); + item.extensions = this.fieldExtensions + .filter(this.isFieldDirectiveOrExtension.bind(this, item)) + .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); item.objectTypeFn = item.kind === 'external' @@ -432,26 +455,6 @@ export class TypeMetadataStorageHost { }); } - private getDirectives( - field: PropertyMetadata | BaseResolverMetadata, - ): PropertyDirectiveMetadata[] { - return this.fieldDirectives - .filter(this.isFieldDirectiveOrExtension.bind(this, field)) - .reduce( - (acc, curr) => - acc.some((item) => item.sdl === curr.sdl) ? acc : [...acc, curr], - [], - ); - } - - private getExtensions( - field: PropertyMetadata | BaseResolverMetadata, - ): Record { - return this.fieldExtensions - .filter(this.isFieldDirectiveOrExtension.bind(this, field)) - .reduce((curr, acc) => ({ ...curr, ...acc.value }), {}); - } - private isFieldDirectiveOrExtension( host: Record<'target' | 'methodName' | 'name', any>, metadata: PropertyDirectiveMetadata | PropertyExtensionsMetadata,