mercurius
Advanced tools
Comparing version 8.10.0 to 8.11.0
# mercurius | ||
- [Federation metadata support](#federation-metadata-support) | ||
- [Federation with \_\_resolveReference caching](#federation-with-__resolvereference-caching) | ||
- [Use GraphQL server as a Gateway for federated schemas](#use-graphql-server-as-a-gateway-for-federated-schemas) | ||
- [Periodically refresh federated schemas in Gateway mode](#periodically-refresh-federated-schemas-in-gateway-mode) | ||
- [Programmatically refresh federated schemas in Gateway mode](#programmatically-refresh-federated-schemas-in-gateway-mode) | ||
- [Using Gateway mode with a schema registry](#using-gateway-mode-with-a-schema-registry) | ||
- [Flag service as mandatory in Gateway mode](#flag-service-as-mandatory-in-gateway-mode) | ||
- [Using a custom errorHandler for handling downstream service errors in Gateway mode](#using-a-custom-errorhandler-for-handling-downstream-service-errors-in-gateway-mode) | ||
- [mercurius](#mercurius) | ||
- [Federation](#federation) | ||
- [Federation metadata support](#federation-metadata-support) | ||
- [Federation with \_\_resolveReference caching](#federation-with-__resolvereference-caching) | ||
- [Use GraphQL server as a Gateway for federated schemas](#use-graphql-server-as-a-gateway-for-federated-schemas) | ||
- [Periodically refresh federated schemas in Gateway mode](#periodically-refresh-federated-schemas-in-gateway-mode) | ||
- [Programmatically refresh federated schemas in Gateway mode](#programmatically-refresh-federated-schemas-in-gateway-mode) | ||
- [Using Gateway mode with a schema registry](#using-gateway-mode-with-a-schema-registry) | ||
- [Flag service as mandatory in Gateway mode](#flag-service-as-mandatory-in-gateway-mode) | ||
- [Batched Queries to services](#batched-queries-to-services) | ||
- [Using a custom errorHandler for handling downstream service errors in Gateway mode](#using-a-custom-errorhandler-for-handling-downstream-service-errors-in-gateway-mode) | ||
- [Securely parse service responses in Gateway mode](#securely-parse-service-responses-in-gateway-mode) | ||
@@ -354,2 +358,37 @@ ## Federation | ||
#### Batched Queries to services | ||
To fully leverage the DataLoader pattern we can tell the Gateway which of its services support [batched queries](batched-queries.md). | ||
In this case the service will receive a request body with an array of queries to execute. | ||
Enabling batched queries for a service that doesn't support it will generate errors. | ||
```js | ||
const Fastify = require('fastify') | ||
const mercurius = require('mercurius') | ||
const server = Fastify() | ||
server.register(mercurius, { | ||
graphiql: true, | ||
gateway: { | ||
services: [ | ||
{ | ||
name: 'user', | ||
url: 'http://localhost:3000/graphql' | ||
allowBatchedQueries: true | ||
}, | ||
{ | ||
name: 'company', | ||
url: 'http://localhost:3001/graphql', | ||
allowBatchedQueries: false | ||
} | ||
] | ||
}, | ||
pollingInterval: 2000 | ||
}) | ||
server.listen(3002) | ||
``` | ||
#### Using a custom errorHandler for handling downstream service errors in Gateway mode | ||
@@ -356,0 +395,0 @@ |
@@ -24,4 +24,4 @@ # mercurius | ||
Dog: { | ||
async owner(queries, { reply }) { | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
async owner (queries, { reply }) { | ||
return queries.map(({ obj, params }) => owners[obj.name]) | ||
} | ||
@@ -44,4 +44,7 @@ } | ||
owner: { | ||
async loader(queries, { reply }) { | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
async loader (queries, { reply }) { | ||
return queries.map(({ obj, params, info }) => { | ||
// info is available only if the loader is not cached | ||
owners[obj.name] | ||
}) | ||
}, | ||
@@ -62,2 +65,24 @@ opts: { | ||
Alternatively, globally disabling caching also disable the Loader cache: | ||
```js | ||
const loaders = { | ||
Dog: { | ||
async owner (queries, { reply }) { | ||
return queries.map(({ obj, params, info }) => { | ||
// info is available only if the loader is not cached | ||
owners[obj.name] | ||
}) | ||
} | ||
} | ||
} | ||
app.register(mercurius, { | ||
schema, | ||
resolvers, | ||
loaders, | ||
cache: false | ||
}) | ||
``` | ||
Disabling caching has the advantage to avoid the serialization at | ||
@@ -64,0 +89,0 @@ the cost of more objects to fetch in the resolvers. |
@@ -12,2 +12,3 @@ # Plugins | ||
- [mercurius-apollo-tracing](#mercurius-apollo-tracing) | ||
- [mercurius-postgraphile](#mercurius-postgraphile) | ||
@@ -122,1 +123,6 @@ ## mercurius-auth | ||
``` | ||
## mercurius-postgraphile | ||
A Mercurius plugin for integrating PostGraphile schemas with Mercurius | ||
Check [https://github.com/autotelic/mercurius-postgraphile](https://github.com/autotelic/mercurius-postgraphile) for usage and readme. |
30
index.js
@@ -31,2 +31,3 @@ 'use strict' | ||
const persistedQueryDefaults = require('./lib/persistedQueryDefaults') | ||
const stringify = require('safe-stable-stringify') | ||
const { | ||
@@ -370,5 +371,6 @@ ErrorWithProps, | ||
function defineLoader (name) { | ||
function defineLoader (name, opts) { | ||
// async needed because of throw | ||
return async function (obj, params, { reply }, info) { | ||
return async function (obj, params, ctx, info) { | ||
const { reply } = ctx | ||
if (!reply) { | ||
@@ -384,2 +386,9 @@ throw new MER_ERR_INVALID_OPTS('loaders only work via reply.graphql()') | ||
function serialize (query) { | ||
if (query.info) { | ||
return stringify({ obj: query.obj, params: query.params }) | ||
} | ||
return query | ||
} | ||
const resolvers = {} | ||
@@ -391,9 +400,16 @@ for (const typeKey of Object.keys(loaders)) { | ||
const name = typeKey + '-' + prop | ||
resolvers[typeKey][prop] = defineLoader(name) | ||
const toAssign = [{}, type[prop].opts || {}] | ||
if (opts.cache === false) { | ||
toAssign.push({ | ||
cache: false | ||
}) | ||
} | ||
const factoryOpts = Object.assign(...toAssign) | ||
resolvers[typeKey][prop] = defineLoader(name, factoryOpts) | ||
if (typeof type[prop] === 'function') { | ||
factory.add(name, type[prop]) | ||
subscriptionFactory.add(name, { cache: false }, type[prop]) | ||
factory.add(name, factoryOpts, type[prop], serialize) | ||
subscriptionFactory.add(name, { cache: false }, type[prop], serialize) | ||
} else { | ||
factory.add(name, type[prop].opts, type[prop].loader) | ||
subscriptionFactory.add(name, Object.assign({}, type[prop].opts, { cache: false }), type[prop].loader) | ||
factory.add(name, factoryOpts, type[prop].loader, serialize) | ||
subscriptionFactory.add(name, Object.assign({}, type[prop].opts, { cache: false }), type[prop].loader, serialize) | ||
} | ||
@@ -400,0 +416,0 @@ } |
@@ -7,4 +7,3 @@ 'use strict' | ||
isScalarType, | ||
Kind, | ||
parse | ||
Kind | ||
} = require('graphql') | ||
@@ -22,5 +21,4 @@ const { Factory } = require('single-user-cache') | ||
const { MER_ERR_GQL_GATEWAY_REFRESH, MER_ERR_GQL_GATEWAY_INIT } = require('./errors') | ||
const { preGatewayExecutionHandler } = require('./handlers') | ||
const findValueTypes = require('./gateway/find-value-types') | ||
const getQueryResult = require('./gateway/get-query-result') | ||
const allSettled = require('promise.allsettled') | ||
@@ -359,69 +357,9 @@ | ||
factory.add(`${service}Entity`, async (queries) => { | ||
const q = [...new Set(queries.map(q => q.query))] | ||
// context is the same for each query, but unfortunately it's not acessible from onRequest | ||
// where we do factory.create(). What is a cleaner option? | ||
const context = queries[0].context | ||
const result = await getQueryResult({ | ||
context, queries, serviceDefinition, service | ||
}) | ||
const resultIndexes = [] | ||
let queryIndex = 0 | ||
const mergedQueries = queries.reduce((acc, curr) => { | ||
if (!acc[curr.query]) { | ||
acc[curr.query] = curr.variables | ||
resultIndexes[q.indexOf(curr.query)] = [] | ||
} else { | ||
acc[curr.query].representations = [ | ||
...acc[curr.query].representations, | ||
...curr.variables.representations | ||
] | ||
} | ||
for (let i = 0; i < curr.variables.representations.length; i++) { | ||
resultIndexes[q.indexOf(curr.query)].push(queryIndex) | ||
} | ||
queryIndex++ | ||
return acc | ||
}, {}) | ||
const result = [] | ||
// Gateway query here | ||
await Promise.all(Object.entries(mergedQueries).map(async ([query, variables], queryIndex, entries) => { | ||
// Trigger preGatewayExecution hook for entities | ||
let modifiedQuery | ||
if (queries[queryIndex].context.preGatewayExecution !== null) { | ||
({ modifiedQuery } = await preGatewayExecutionHandler({ | ||
schema: serviceDefinition.schema, | ||
document: parse(query), | ||
context: queries[queryIndex].context, | ||
service: { name: service } | ||
})) | ||
} | ||
const response = await serviceDefinition.sendRequest({ | ||
originalRequestHeaders: queries[queryIndex].originalRequestHeaders, | ||
body: JSON.stringify({ | ||
query: modifiedQuery || query, | ||
variables | ||
}), | ||
context: queries[queryIndex].context | ||
}) | ||
let entityIndex = 0 | ||
for (const entity of response.json.data._entities) { | ||
if (!result[resultIndexes[queryIndex][entityIndex]]) { | ||
result[resultIndexes[queryIndex][entityIndex]] = { | ||
...response, | ||
json: { | ||
data: { | ||
_entities: [entity] | ||
} | ||
} | ||
} | ||
} else { | ||
result[resultIndexes[queryIndex][entityIndex]].json.data._entities.push(entity) | ||
} | ||
entityIndex++ | ||
} | ||
})) | ||
return result | ||
@@ -428,0 +366,0 @@ }, query => query.id) |
@@ -511,6 +511,8 @@ 'use strict' | ||
// This method is declared in gateway.js inside of onRequest | ||
// hence it's unique per request. | ||
const response = await entityResolvers[`${service.name}Entity`]({ | ||
document: operation, | ||
query, | ||
variables, | ||
originalRequestHeaders: reply.request.headers, | ||
context, | ||
@@ -517,0 +519,0 @@ id: queryId |
@@ -224,2 +224,3 @@ 'use strict' | ||
serviceMap[service.name].name = service.name | ||
serviceMap[service.name].allowBatchedQueries = service.allowBatchedQueries | ||
} | ||
@@ -226,0 +227,0 @@ |
{ | ||
"name": "mercurius", | ||
"version": "8.10.0", | ||
"version": "8.11.0", | ||
"description": "Fastify GraphQL adapter with gateway and subscription support", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -137,3 +137,3 @@ 'use strict' | ||
errorFormatter: (_execution, context) => { | ||
t.include(context, { topic: 'NOTIFICATIONS_ADDED' }) | ||
t.has(context, { topic: 'NOTIFICATIONS_ADDED' }) | ||
return { | ||
@@ -140,0 +140,0 @@ response: { |
@@ -17,3 +17,3 @@ 'use strict' | ||
name: 'Max' | ||
}] | ||
}].map(Object.freeze) | ||
@@ -91,3 +91,5 @@ const owners = { | ||
}]) | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
} | ||
@@ -147,3 +149,6 @@ } | ||
// note that the second entry for max is NOT cached | ||
t.same(queries, [{ | ||
const found = queries.map((q) => { | ||
return { obj: q.obj, params: q.params } | ||
}) | ||
t.same(found, [{ | ||
obj: { | ||
@@ -169,3 +174,5 @@ name: 'Max' | ||
}]) | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
}, | ||
@@ -227,3 +234,5 @@ opts: { | ||
async owner (queries, { reply }) { | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
} | ||
@@ -293,3 +302,5 @@ } | ||
t.equal(context.app, app) | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
} | ||
@@ -449,3 +460,5 @@ } | ||
async owner (queries) { | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
} | ||
@@ -507,3 +520,5 @@ } | ||
}]) | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
} | ||
@@ -576,3 +591,3 @@ } | ||
Dog: { | ||
owner: async () => [owners[dogs[0].name]] | ||
owner: async () => [{ ...owners[dogs[0].name] }] | ||
} | ||
@@ -620,6 +635,6 @@ }, | ||
topic: 'PINGED_DOG', | ||
payload: { onPingDog: dogs[0] } | ||
payload: { onPingDog: { ...dogs[0] } } | ||
}) | ||
} else if (data.id === 1) { | ||
const expectedDog = dogs[0] | ||
const expectedDog = { ...dogs[0] } | ||
expectedDog.owner = owners[dogs[0].name] | ||
@@ -733,43 +748,53 @@ | ||
Dog: { | ||
async owner (queries, context) { | ||
t.equal(context.app, app) | ||
return queries.map(({ obj, info }) => { | ||
// verify info properties | ||
t.equal(info.operation.operation, 'query') | ||
owner: { | ||
async loader (queries, context) { | ||
t.equal(context.app, app) | ||
return queries.map(({ obj, info }) => { | ||
// verify info properties | ||
t.equal(info.operation.operation, 'query') | ||
const resolverOutputParams = info.operation.selectionSet.selections[0].selectionSet.selections | ||
t.equal(resolverOutputParams.length, 3) | ||
t.equal(resolverOutputParams[0].name.value, 'dogName') | ||
t.equal(resolverOutputParams[1].name.value, 'age') | ||
t.equal(resolverOutputParams[2].name.value, 'owner') | ||
const resolverOutputParams = info.operation.selectionSet.selections[0].selectionSet.selections | ||
t.equal(resolverOutputParams.length, 3) | ||
t.equal(resolverOutputParams[0].name.value, 'dogName') | ||
t.equal(resolverOutputParams[1].name.value, 'age') | ||
t.equal(resolverOutputParams[2].name.value, 'owner') | ||
const loaderOutputParams = resolverOutputParams[2].selectionSet.selections | ||
const loaderOutputParams = resolverOutputParams[2].selectionSet.selections | ||
t.equal(loaderOutputParams.length, 2) | ||
t.equal(loaderOutputParams[0].name.value, 'nickName') | ||
t.equal(loaderOutputParams[1].name.value, 'age') | ||
t.equal(loaderOutputParams.length, 2) | ||
t.equal(loaderOutputParams[0].name.value, 'nickName') | ||
t.equal(loaderOutputParams[1].name.value, 'age') | ||
return owners[obj.dogName] | ||
}) | ||
return { ...owners[obj.dogName] } | ||
}) | ||
}, | ||
opts: { | ||
cache: false | ||
} | ||
} | ||
}, | ||
Cat: { | ||
async owner (queries, context) { | ||
t.equal(context.app, app) | ||
return queries.map(({ obj, info }) => { | ||
// verify info properties | ||
t.equal(info.operation.operation, 'query') | ||
owner: { | ||
async loader (queries, context) { | ||
t.equal(context.app, app) | ||
return queries.map(({ obj, info }) => { | ||
// verify info properties | ||
t.equal(info.operation.operation, 'query') | ||
const resolverOutputParams = info.operation.selectionSet.selections[1].selectionSet.selections | ||
t.equal(resolverOutputParams.length, 2) | ||
t.equal(resolverOutputParams[0].name.value, 'catName') | ||
t.equal(resolverOutputParams[1].name.value, 'owner') | ||
const resolverOutputParams = info.operation.selectionSet.selections[1].selectionSet.selections | ||
t.equal(resolverOutputParams.length, 2) | ||
t.equal(resolverOutputParams[0].name.value, 'catName') | ||
t.equal(resolverOutputParams[1].name.value, 'owner') | ||
const loaderOutputParams = resolverOutputParams[1].selectionSet.selections | ||
const loaderOutputParams = resolverOutputParams[1].selectionSet.selections | ||
t.equal(loaderOutputParams.length, 1) | ||
t.equal(loaderOutputParams[0].name.value, 'age') | ||
t.equal(loaderOutputParams.length, 1) | ||
t.equal(loaderOutputParams[0].name.value, 'age') | ||
return owners[obj.catName] | ||
}) | ||
return { ...owners[obj.catName] } | ||
}) | ||
}, | ||
opts: { | ||
cache: false | ||
} | ||
} | ||
@@ -782,4 +807,3 @@ } | ||
resolvers, | ||
loaders, | ||
cache: false | ||
loaders | ||
}) | ||
@@ -873,3 +897,5 @@ | ||
t.equal(queries[0].info, undefined) | ||
return queries.map(({ obj }) => owners[obj.name]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
} | ||
@@ -924,1 +950,82 @@ } | ||
}) | ||
test('loaders create batching resolvers', { only: true }, async (t) => { | ||
const app = Fastify() | ||
const loaders = { | ||
Dog: { | ||
async owner (queries, { reply }) { | ||
// note that the second entry for max is cached | ||
const found = queries.map((q) => { | ||
return { obj: q.obj, params: q.params } | ||
}) | ||
t.same(found, [{ | ||
obj: { | ||
name: 'Max' | ||
}, | ||
params: {} | ||
}, { | ||
obj: { | ||
name: 'Charlie' | ||
}, | ||
params: {} | ||
}, { | ||
obj: { | ||
name: 'Buddy' | ||
}, | ||
params: {} | ||
}, { | ||
obj: { | ||
name: 'Max' | ||
}, | ||
params: {} | ||
}]) | ||
return queries.map(({ obj }) => { | ||
return { ...owners[obj.name] } | ||
}) | ||
} | ||
} | ||
} | ||
app.register(GQL, { | ||
schema, | ||
resolvers, | ||
loaders, | ||
cache: false | ||
}) | ||
const res = await app.inject({ | ||
method: 'POST', | ||
url: '/graphql', | ||
body: { | ||
query | ||
} | ||
}) | ||
t.equal(res.statusCode, 200) | ||
t.same(JSON.parse(res.body), { | ||
data: { | ||
dogs: [{ | ||
name: 'Max', | ||
owner: { | ||
name: 'Jennifer' | ||
} | ||
}, { | ||
name: 'Charlie', | ||
owner: { | ||
name: 'Sarah' | ||
} | ||
}, { | ||
name: 'Buddy', | ||
owner: { | ||
name: 'Tracy' | ||
} | ||
}, { | ||
name: 'Max', | ||
owner: { | ||
name: 'Jennifer' | ||
} | ||
}] | ||
} | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
898678
138
32050