diff --git a/packages/query-graphql/__tests__/metadata/metadata-storage.spec.ts b/packages/query-graphql/__tests__/metadata/metadata-storage.spec.ts index c35a7be7d..330da7f57 100644 --- a/packages/query-graphql/__tests__/metadata/metadata-storage.spec.ts +++ b/packages/query-graphql/__tests__/metadata/metadata-storage.spec.ts @@ -1,20 +1,53 @@ -import { ObjectType, Int } from '@nestjs/graphql'; -import { FilterableField } from '../../src/decorators/filterable-field.decorator'; +import { ObjectType, Int, Field } from '@nestjs/graphql'; +import { FilterableField, Relation, Connection, PagingStrategies, Reference } from '../../src'; import { getMetadataStorage } from '../../src/metadata'; describe('GraphQLQueryMetadataStorage', () => { + @ObjectType() + class SomeRelation {} + + @ObjectType() + class SomeReference { + @Field() + id!: number; + } + @ObjectType({ isAbstract: true }) + @Relation('test', () => SomeRelation) + @Relation('tests', () => [SomeRelation]) + @Connection('testConnection', () => SomeRelation) + @Reference('testReference', () => SomeReference, { id: 'referenceId' }) class BaseType { @FilterableField(() => Int) id!: number; + + @Field() + referenceId!: number; } @ObjectType() + @Relation('implementedRelation', () => SomeRelation) + @Relation('implementedRelations', () => [SomeRelation]) + @Connection('implementedConnection', () => SomeRelation) + @Reference('implementedReference', () => SomeReference, { id: 'referenceId' }) class ImplementingClass extends BaseType { @FilterableField() implemented!: boolean; } + @ObjectType() + @Relation('implementedRelation', () => SomeRelation, { relationName: 'test' }) + @Relation('implementedRelations', () => [SomeRelation], { relationName: 'tests' }) + @Connection('implementedConnection', () => SomeRelation, { relationName: 'testConnection' }) + @Reference('implementedReference', () => SomeReference, { id: 'someReferenceId' }) + class DuplicateImplementor extends ImplementingClass { + @FilterableField({ name: 'test' }) + id!: number; + + @Field() + someReferenceId!: number; + } + const metadataStorage = getMetadataStorage(); describe('getFilterableObjectFields', () => { @@ -32,15 +65,80 @@ describe('GraphQLQueryMetadataStorage', () => { }); it('should exclude duplicate fields inherited filterable fields for a type', () => { - @ObjectType() - class DuplicateImplementor extends ImplementingClass { - @FilterableField({ name: 'test' }) - id!: number; - } expect(metadataStorage.getFilterableObjectFields(DuplicateImplementor)).toEqual([ { propertyName: 'implemented', target: Boolean }, { propertyName: 'id', target: Number, advancedOptions: { name: 'test' } }, ]); }); }); + + describe('getRelations', () => { + it('should return relations for a type', () => { + expect(metadataStorage.getRelations(BaseType)).toEqual({ + one: { + test: { DTO: SomeRelation }, + }, + many: { + tests: { DTO: SomeRelation, pagingStrategy: 'offset' }, + testConnection: { DTO: SomeRelation, pagingStrategy: 'cursor' }, + }, + }); + }); + + it('should return inherited relations fields for a type', () => { + expect(metadataStorage.getRelations(ImplementingClass)).toEqual({ + one: { + test: { DTO: SomeRelation }, + implementedRelation: { DTO: SomeRelation }, + }, + many: { + tests: { DTO: SomeRelation, pagingStrategy: PagingStrategies.OFFSET }, + testConnection: { DTO: SomeRelation, pagingStrategy: PagingStrategies.CURSOR }, + implementedRelations: { DTO: SomeRelation, pagingStrategy: PagingStrategies.OFFSET }, + implementedConnection: { DTO: SomeRelation, pagingStrategy: PagingStrategies.CURSOR }, + }, + }); + }); + + it('should exclude duplicate inherited relations fields for a type', () => { + expect(metadataStorage.getRelations(DuplicateImplementor)).toEqual({ + one: { + test: { DTO: SomeRelation }, + implementedRelation: { DTO: SomeRelation, relationName: 'test' }, + }, + many: { + tests: { DTO: SomeRelation, pagingStrategy: PagingStrategies.OFFSET }, + testConnection: { DTO: SomeRelation, pagingStrategy: PagingStrategies.CURSOR }, + implementedRelations: { DTO: SomeRelation, pagingStrategy: PagingStrategies.OFFSET, relationName: 'tests' }, + implementedConnection: { + DTO: SomeRelation, + pagingStrategy: PagingStrategies.CURSOR, + relationName: 'testConnection', + }, + }, + }); + }); + }); + + describe('getReferences', () => { + it('should return references for a type', () => { + expect(metadataStorage.getReferences(BaseType)).toEqual({ + testReference: { DTO: SomeReference, keys: { id: 'referenceId' } }, + }); + }); + + it('should return inherited references fields for a type', () => { + expect(metadataStorage.getReferences(ImplementingClass)).toEqual({ + testReference: { DTO: SomeReference, keys: { id: 'referenceId' } }, + implementedReference: { DTO: SomeReference, keys: { id: 'referenceId' } }, + }); + }); + + it('should exclude duplicate inherited references fields for a type', () => { + expect(metadataStorage.getReferences(DuplicateImplementor)).toEqual({ + testReference: { DTO: SomeReference, keys: { id: 'referenceId' } }, + implementedReference: { DTO: SomeReference, keys: { id: 'someReferenceId' } }, + }); + }); + }); }); diff --git a/packages/query-graphql/src/metadata/metadata-storage.ts b/packages/query-graphql/src/metadata/metadata-storage.ts index ea32e2fd2..2c8e0eb12 100644 --- a/packages/query-graphql/src/metadata/metadata-storage.ts +++ b/packages/query-graphql/src/metadata/metadata-storage.ts @@ -1,9 +1,8 @@ -import { TypeMetadataStorage } from '@nestjs/graphql/dist/schema-builder/storages/type-metadata.storage'; import { LazyMetadataStorage } from '@nestjs/graphql/dist/schema-builder/storages/lazy-metadata.storage'; -import { AggregateResponse, Class, Filter, SortField } from '@nestjs-query/core'; import { ObjectTypeMetadata } from '@nestjs/graphql/dist/schema-builder/metadata/object-type.metadata'; -import { ReturnTypeFunc, FieldOptions } from '@nestjs/graphql'; import { EnumMetadata } from '@nestjs/graphql/dist/schema-builder/metadata'; +import { AggregateResponse, Class, Filter, SortField } from '@nestjs-query/core'; +import { ReturnTypeFunc, FieldOptions, TypeMetadataStorage } from '@nestjs/graphql'; import { ReferencesOpts, RelationsOpts, ResolverRelation, ResolverRelationReference } from '../resolvers/relations'; import { ReferencesKeys } from '../resolvers/relations/relations.interface'; import { EdgeType, StaticConnectionType } from '../types/connection'; @@ -143,22 +142,8 @@ export class GraphQLQueryMetadataStorage { } getRelations(type: Class): RelationsOpts { - const relations: RelationsOpts = {}; - const metaRelations = this.relationStorage.get(type); - if (!metaRelations) { - return relations; - } - metaRelations.forEach((r) => { - const relationType = r.relationTypeFunc(); - const DTO = Array.isArray(relationType) ? relationType[0] : relationType; - const opts = { ...r.relationOpts, DTO }; - if (r.isMany) { - relations.many = { ...relations.many, [r.name]: opts }; - } else { - relations.one = { ...relations.one, [r.name]: opts }; - } - }); - return relations; + const relationDescriptors = this.getRelationsDescriptors(type); + return this.convertRelationsToOpts(relationDescriptors); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -172,14 +157,8 @@ export class GraphQLQueryMetadataStorage { } getReferences(type: Class): ReferencesOpts { - const metaReferences = this.referenceStorage.get(type); - if (!metaReferences) { - return {}; - } - return metaReferences.reduce((references, r) => { - const opts = { ...r.relationOpts, DTO: r.relationTypeFunc(), keys: r.keys }; - return { ...references, [r.name]: opts }; - }, {} as ReferencesOpts); + const descriptors = this.getReferenceDescriptors(type); + return this.convertReferencesToOpts(descriptors); } addAggregateResponseType(name: string, agg: Class>): void { @@ -219,4 +198,54 @@ export class GraphQLQueryMetadataStorage { } return undefined; } + + private getRelationsDescriptors(type: Class): RelationDescriptor[] { + const metaRelations = this.relationStorage.get(type) ?? []; + const relationNames = metaRelations.map((t) => t.name); + const baseClass = Object.getPrototypeOf(type) as Class; + if (baseClass) { + const inheritedRelations = this.getRelationsDescriptors(baseClass).filter( + // filter out duplicates + (t) => !relationNames.includes(t.name), + ); + return [...inheritedRelations, ...metaRelations]; + } + return metaRelations; + } + + private getReferenceDescriptors(type: Class): ReferenceDescriptor[] { + const metaReferences = this.referenceStorage.get(type) ?? []; + const referenceNames = metaReferences.map((r) => r.name); + const baseClass = Object.getPrototypeOf(type) as Class; + if (baseClass) { + const inheritedReferences = this.getReferenceDescriptors(baseClass).filter( + // filter out duplicates + (t) => !referenceNames.includes(t.name), + ); + return [...inheritedReferences, ...metaReferences]; + } + return metaReferences; + } + + private convertRelationsToOpts(relations: RelationDescriptor[]): RelationsOpts { + const relationOpts: RelationsOpts = {}; + relations.forEach((r) => { + const relationType = r.relationTypeFunc(); + const DTO = Array.isArray(relationType) ? relationType[0] : relationType; + const opts = { ...r.relationOpts, DTO }; + if (r.isMany) { + relationOpts.many = { ...relationOpts.many, [r.name]: opts }; + } else { + relationOpts.one = { ...relationOpts.one, [r.name]: opts }; + } + }); + return relationOpts; + } + + private convertReferencesToOpts(references: ReferenceDescriptor[]): ReferencesOpts { + return references.reduce((referenceOpts, r) => { + const opts = { ...r.relationOpts, DTO: r.relationTypeFunc(), keys: r.keys }; + return { ...referenceOpts, [r.name]: opts }; + }, {} as ReferencesOpts); + } }