Skip to content

Commit

Permalink
[gateway] Resolve nested references in a value type resolved from a t…
Browse files Browse the repository at this point in the history
…op-level resolver

Fixes #731
Fixes #732

Signed-off-by: Matteo Collina <hello@matteocollina.com>
  • Loading branch information
mcollina committed Feb 11, 2022
1 parent 2ec084d commit 4a6c2ee
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 7 deletions.
38 changes: 31 additions & 7 deletions lib/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/gateway/make-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [{
Expand Down Expand Up @@ -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}`)
Expand Down Expand Up @@ -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)
Expand Down
172 changes: 172 additions & 0 deletions test/gateway/type-redefined.js
Original file line number Diff line number Diff line change
@@ -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' }] } } })
}
})

0 comments on commit 4a6c2ee

Please sign in to comment.