Skip to content

Commit

Permalink
fix(graphql): Include inherited references and relations
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Jul 24, 2020
1 parent ec70d69 commit 26dd6f9
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 34 deletions.
112 changes: 105 additions & 7 deletions packages/query-graphql/__tests__/metadata/metadata-storage.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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' } },
});
});
});
});
83 changes: 56 additions & 27 deletions packages/query-graphql/src/metadata/metadata-storage.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -143,22 +142,8 @@ export class GraphQLQueryMetadataStorage {
}

getRelations<T>(type: Class<T>): 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
Expand All @@ -172,14 +157,8 @@ export class GraphQLQueryMetadataStorage {
}

getReferences<T>(type: Class<T>): ReferencesOpts<T> {
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<T>);
const descriptors = this.getReferenceDescriptors(type);
return this.convertReferencesToOpts(descriptors);
}

addAggregateResponseType<T>(name: string, agg: Class<AggregateResponse<T>>): void {
Expand Down Expand Up @@ -219,4 +198,54 @@ export class GraphQLQueryMetadataStorage {
}
return undefined;
}

private getRelationsDescriptors(type: Class<unknown>): RelationDescriptor<unknown>[] {
const metaRelations = this.relationStorage.get(type) ?? [];
const relationNames = metaRelations.map((t) => t.name);
const baseClass = Object.getPrototypeOf(type) as Class<unknown>;
if (baseClass) {
const inheritedRelations = this.getRelationsDescriptors(baseClass).filter(
// filter out duplicates
(t) => !relationNames.includes(t.name),
);
return [...inheritedRelations, ...metaRelations];
}
return metaRelations;
}

private getReferenceDescriptors<DTO>(type: Class<unknown>): ReferenceDescriptor<DTO, unknown>[] {
const metaReferences = this.referenceStorage.get(type) ?? [];
const referenceNames = metaReferences.map((r) => r.name);
const baseClass = Object.getPrototypeOf(type) as Class<unknown>;
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<unknown>[]): 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<DTO>(references: ReferenceDescriptor<DTO, unknown>[]): ReferencesOpts<DTO> {
return references.reduce((referenceOpts, r) => {
const opts = { ...r.relationOpts, DTO: r.relationTypeFunc(), keys: r.keys };
return { ...referenceOpts, [r.name]: opts };
}, {} as ReferencesOpts<DTO>);
}
}

0 comments on commit 26dd6f9

Please sign in to comment.