Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mercurius

Package Overview
Dependencies
Maintainers
2
Versions
116
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mercurius - npm Package Compare versions

Comparing version 8.10.0 to 8.11.0

lib/gateway/get-query-result.js

55

docs/federation.md
# 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'
}
}]
}
})
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc