Repository: mercurius-js/mercurius
Pull request #515 (2021-06-17 11:53:23)

fix: change context merging behavior
Use provided `context` as a target in merging to keep the object prototype.
Resolves: #514 

DanTsk: > Can you please add a unit test?

Done

Patch:
diff --git a/index.js b/index.js
index a3309e0..8aaad40 100644
--- a/index.js
+++ b/index.js
@@ -184,7 +184,7 @@ const plugin = fp(async function (app, opts) {
       if (typeof gateway.pollingInterval === 'number') {
         gatewayInterval = setInterval(async () => {
           try {
-            const context = assignApplicationLifecycleHooksToContext(fastifyGraphQl[kHooks], {})
+            const context = assignApplicationLifecycleHooksToContext({}, fastifyGraphQl[kHooks])
             const schema = await gateway.refresh()
             if (schema !== null) {
               // Trigger onGatewayReplaceSchema hook
@@ -254,7 +254,11 @@ const plugin = fp(async function (app, opts) {
   app.decorateReply(graphqlCtx, null)
 
   app.decorateReply('graphql', function (source, context, variables, operationName) {
-    context = Object.assign({ reply: this, app }, context)
+    if (!context) {
+      context = {}
+    }
+
+    context = Object.assign(context, { reply: this, app })
     if (app[kFactory]) {
       this[kLoaders] = factory.create(context)
     }
@@ -424,8 +428,12 @@ const plugin = fp(async function (app, opts) {
   }
 
   async function fastifyGraphQl (source, context, variables, operationName) {
-    context = Object.assign({ app: this, lruGatewayResolvers, errors: null }, context)
-    context = assignLifeCycleHooksToContext(fastifyGraphQl[kHooks], context)
+    if (!context) {
+      context = {}
+    }
+
+    context = Object.assign(context, { app: this, lruGatewayResolvers, errors: null })
+    context = assignLifeCycleHooksToContext(context, fastifyGraphQl[kHooks])
     const reply = context.reply
 
     // Trigger preParsing hook
diff --git a/lib/hooks.js b/lib/hooks.js
index 3060489..f059e6b 100644
--- a/lib/hooks.js
+++ b/lib/hooks.js
@@ -45,15 +45,15 @@ Hooks.prototype.add = function (hook, fn) {
   this[hook].push(fn)
 }
 
-function assignApplicationLifecycleHooksToContext (hooks, context) {
+function assignApplicationLifecycleHooksToContext (context, hooks) {
   const contextHooks = {
     onGatewayReplaceSchema: null
   }
   if (hooks.onGatewayReplaceSchema.length > 0) contextHooks.onGatewayReplaceSchema = hooks.onGatewayReplaceSchema.slice()
-  return Object.assign(contextHooks, context)
+  return Object.assign(context, contextHooks)
 }
 
-function assignLifeCycleHooksToContext (hooks, context) {
+function assignLifeCycleHooksToContext (context, hooks) {
   const contextHooks = {
     preParsing: null,
     preValidation: null,
@@ -76,7 +76,7 @@ function assignLifeCycleHooksToContext (hooks, context) {
   if (hooks.preGatewaySubscriptionExecution.length > 0) contextHooks.preGatewaySubscriptionExecution = hooks.preGatewaySubscriptionExecution.slice()
   if (hooks.onSubscriptionResolution.length > 0) contextHooks.onSubscriptionResolution = hooks.onSubscriptionResolution.slice()
   if (hooks.onSubscriptionEnd.length > 0) contextHooks.onSubscriptionEnd = hooks.onSubscriptionEnd.slice()
-  return Object.assign(contextHooks, context)
+  return Object.assign(context, contextHooks)
 }
 
 async function hooksRunner (functions, runner, request) {
diff --git a/lib/routes.js b/lib/routes.js
index 563f19c..ad6a95e 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -178,7 +178,7 @@ module.exports = async function (app, opts) {
     }
 
     // Handle the query, throwing an error if required
-    return reply.graphql(query, { pubsub: subscriber, ...context, __currentQuery: query }, variables, operationName)
+    return reply.graphql(query, Object.assign(context, { pubsub: subscriber, __currentQuery: query }), variables, operationName)
   }
 
   function executeRegularQuery (body, request, reply) {
diff --git a/lib/subscription.js b/lib/subscription.js
index c166ee3..61502e7 100644
--- a/lib/subscription.js
+++ b/lib/subscription.js
@@ -25,9 +25,9 @@ function createConnectionHandler ({ subscriber, fastify, onConnect, onDisconnect
     }
 
     if (context.app.graphql && context.app.graphql[kHooks]) {
-      context = assignLifeCycleHooksToContext(context.app.graphql[kHooks], context)
+      context = assignLifeCycleHooksToContext(context, context.app.graphql[kHooks])
     } else {
-      context = assignLifeCycleHooksToContext(new Hooks(), context)
+      context = assignLifeCycleHooksToContext(context, new Hooks())
     }
 
     let resolveContext


Test patch:
diff --git a/test/routes.js b/test/routes.js
index c77538a..2991ed4 100644
--- a/test/routes.js
+++ b/test/routes.js
@@ -1411,6 +1411,74 @@ test('routes with custom context', async (t) => {
   })
 })
 
+test('routes with custom class-based context', async (t) => {
+  const app = Fastify()
+
+  const schema = `
+    type Query {
+      test: String
+    }
+  `
+
+  class CustomContext {
+    constructor () {
+      this.test = 'custom'
+    }
+
+    method () {
+      return this.test
+    }
+  }
+
+  const resolvers = {
+    test: async (args, ctx) => {
+      t.type(ctx, 'object')
+      t.type(ctx.reply, 'object')
+      t.type(ctx.app, 'object')
+      t.type(ctx.method, 'function')
+      t.equal(ctx.test, 'custom')
+      t.equal(ctx.method(), 'custom')
+      t.equal(ctx.constructor, CustomContext)
+      return ctx.method()
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    context: (request, reply) => {
+      t.type(request, 'object')
+      t.type(reply, 'object')
+      return new CustomContext()
+    }
+  })
+
+  const get = await app.inject({
+    method: 'GET',
+    url: '/graphql?query=query { test }'
+  })
+
+  t.same(JSON.parse(get.body), {
+    data: {
+      test: 'custom'
+    }
+  })
+
+  const post = await app.inject({
+    method: 'POST',
+    url: '/graphql',
+    body: {
+      query: 'query { test }'
+    }
+  })
+
+  t.same(JSON.parse(post.body), {
+    data: {
+      test: 'custom'
+    }
+  })
+})
+
 test('connection is not allowed when verifyClient callback called with `false`', t => {
   t.plan(2)
   const app = Fastify()