From 4a6c2ee5f7ba298d1eca5c6c7775ad04b0f42720 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 11 Feb 2022 20:44:28 +0100 Subject: [PATCH] [gateway] Resolve nested references in a value type resolved from a top-level resolver Fixes #731 Fixes #732 Signed-off-by: Matteo Collina --- lib/gateway.js | 38 ++++++-- lib/gateway/make-resolver.js | 5 + test/gateway/type-redefined.js | 172 +++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 test/gateway/type-redefined.js diff --git a/lib/gateway.js b/lib/gateway.js index a40c33e2..517a5891 100644 --- a/lib/gateway.js +++ b/lib/gateway.js @@ -144,6 +144,7 @@ function defineResolvers (schema, typeToServiceMap, serviceMap, typeFieldsToServ * - Or there is a service for the type and a service for the field type and both refer to the same service. */ field.resolve = (parent, args, context, info) => parent && parent[info.path.key] + // } else if (type.name === 'Query' || type.name === 'Mutation' || type.name === 'Subscription') { } else if (serviceForType === null) { /** * If the return type of a query, subscription or mutation is a value type, its service is undefined or null, e.g. for @@ -154,18 +155,41 @@ function defineResolvers (schema, typeToServiceMap, serviceMap, typeFieldsToServ * In these cases, we get the service from the typeFieldsToService map. */ let service = serviceMap[typeFieldsToService[`${type}-${fieldName}`]] - if (!service) { + if (!service && (type.name === 'Query' || type.name === 'Mutation' || type.name === 'Subscription')) { /** * If there is no service for the type, it is a query, mutation or subscription */ service = serviceMap[serviceForFieldType] } if (!service) { - /** - * If the type is a nested value type, the service can still be null or undefined. - * In these cases, we resolve from the parent. - */ - field.resolve = (parent, args, context, info) => parent && parent[info.path.key] + service = serviceMap[serviceForFieldType] + + if (!service) { + /** + * If the type is a nested value type, the service can still be null or undefined. + * In these cases, we resolve from the parent. + */ + field.resolve = (parent, args, context, info) => parent && parent[info.path.key] + } else { + const isNonNull = field.astNode.type.kind === Kind.NON_NULL_TYPE + const leafKind = isNonNull ? field.astNode.type.type.kind : field.astNode.type.kind + + if (leafKind === Kind.LIST_TYPE) { + field.resolve = makeResolver({ + service, + createOperation: createEntityReferenceResolverOperation, + transformData: response => response ? response.json.data._entities : (isNonNull ? [] : null), + isReference: true + }) + } else { + field.resolve = makeResolver({ + service, + createOperation: createEntityReferenceResolverOperation, + transformData: response => response.json.data._entities[0], + isReference: true + }) + } + } } else if (type.name === 'Subscription') { field.subscribe = makeResolver({ service, @@ -183,7 +207,7 @@ function defineResolvers (schema, typeToServiceMap, serviceMap, typeFieldsToServ isQuery: true }) } - } else if (serviceForFieldType !== null && serviceForFieldType !== serviceForType) { + } else if (serviceForType && serviceForFieldType !== null && serviceForFieldType !== serviceForType) { /** * If there is a service for the field type and a service for the type and it is not the same service, * it is an entity diff --git a/lib/gateway/make-resolver.js b/lib/gateway/make-resolver.js index 726e851d..0e0db571 100644 --- a/lib/gateway/make-resolver.js +++ b/lib/gateway/make-resolver.js @@ -121,6 +121,7 @@ function collectServiceTypeFields (selections, service, type, schema) { } function createQueryOperation ({ fieldName, selections, variableDefinitions, args, fragments, operation }) { + // console.log('operation', operation) return { kind: Kind.DOCUMENT, definitions: [{ @@ -395,10 +396,13 @@ function makeResolver ({ service, createOperation, transformData, isQuery, isRef schema } = info + // console.log('==========================================================') + if (isReference && !parent[fieldName]) return null const queryId = generatePathKey(info.path).join('.') const resolverKey = queryId.replace(/\d/g, '_IDX_') + // console.log(queryId, 'isQuery', isQuery, 'isReference', isReference) const { reply, __currentQuery, lruGatewayResolvers, pubsub } = context const cached = lruGatewayResolvers.get(`${__currentQuery}_${resolverKey}`) @@ -437,6 +441,7 @@ function makeResolver ({ service, createOperation, transformData, isQuery, isRef }) query = print(operation) + // console.log(query) // check if fragments are used in the original query const usedFragments = getFragmentNamesInSelection(selections) diff --git a/test/gateway/type-redefined.js b/test/gateway/type-redefined.js new file mode 100644 index 00000000..1b839070 --- /dev/null +++ b/test/gateway/type-redefined.js @@ -0,0 +1,172 @@ +'use strict' + +const { test } = require('tap') +const Fastify = require('fastify') +const GQL = require('../..') + +const users = { + 1: { + id: 1, + name: 'John', + username: '@john' + }, + 2: { + id: 2, + name: 'Jane', + username: '@jane' + } +} + +async function buildService () { + const app = Fastify() + const schema = ` + extend type Query { + me: User + } + + type PageInfo { + edges: [User] + } + + type User @key(fields: "id") { + id: ID! + name: String + username: String + } + ` + + const resolvers = { + Query: { + me: () => { + return users['1'] + } + } + } + + const loaders = { + User: { + async __resolveReference (queries, { reply }) { + return queries.map(({ obj }) => users[obj.id]) + } + } + } + + app.register(GQL, { + schema, + resolvers, + loaders, + federationMetadata: true, + allowBatchedQueries: true + }) + + return app +} + +async function buildServiceExternal () { + const app = Fastify() + const schema = ` + extend type Query { + meWrap: PageInfo + meWrapDifferentName: PageInfoRenamed + } + + type PageInfoRenamed { + edges: [User] + } + + type PageInfo { + edges: [User] + } + + type User @key(fields: "id") @extends { + id: ID! @external + } + ` + + const resolvers = { + Query: { + meWrap: () => { + return { edges: [{ id: '1', __typename: 'User' }] } + }, + meWrapDifferentName: () => { + return { edges: [{ id: '1', __typename: 'User' }] } + } + } + } + + app.register(GQL, { + schema, + resolvers, + federationMetadata: true, + allowBatchedQueries: true + }) + + return app +} + +async function buildProxy (port1, port2) { + const proxy = Fastify() + + proxy.register(GQL, { + graphiql: true, + gateway: { + services: [ + { + name: 'ext1', + url: `http://localhost:${port1}/graphql` + }, + { + name: 'ext2', + url: `http://localhost:${port2}/graphql` + } + ] + }, + pollingInterval: 2000 + }) + + return proxy +} + +test('federated node should be able to redefine type', async (t) => { + const port1 = 3027 + const serviceOne = await buildService() + await serviceOne.listen(port1) + t.teardown(() => { serviceOne.close() }) + + const port2 = 3028 + const serviceTwo = await buildServiceExternal() + await serviceTwo.listen(port2) + t.teardown(() => { serviceTwo.close() }) + + const serviceProxy = await buildProxy(port1, port2) + await serviceProxy.ready() + t.teardown(() => { serviceProxy.close() }) + + { + const res = await serviceProxy.inject({ + method: 'POST', + url: '/graphql', + body: { + query: `{ + meWrap { edges { name } } + }` + } + }) + + t.same(res.json(), { data: { meWrap: { edges: [{ name: 'John' }] } } }) + } + + { + const res = await serviceProxy.inject({ + method: 'POST', + url: '/graphql', + body: { + query: `{ + meWrapDifferentName { edges { name } } + }` + } + }) + + t.same(res.json(), { data: { meWrapDifferentName: { edges: [{ name: 'John' }] } } }) + } +})