From db9307d47590f39d940054878907ad6d3a4fdb51 Mon Sep 17 00:00:00 2001 From: yaacovCR Date: Tue, 24 Sep 2019 14:45:05 -0400 Subject: [PATCH] fix(remote schemas): must add __typename to remote query to properly resolve interfaces. Error will occur only when querying directly on the remote schema, as delegateToSchema automatically adds type names. addTypenameToAbstract is necessary for resolveType to work in the parent schema just as checkResultAndHandleErrors is necessary for resolve to work! --- src/stitching/addTypenameToAbstract.ts | 56 ++++++++++++++++++++ src/stitching/makeRemoteExecutableSchema.ts | 17 ++++-- src/test/testMakeRemoteExecutableSchema.ts | 44 +++++++++++++++- src/transforms/AddTypenameToAbstract.ts | 58 +-------------------- 4 files changed, 113 insertions(+), 62 deletions(-) create mode 100644 src/stitching/addTypenameToAbstract.ts diff --git a/src/stitching/addTypenameToAbstract.ts b/src/stitching/addTypenameToAbstract.ts new file mode 100644 index 00000000000..706574b1b32 --- /dev/null +++ b/src/stitching/addTypenameToAbstract.ts @@ -0,0 +1,56 @@ +import { + GraphQLType, + DocumentNode, + TypeInfo, + visit, + visitWithTypeInfo, + SelectionSetNode, + Kind, + FieldNode, + GraphQLInterfaceType, + GraphQLUnionType, +} from 'graphql'; +import { GraphQLSchemaWithTransforms } from '../Interfaces'; + +export function addTypenameToAbstract( + targetSchema: GraphQLSchemaWithTransforms, + document: DocumentNode, +): DocumentNode { + const typeInfo = new TypeInfo(targetSchema); + return visit( + document, + visitWithTypeInfo(typeInfo, { + [Kind.SELECTION_SET]( + node: SelectionSetNode, + ): SelectionSetNode | null | undefined { + const parentType: GraphQLType = typeInfo.getParentType(); + let selections = node.selections; + if ( + parentType && + (parentType instanceof GraphQLInterfaceType || + parentType instanceof GraphQLUnionType) && + !selections.find( + _ => + (_ as FieldNode).kind === Kind.FIELD && + (_ as FieldNode).name.value === '__typename', + ) + ) { + selections = selections.concat({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }); + } + + if (selections !== node.selections) { + return { + ...node, + selections, + }; + } + }, + }), + ); +} diff --git a/src/stitching/makeRemoteExecutableSchema.ts b/src/stitching/makeRemoteExecutableSchema.ts index b4ac0506443..77f6031a5c5 100644 --- a/src/stitching/makeRemoteExecutableSchema.ts +++ b/src/stitching/makeRemoteExecutableSchema.ts @@ -8,10 +8,12 @@ import { buildSchema, Kind, GraphQLResolveInfo, - BuildSchemaOptions + BuildSchemaOptions, + DocumentNode, } from 'graphql'; import linkToFetcher, { execute } from './linkToFetcher'; import { Fetcher, Operation } from '../Interfaces'; +import { addTypenameToAbstract } from './addTypenameToAbstract'; import { checkResultAndHandleErrors } from './checkResultAndHandleErrors'; import { observableToAsyncIterable } from './observableToAsyncIterable'; import mapAsyncIterator from './mapAsyncIterator'; @@ -79,12 +81,15 @@ export default function makeRemoteExecutableSchema({ export function createResolver(fetcher: Fetcher): GraphQLFieldResolver { return async (root, args, context, info) => { const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]); - const document = { + let query: DocumentNode = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments] }; + + query = addTypenameToAbstract(info.schema, query); + const result = await fetcher({ - query: document, + query, variables: info.variableValues, context: { graphqlContext: context } }); @@ -95,13 +100,15 @@ export function createResolver(fetcher: Fetcher): GraphQLFieldResolver function createSubscriptionResolver(link: ApolloLink): ResolverFn { return (root, args, context, info) => { const fragments = Object.keys(info.fragments).map(fragment => info.fragments[fragment]); - const document = { + let query: DocumentNode = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments] }; + query = addTypenameToAbstract(info.schema, query); + const operation = { - query: document, + query, variables: info.variableValues, context: { graphqlContext: context } }; diff --git a/src/test/testMakeRemoteExecutableSchema.ts b/src/test/testMakeRemoteExecutableSchema.ts index e17ad9c0b92..0e30c64172a 100644 --- a/src/test/testMakeRemoteExecutableSchema.ts +++ b/src/test/testMakeRemoteExecutableSchema.ts @@ -2,8 +2,9 @@ import { expect } from 'chai'; import { forAwaitEach } from 'iterall'; -import { GraphQLSchema, ExecutionResult, subscribe, parse } from 'graphql'; +import { GraphQLSchema, ExecutionResult, subscribe, parse, graphql } from 'graphql'; import { + propertySchema, subscriptionSchema, subscriptionPubSubTrigger, subscriptionPubSub, @@ -11,6 +12,47 @@ import { } from '../test/testingSchemas'; import { makeRemoteExecutableSchema } from '../stitching'; +describe('remote queries', () => { + let schema: GraphQLSchema; + before(async () => { + const remoteSchemaExecConfig = await makeSchemaRemoteFromLink(propertySchema); + schema = makeRemoteExecutableSchema({ + schema: remoteSchemaExecConfig.schema, + link: remoteSchemaExecConfig.link + }); + }); + + it('should work', async () => { + const query = ` + { + interfaceTest(kind: ONE) { + kind + testString + ...on TestImpl1 { + foo + } + ...on TestImpl2 { + bar + } + } + } + `; + + const expected = { + data: { + interfaceTest: { + foo: 'foo', + kind: 'ONE', + testString: 'test', + }, + }, + }; + + const result = await graphql(schema, query); + expect(result).to.deep.equal(expected); + }); +}); + describe('remote subscriptions', () => { let schema: GraphQLSchema; before(async () => { diff --git a/src/transforms/AddTypenameToAbstract.ts b/src/transforms/AddTypenameToAbstract.ts index ef790062219..be773a65b6a 100644 --- a/src/transforms/AddTypenameToAbstract.ts +++ b/src/transforms/AddTypenameToAbstract.ts @@ -1,18 +1,7 @@ -import { - DocumentNode, - FieldNode, - GraphQLInterfaceType, - GraphQLSchema, - GraphQLType, - GraphQLUnionType, - Kind, - SelectionSetNode, - TypeInfo, - visit, - visitWithTypeInfo, -} from 'graphql'; +import { GraphQLSchema } from 'graphql'; import { Request } from '../Interfaces'; import { Transform } from './transforms'; +import { addTypenameToAbstract } from '../stitching/addTypenameToAbstract'; export default class AddTypenameToAbstract implements Transform { private targetSchema: GraphQLSchema; @@ -32,46 +21,3 @@ export default class AddTypenameToAbstract implements Transform { }; } } - -function addTypenameToAbstract( - targetSchema: GraphQLSchema, - document: DocumentNode, -): DocumentNode { - const typeInfo = new TypeInfo(targetSchema); - return visit( - document, - visitWithTypeInfo(typeInfo, { - [Kind.SELECTION_SET]( - node: SelectionSetNode, - ): SelectionSetNode | null | undefined { - const parentType: GraphQLType = typeInfo.getParentType(); - let selections = node.selections; - if ( - parentType && - (parentType instanceof GraphQLInterfaceType || - parentType instanceof GraphQLUnionType) && - !selections.find( - _ => - (_ as FieldNode).kind === Kind.FIELD && - (_ as FieldNode).name.value === '__typename', - ) - ) { - selections = selections.concat({ - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: '__typename', - }, - }); - } - - if (selections !== node.selections) { - return { - ...node, - selections, - }; - } - }, - }), - ); -}