Repository: mcollina/fastify-gql
Pull request #77 (2019-10-22 11:18:32)

Add custom GraphQL context option
This PR is the same as stated in #74 and addresses the review comments.

Closes #74 
Closes #71

mcollina: Would you mind adding a `co-authored-by` in the commit so that we keep attribution of @brettlaforge work in https://github.com/mcollina/fastify-gql/pull/74?

Patch:
diff --git a/README.md b/README.md
index f7e6451..0df7d37 100644
--- a/README.md
+++ b/README.md
@@ -106,6 +106,30 @@ const resolvers = {
 ...
 ```
 
+### Build a custom GraphQL context object
+
+```js
+...
+const resolvers = {
+  Query: {
+    me: async (obj, args, ctx) => {
+      // access user_id in ctx
+      console.log(ctx.user_id)
+    }
+  }
+}
+app.register(GQL, {
+  schema: makeExecutableSchema({ typeDefs, resolvers }),
+  context: (request, reply) => {
+    // Return an object that will be available in your GraphQL resolvers
+    return {
+        user_id: 1234
+    }
+  }
+})
+...
+```
+
 ## API
 
 ### plugin options
@@ -126,6 +150,7 @@ __fastify-gql__ supports the following options:
   executed before being jit'ed.
 * `routes`: boolean. Serves the Default: `true`. A graphql endpoint is
   exposed at `/graphql`.
+* `context`: `Function`. Result of function is passed to resolvers as a custom GraphQL context. The function receives the `request` and `reply` as parameters. It is only called when `routes` options is `true`
 * `prefix`: String. Change the route prefix of the graphql endpoint if enabled.
 * `defineMutation`: Boolean. Add the empty Mutation definition if schema is not defined (Default: `false`).
 * `errorHandler`: `Function`  or `boolean`. Change the default error handler (Default: `true`). _Note: If a custom error handler is defined, it should return the standardized response format according to [GraphQL spec](https://graphql.org/learn/serving-over-http/#response)._
diff --git a/index.d.ts b/index.d.ts
index a62c225..30e9b8b 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -31,7 +31,7 @@ declare namespace FastifyGQL {
         [key: string]: (
           queries: Array<{
             obj: any,
-            params: any 
+            params: any
           }>,
           context: {
             reply: FastifyReply<HttpResponse>
@@ -44,7 +44,7 @@ declare namespace FastifyGQL {
     HttpServer extends (Server | Http2Server),
     HttpRequest extends (IncomingMessage | Http2ServerRequest),
     HttpResponse extends (ServerResponse | Http2ServerResponse)
-  > extends RegisterOptions<HttpServer, HttpRequest, HttpResponse> {
+    > extends RegisterOptions<HttpServer, HttpRequest, HttpResponse> {
     /**
      * The GraphQL schema. String schema will be parsed
      */
@@ -61,7 +61,7 @@ declare namespace FastifyGQL {
         [key: string]: (
           queries: Array<{
             obj: any,
-            params: any 
+            params: any
           }>,
           context: {
             reply: FastifyReply<HttpResponse>
@@ -104,7 +104,8 @@ declare namespace FastifyGQL {
     /**
      * The maximum depth allowed for a single query.
      */
-    queryDepth?: number
+    queryDepth?: number,
+    context?: (request: FastifyRequest<HttpRequest>, reply: FastifyReply<HttpResponse>) => Promise<any>
   }
 }
 
@@ -123,22 +124,22 @@ declare module "fastify" {
      * @param variables request variables which will get passed to the executor
      * @param operationName specify which operation will be run
      */
-      graphql(
-        source: string,
-        context?: any,
-        variables?: { [key: string]: any },
-        operationName?: string
-      ): FastifyReply<HttpResponse>;
+    graphql(
+      source: string,
+      context?: any,
+      variables?: { [key: string]: any },
+      operationName?: string
+    ): FastifyReply<HttpResponse>;
   }
 }
 
 declare function fastifyGQL<
   HttpServer extends (Server | Http2Server),
   HttpRequest extends (IncomingMessage | Http2ServerRequest),
-  HttpResponse extends (ServerResponse | Http2ServerResponse), 
+  HttpResponse extends (ServerResponse | Http2ServerResponse),
   Options = FastifyGQL.Options<HttpServer, HttpRequest, HttpResponse>
-> (
+>(
   fastify: fastify.FastifyInstance<HttpServer, HttpRequest, HttpResponse>,
-  opts: Options) : void;
+  opts: Options): void;
 
 export = fastifyGQL;
diff --git a/index.js b/index.js
index 9bac0a8..8b47e7a 100644
--- a/index.js
+++ b/index.js
@@ -88,7 +88,8 @@ module.exports = fp(async function (app, opts) {
     app.register(routes, {
       errorHandler: opts.errorHandler,
       graphiql: opts.graphiql,
-      prefix: opts.prefix
+      prefix: opts.prefix,
+      context: opts.context
     })
   }
 
diff --git a/routes.js b/routes.js
index 1662252..c0829be 100644
--- a/routes.js
+++ b/routes.js
@@ -73,6 +73,8 @@ module.exports = async function (app, opts) {
     app.setErrorHandler(defaultErrorHandler)
   }
 
+  const contextFn = opts.context
+
   app.get('/graphql', {
     schema: {
       querystring: {
@@ -92,7 +94,7 @@ module.exports = async function (app, opts) {
       response: responseSchema
     },
     attachValidation: true
-  }, function (request, reply) {
+  }, async function (request, reply) {
     validationHandler(request.validationError)
 
     let {
@@ -101,6 +103,11 @@ module.exports = async function (app, opts) {
       operationName
     } = request.query
 
+    let context = {}
+    if (contextFn) {
+      context = await contextFn(request, reply)
+    }
+
     if (variables) {
       try {
         variables = JSON.parse(variables)
@@ -111,7 +118,7 @@ module.exports = async function (app, opts) {
       }
     }
 
-    return reply.graphql(query, null, variables, operationName)
+    return reply.graphql(query, context, variables, operationName)
   })
 
   app.post('/graphql', {
@@ -144,7 +151,12 @@ module.exports = async function (app, opts) {
       operationName
     } = request.body
 
-    return reply.graphql(query, null, variables, operationName)
+    let context = {}
+    if (contextFn) {
+      context = await contextFn(request, reply)
+    }
+
+    return reply.graphql(query, context, variables, operationName)
   })
 
   if (opts.graphiql) {


Test patch:
diff --git a/test/routes.js b/test/routes.js
index b7ead3d..708a8c8 100644
--- a/test/routes.js
+++ b/test/routes.js
@@ -835,3 +835,60 @@ test('route validation is catched and parsed to graphql error', async (t) => {
   t.strictEqual(res.statusCode, 400)
   t.strictDeepEqual(JSON.parse(res.body), expectedResult)
 })
+
+test('routes with custom context', async (t) => {
+  const app = Fastify()
+
+  const schema = `
+    type Query {
+      test: String
+    }
+  `
+
+  const resolvers = {
+    test: async (args, ctx) => {
+      t.type(ctx, 'object')
+      t.type(ctx.reply, 'object')
+      t.type(ctx.app, 'object')
+      t.equal(ctx.test, 'custom')
+      return ctx.test
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    context: (request, reply) => {
+      t.type(request, 'object')
+      t.type(reply, 'object')
+      return {
+        test: 'custom'
+      }
+    }
+  })
+
+  const get = await app.inject({
+    method: 'GET',
+    url: '/graphql?query=query { test }'
+  })
+
+  t.deepEqual(JSON.parse(get.body), {
+    data: {
+      test: 'custom'
+    }
+  })
+
+  const post = await app.inject({
+    method: 'POST',
+    url: '/graphql',
+    body: {
+      query: 'query { test }'
+    }
+  })
+
+  t.deepEqual(JSON.parse(post.body), {
+    data: {
+      test: 'custom'
+    }
+  })
+})