From 55991b4fc408eeb18c0b31bbda7673eaaeb0100f Mon Sep 17 00:00:00 2001 From: Thomas Blommaert <66438062+thomas-advantitge@users.noreply.github.com> Date: Thu, 18 Mar 2021 01:57:26 +0100 Subject: [PATCH 1/4] fix(federation): override federated resolveType for interfaces Interfaces example (https://docs.nestjs.com/graphql/interfaces) with `resolveType` didn't work when changing `GraphQLModule` to `GraphQLFederationModule`. --- lib/federation/graphql-federation.factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/federation/graphql-federation.factory.ts b/lib/federation/graphql-federation.factory.ts index a53cf8605..abf757e5e 100644 --- a/lib/federation/graphql-federation.factory.ts +++ b/lib/federation/graphql-federation.factory.ts @@ -222,7 +222,7 @@ export class GraphQLFederationFactory { // Bail if inconsistent with original schema if ( !autoGeneratedType || - !(autoGeneratedType instanceof GraphQLUnionType) || + !(autoGeneratedType instanceof GraphQLUnionType || autoGeneratedType instanceof GraphQLInterfaceType) || !autoGeneratedType.resolveType ) { return typeInFederatedSchema; From d6e53f3f87349399992942e296a0eed5e9e4f30b Mon Sep 17 00:00:00 2001 From: Thomas Blommaert Date: Sat, 20 Mar 2021 19:05:58 +0100 Subject: [PATCH 2/4] test: add federation resolveType test --- .../gql-module-options.interface.ts | 3 ++ tests/code-first-federation/app.module.ts | 2 ++ .../recipe/irecipe.resolver.ts | 10 +++++++ .../recipe/recipe.module.ts | 7 +++++ tests/code-first-federation/recipe/recipe.ts | 28 +++++++++++++++++++ tests/e2e/code-first-federation.spec.ts | 26 ++++++++++++++++- 6 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 tests/code-first-federation/recipe/irecipe.resolver.ts create mode 100644 tests/code-first-federation/recipe/recipe.module.ts create mode 100644 tests/code-first-federation/recipe/recipe.ts diff --git a/lib/interfaces/gql-module-options.interface.ts b/lib/interfaces/gql-module-options.interface.ts index 11dff2b4f..df0138bc7 100644 --- a/lib/interfaces/gql-module-options.interface.ts +++ b/lib/interfaces/gql-module-options.interface.ts @@ -66,6 +66,9 @@ export interface GqlModuleOptions * Apply `transformSchema` to the `autoSchemaFile` */ transformAutoSchemaFile?: boolean; + /** + * Pass a custom apollo server + */ } export interface GqlOptionsFactory { diff --git a/tests/code-first-federation/app.module.ts b/tests/code-first-federation/app.module.ts index 5542db0c4..3703ffbb9 100644 --- a/tests/code-first-federation/app.module.ts +++ b/tests/code-first-federation/app.module.ts @@ -3,11 +3,13 @@ import { GraphQLFederationModule } from '../../lib'; import { UserModule } from './user/user.module'; import { PostModule } from './post/post.module'; import { User } from './user/user.entity'; +import { RecipeModule } from './recipe/recipe.module'; @Module({ imports: [ UserModule, PostModule, + RecipeModule, GraphQLFederationModule.forRoot({ debug: false, autoSchemaFile: true, diff --git a/tests/code-first-federation/recipe/irecipe.resolver.ts b/tests/code-first-federation/recipe/irecipe.resolver.ts new file mode 100644 index 000000000..529f260c2 --- /dev/null +++ b/tests/code-first-federation/recipe/irecipe.resolver.ts @@ -0,0 +1,10 @@ +import { Query, Resolver } from '../../../lib'; +import { IRecipe } from './recipe'; + +@Resolver((of) => IRecipe) +export class IRecipeResolver { + @Query((returns) => IRecipe) + public recipe() { + return { id: '1', title: 'Recipe', description: 'Interface description' } + } +} diff --git a/tests/code-first-federation/recipe/recipe.module.ts b/tests/code-first-federation/recipe/recipe.module.ts new file mode 100644 index 000000000..4a7ab7da9 --- /dev/null +++ b/tests/code-first-federation/recipe/recipe.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { IRecipeResolver } from './irecipe.resolver'; + +@Module({ + providers: [IRecipeResolver], +}) +export class RecipeModule {} diff --git a/tests/code-first-federation/recipe/recipe.ts b/tests/code-first-federation/recipe/recipe.ts new file mode 100644 index 000000000..c2ce533ff --- /dev/null +++ b/tests/code-first-federation/recipe/recipe.ts @@ -0,0 +1,28 @@ +import { + Field, + ID, + InterfaceType, + ObjectType, +} from '../../../lib'; + +@InterfaceType() +export abstract class Base { + @Field((type) => ID) + id: string; +} + +@InterfaceType({ + resolveType: (value) => { + return Recipe; + }, +}) +export abstract class IRecipe extends Base { + @Field() + title: string; +} + +@ObjectType({ implements: IRecipe }) +export class Recipe extends IRecipe { + @Field() + description: string; +} diff --git a/tests/e2e/code-first-federation.spec.ts b/tests/e2e/code-first-federation.spec.ts index 946ceb778..b02f36a67 100644 --- a/tests/e2e/code-first-federation.spec.ts +++ b/tests/e2e/code-first-federation.spec.ts @@ -32,7 +32,7 @@ describe('Code-first - Federation', () => { expect(response.data).toEqual({ _service: { sdl: - '"""Search result description"""\nunion FederationSearchResultUnion = Post | User\n\ntype Post @key(fields: "id") {\n id: ID!\n title: String!\n authorId: Int!\n}\n\ntype Query {\n findPost(id: Float!): Post!\n getPosts: [Post!]!\n search: [FederationSearchResultUnion!]! @deprecated(reason: "test")\n}\n\ntype User @extends @key(fields: "id") {\n id: ID! @external\n posts: [Post!]!\n}\n', + '"""Search result description"""\nunion FederationSearchResultUnion = Post | User\n\ninterface IRecipe {\n id: ID!\n title: String!\n}\n\ntype Post @key(fields: "id") {\n id: ID!\n title: String!\n authorId: Int!\n}\n\ntype Query {\n findPost(id: Float!): Post!\n getPosts: [Post!]!\n search: [FederationSearchResultUnion!]! @deprecated(reason: "test")\n recipe: IRecipe!\n}\n\ntype Recipe implements IRecipe {\n id: ID!\n title: String!\n description: String!\n}\n\ntype User @extends @key(fields: "id") {\n id: ID! @external\n posts: [Post!]!\n}\n', }, }); }); @@ -67,6 +67,30 @@ describe('Code-first - Federation', () => { }); }); + it(`should return query result`, async () => { + const response = await apolloClient.query({ + query: gql` + { + recipe { + id + title + ... on Recipe { + description + } + } + } + `, + }); + expect(response.data).toEqual({ + recipe: + { + id: '1', + title: 'Recipe', + description: 'Interface description', + }, + }); + }); + afterEach(async () => { await app.close(); }); From 549dd5d8d4c2fb780232299cb646bb167c1c307e Mon Sep 17 00:00:00 2001 From: Thomas Blommaert Date: Sun, 21 Mar 2021 11:50:01 +0100 Subject: [PATCH 3/4] style: remove unused comment --- lib/interfaces/gql-module-options.interface.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/interfaces/gql-module-options.interface.ts b/lib/interfaces/gql-module-options.interface.ts index df0138bc7..11dff2b4f 100644 --- a/lib/interfaces/gql-module-options.interface.ts +++ b/lib/interfaces/gql-module-options.interface.ts @@ -66,9 +66,6 @@ export interface GqlModuleOptions * Apply `transformSchema` to the `autoSchemaFile` */ transformAutoSchemaFile?: boolean; - /** - * Pass a custom apollo server - */ } export interface GqlOptionsFactory { From 41ad53a91a04ce1ece0b2e75b7d36d90d6fc862d Mon Sep 17 00:00:00 2001 From: Thomas Blommaert Date: Sun, 21 Mar 2021 17:31:42 +0100 Subject: [PATCH 4/4] fix: add field directives on interface types --- .../factories/interface-definition.factory.ts | 22 ++++++++++++++----- tests/code-first-federation/recipe/recipe.ts | 5 +++++ tests/e2e/code-first-federation.spec.ts | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/schema-builder/factories/interface-definition.factory.ts b/lib/schema-builder/factories/interface-definition.factory.ts index 4f7833c2a..f468f7fb0 100644 --- a/lib/schema-builder/factories/interface-definition.factory.ts +++ b/lib/schema-builder/factories/interface-definition.factory.ts @@ -115,14 +115,15 @@ export class InterfaceDefinitionFactory { return () => { let fields: GraphQLFieldConfigMap = {}; metadata.properties.forEach((field) => { + const type = this.outputTypeFactory.create( + field.name, + field.typeFn(), + options, + field.options, + ); fields[field.schemaName] = { description: field.description, - type: this.outputTypeFactory.create( - field.name, - field.typeFn(), - options, - field.options, - ), + type, args: this.argsFactory.create(field.methodArgs, options), resolve: (root: object) => { const value = root[field.name]; @@ -131,6 +132,15 @@ export class InterfaceDefinitionFactory { : value; }, deprecationReason: field.deprecationReason, + /** + * AST node has to be manually created in order to define directives + * (more on this topic here: https://github.com/graphql/graphql-js/issues/1343) + */ + astNode: this.astDefinitionNodeFactory.createFieldNode( + field.name, + type, + field.directives, + ), extensions: { complexity: field.complexity, ...field.extensions, diff --git a/tests/code-first-federation/recipe/recipe.ts b/tests/code-first-federation/recipe/recipe.ts index c2ce533ff..3b011db69 100644 --- a/tests/code-first-federation/recipe/recipe.ts +++ b/tests/code-first-federation/recipe/recipe.ts @@ -1,4 +1,5 @@ import { + Directive, Field, ID, InterfaceType, @@ -19,6 +20,10 @@ export abstract class Base { export abstract class IRecipe extends Base { @Field() title: string; + + @Field() + @Directive('@external') + externalField: string; } @ObjectType({ implements: IRecipe }) diff --git a/tests/e2e/code-first-federation.spec.ts b/tests/e2e/code-first-federation.spec.ts index b02f36a67..fdd5b8b2d 100644 --- a/tests/e2e/code-first-federation.spec.ts +++ b/tests/e2e/code-first-federation.spec.ts @@ -32,7 +32,7 @@ describe('Code-first - Federation', () => { expect(response.data).toEqual({ _service: { sdl: - '"""Search result description"""\nunion FederationSearchResultUnion = Post | User\n\ninterface IRecipe {\n id: ID!\n title: String!\n}\n\ntype Post @key(fields: "id") {\n id: ID!\n title: String!\n authorId: Int!\n}\n\ntype Query {\n findPost(id: Float!): Post!\n getPosts: [Post!]!\n search: [FederationSearchResultUnion!]! @deprecated(reason: "test")\n recipe: IRecipe!\n}\n\ntype Recipe implements IRecipe {\n id: ID!\n title: String!\n description: String!\n}\n\ntype User @extends @key(fields: "id") {\n id: ID! @external\n posts: [Post!]!\n}\n', + '"""Search result description"""\nunion FederationSearchResultUnion = Post | User\n\ninterface IRecipe {\n id: ID!\n title: String!\n externalField: String! @external\n}\n\ntype Post @key(fields: "id") {\n id: ID!\n title: String!\n authorId: Int!\n}\n\ntype Query {\n findPost(id: Float!): Post!\n getPosts: [Post!]!\n search: [FederationSearchResultUnion!]! @deprecated(reason: "test")\n recipe: IRecipe!\n}\n\ntype Recipe implements IRecipe {\n id: ID!\n title: String!\n externalField: String! @external\n description: String!\n}\n\ntype User @extends @key(fields: "id") {\n id: ID! @external\n posts: [Post!]!\n}\n', }, }); });