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; diff --git a/lib/schema-builder/factories/interface-definition.factory.ts b/lib/schema-builder/factories/interface-definition.factory.ts index 1ba6f2f8d..5c583a30c 100644 --- a/lib/schema-builder/factories/interface-definition.factory.ts +++ b/lib/schema-builder/factories/interface-definition.factory.ts @@ -111,14 +111,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]; @@ -127,6 +128,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/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..3b011db69 --- /dev/null +++ b/tests/code-first-federation/recipe/recipe.ts @@ -0,0 +1,33 @@ +import { + Directive, + 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; + + @Field() + @Directive('@external') + externalField: 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..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\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 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', }, }); }); @@ -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(); });