Skip to content

Commit

Permalink
Add service metadata parameter to gateway hooks (#416)
Browse files Browse the repository at this point in the history
* Add service metadata parameter to gateway hooks

* Call the gateway hooks with the full service object in make-resolver
  • Loading branch information
jonnydgreen authored Mar 3, 2021
1 parent c326546 commit 6e5142e
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 10 deletions.
10 changes: 8 additions & 2 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@ In the `preGatewayExecution` hook, you can modify the following items by returni

This hook will only be triggered in gateway mode. When in gateway mode, each hook definition will trigger multiple times in a single request just before executing remote GraphQL queries on the federated services.

Note, this hook contains service metadata in the `service` parameter:
- `name`: service name

```js
fastify.graphql.addHook('preGatewayExecution', async (schema, document, context) => {
fastify.graphql.addHook('preGatewayExecution', async (schema, document, context, service) => {
const { modifiedDocument, errors } = await asyncMethod(document)

return {
Expand Down Expand Up @@ -188,8 +191,11 @@ fastify.graphql.addHook('preSubscriptionExecution', async (schema, document, con

This hook will only be triggered in gateway mode. When in gateway mode, each hook definition will trigger when creating a subscription with a federated service.

Note, this hook contains service metadata in the `service` parameter:
- `name`: service name

```js
fastify.graphql.addHook('preGatewaySubscriptionExecution', async (schema, document, context) => {
fastify.graphql.addHook('preGatewaySubscriptionExecution', async (schema, document, context, service) => {
await asyncMethod()
})
```
Expand Down
4 changes: 2 additions & 2 deletions examples/hooks-gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ async function start () {
}
})

gateway.graphql.addHook('preGatewayExecution', async function (schema, document, context) {
console.log('preGatewayExecution called')
gateway.graphql.addHook('preGatewayExecution', async function (schema, document, context, service) {
console.log('preGatewayExecution called', service.name)
return {
document,
errors: [
Expand Down
15 changes: 15 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export interface MercuriusLoaders<TContext extends Record<string, any> = Mercuri
};
}

/**
* Federated GraphQL Service metadata
*/
export interface MercuriusServiceMetadata {
name: string;
}

// ------------------------
// Request Lifecycle hooks
// ------------------------
Expand Down Expand Up @@ -108,12 +115,16 @@ export interface preExecutionHookHandler<TContext = MercuriusContext, TError ext
* - `document`
* - `errors`
* This hook will only be triggered in gateway mode. When in gateway mode, each hook definition will trigger multiple times in a single request just before executing remote GraphQL queries on the federated services.
*
* Because it is a gateway hook, this hook contains service metadata in the `service` parameter:
* - `name`: service name
*/
export interface preGatewayExecutionHookHandler<TContext = MercuriusContext, TError extends Error = Error> {
(
schema: GraphQLSchema,
source: DocumentNode,
context: TContext,
service: MercuriusServiceMetadata
): Promise<PreExecutionHookResponse<TError> | void>;
}

Expand Down Expand Up @@ -158,12 +169,16 @@ export interface preSubscriptionExecutionHookHandler<TContext = MercuriusContext
/**
* `preGatewaySubscriptionExecution` is the third hook to be executed in the GraphQL subscription lifecycle. The previous hook was `preSubscriptionExecution`, the next hook will be `onSubscriptionResolution`.
* This hook will only be triggered in gateway mode when subscriptions are enabled.
*
* Because it is a gateway hook, this hook contains service metadata in the `service` parameter:
* - `name`: service name
*/
export interface preGatewaySubscriptionExecutionHookHandler<TContext = MercuriusContext> {
(
schema: GraphQLSchema,
source: DocumentNode,
context: TContext,
service: MercuriusServiceMetadata
): Promise<void>;
}

Expand Down
3 changes: 2 additions & 1 deletion lib/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ async function buildGateway (gatewayOpts, app) {
({ modifiedQuery } = await preGatewayExecutionHandler({
schema: serviceDefinition.schema,
document: parse(query),
context: queries[queryIndex].context
context: queries[queryIndex].context,
service: { name: service }
}))
}

Expand Down
4 changes: 2 additions & 2 deletions lib/gateway/make-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ function makeResolver ({ service, createOperation, transformData, isQuery, isRef
if (isSubscription) {
// Trigger preGatewaySubscriptionExecution hook
if (context.preGatewaySubscriptionExecution !== null) {
await preGatewaySubscriptionExecutionHandler({ schema, document: operation, context })
await preGatewaySubscriptionExecutionHandler({ schema, document: operation, context, service })
}
const subscriptionId = service.createSubscription(query, variables, pubsub.publish.bind(pubsub), context._connectionInit)
return pubsub.subscribe(`${service.name}_${subscriptionId}`)
Expand All @@ -452,7 +452,7 @@ function makeResolver ({ service, createOperation, transformData, isQuery, isRef
// Trigger preGatewayExecution hook
let modifiedQuery
if (context.preGatewayExecution !== null) {
({ modifiedQuery } = await preGatewayExecutionHandler({ schema, document: operation, context }))
({ modifiedQuery } = await preGatewayExecutionHandler({ schema, document: operation, context, service }))
}

const response = await service.sendRequest({
Expand Down
6 changes: 3 additions & 3 deletions lib/handlers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const { hooksRunner, preExecutionHooksRunner, hookRunner, preParsingHookRunner, onResolutionHookRunner, onEndHookRunner } = require('./hooks')
const { hooksRunner, preExecutionHooksRunner, preGatewayExecutionHooksRunner, gatewayHookRunner, hookRunner, preParsingHookRunner, onResolutionHookRunner, onEndHookRunner } = require('./hooks')
const { addErrorsToContext } = require('./errors')
const { print } = require('graphql')

Expand Down Expand Up @@ -35,7 +35,7 @@ async function preExecutionHandler (request) {
}

async function preGatewayExecutionHandler (request) {
const { errors, modifiedDocument } = await preExecutionHooksRunner(
const { errors, modifiedDocument } = await preGatewayExecutionHooksRunner(
request.context.preGatewayExecution,
request
)
Expand Down Expand Up @@ -75,7 +75,7 @@ async function preSubscriptionExecutionHandler (request) {
async function preGatewaySubscriptionExecutionHandler (request) {
await hooksRunner(
request.context.preGatewaySubscriptionExecution,
hookRunner,
gatewayHookRunner,
request
)
}
Expand Down
26 changes: 26 additions & 0 deletions lib/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,34 @@ async function preExecutionHooksRunner (functions, request) {
return { errors, modifiedDocument }
}

async function preGatewayExecutionHooksRunner (functions, request) {
let errors = []
let modifiedDocument

for (const fn of functions) {
const result = await fn(request.schema, modifiedDocument || request.document, request.context, request.service)

if (result) {
if (typeof result.document !== 'undefined') {
modifiedDocument = result.document
}
if (typeof result.errors !== 'undefined') {
errors = errors.concat(result.errors)
}
}
}

return { errors, modifiedDocument }
}

function hookRunner (fn, request) {
return fn(request.schema, request.document, request.context)
}

function gatewayHookRunner (fn, request) {
return fn(request.schema, request.document, request.context, request.service)
}

function preParsingHookRunner (fn, request) {
return fn(request.schema, request.source, request.context)
}
Expand All @@ -114,6 +138,8 @@ module.exports = {
hooksRunner,
preExecutionHooksRunner,
hookRunner,
gatewayHookRunner,
preGatewayExecutionHooksRunner,
preParsingHookRunner,
onResolutionHookRunner,
onEndHookRunner,
Expand Down
64 changes: 64 additions & 0 deletions test/gateway/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,70 @@ test('gateway - preGatewayExecution hooks should be able to modify the request d
})
})

test('gateway - preGatewayExecution hooks should contain service metadata', async (t) => {
t.plan(21)
const app = await createTestGatewayServer(t)

// Execution events:
// - user service: once for user service query
// - post service: once for post service query
// - post service: once for reference type topPosts on User
// - user service: once for reference type author on Post
app.graphql.addHook('preGatewayExecution', async function (schema, document, context, service) {
await immediate()
t.type(schema, GraphQLSchema)
t.type(document, 'object')
t.type(context, 'object')
if (typeof service === 'object' && service.name === 'user') {
t.is(service.name, 'user')
} else if (typeof service === 'object' && service.name === 'post') {
t.is(service.name, 'post')
} else {
t.fail('service metadata should be correctly populated')
return
}
t.ok('preGatewayExecution called')
})

const res = await app.inject({
method: 'POST',
headers: { 'content-type': 'application/json' },
url: '/graphql',
body: JSON.stringify({ query })
})

t.deepEqual(JSON.parse(res.body), {
data: {
me: {
id: 'u1',
name: 'John',
topPosts: [
{
pid: 'p1',
author: {
id: 'u1'
}
},
{
pid: 'p3',
author: {
id: 'u1'
}
}
]
},
topPosts: [
{
pid: 'p1'
},
{
pid: 'p2'
}
]
}
})
})

// -------------
// onResolution
// -------------
Expand Down
80 changes: 80 additions & 0 deletions test/gateway/subscription-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,86 @@ test('gateway - preGatewaySubscriptionExecution hooks should handle errors', asy
}
})

test('gateway subscription - preGatewaySubscriptionExecution hooks should contain service metadata', async t => {
t.plan(8)
const gateway = await createTestGatewayServer(t)

const subscriptionQuery = query('u1')

gateway.graphql.addHook('preGatewaySubscriptionExecution', async (schema, document, context, service) => {
t.type(schema, GraphQLSchema)
t.type(document, 'object')
t.type(context, 'object')
t.type(service, 'object')
t.is(service.name, 'message')
t.ok('preGatewaySubscriptionExecution called')
})

await gateway.listen(0)

const { client } = createWebSocketClient(t, gateway)

client.write(JSON.stringify({
type: 'connection_init'
}))
client.write(JSON.stringify({
id: 1,
type: 'start',
payload: {
query: subscriptionQuery
}
}))

{
const [chunk] = await once(client, 'data')
const data = JSON.parse(chunk)
t.is(data.type, 'connection_ack')
}

gateway.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
sendMessage(message: {
text: "Hi there u1",
fromUserId: "u2",
toUserId: "u1"
}) {
id
}
}
`
}
})

{
const [chunk] = await once(client, 'data')
const data = JSON.parse(chunk)
t.deepEqual(data, {
id: 1,
type: 'data',
payload: {
data: {
newMessage: {
id: '2',
text: 'Hi there u1',
from: {
id: 'u2',
name: 'Jane'
},
to: {
id: 'u1',
name: 'John'
}
}
}
}
})
}
})

// -------------------------
// onSubscriptionResolution
// -------------------------
Expand Down
9 changes: 9 additions & 0 deletions test/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,12 @@ app.graphql.addHook('onSubscriptionResolution', async function (execution, conte
app.graphql.addHook('onSubscriptionEnd', async function (context) {
console.log('onSubscriptionEnd called')
})

// Hooks containing service metadata
app.graphql.addHook('preGatewayExecution', async function (schema, document, context, service) {
console.log('preGatewayExecution called')
})

app.graphql.addHook('preGatewaySubscriptionExecution', async function (schema, document, context, service) {
console.log('preGatewaySubscriptionExecution called')
})

0 comments on commit 6e5142e

Please sign in to comment.