Repository: mercurius-js/mercurius
Pull request #581 (2021-09-23 09:39:41)

Do not crash with federationMetadata with a schema definition
Fixes https://github.com/mercurius-js/mercurius/issues/580


Patch:
diff --git a/lib/federation.js b/lib/federation.js
index 3b56ce2..4c8736f 100644
--- a/lib/federation.js
+++ b/lib/federation.js
@@ -92,6 +92,10 @@ function getStubTypes (schemaDefinitions, isGateway) {
   const directiveDefinitions = []
 
   for (const definition of schemaDefinitions) {
+    if (definition.kind === 'SchemaDefinition') {
+      continue
+    }
+
     const typeName = definition.name.value
     const isTypeExtensionByDirective = hasExtensionDirective(definition)
     /* istanbul ignore else we are not interested in nodes that does not match if statements */
diff --git a/lib/subscription-connection.js b/lib/subscription-connection.js
index 163700d..3413624 100644
--- a/lib/subscription-connection.js
+++ b/lib/subscription-connection.js
@@ -49,9 +49,9 @@ module.exports = class SubscriptionConnection {
   }
 
   async handleConnection () {
-    for await (const [message] of on(this.socket, 'message')) {
+    for await (const [message, isBinary] of on(this.socket, 'message')) {
       try {
-        await this.handleMessage(message)
+        await this.handleMessage(message, isBinary)
       } catch (err) {
         this.fastify.log.error(err)
         this.handleConnectionClose()
@@ -59,10 +59,10 @@ module.exports = class SubscriptionConnection {
     }
   }
 
-  async handleMessage (message) {
+  async handleMessage (message, isBinary) {
     let data
     try {
-      data = sJSON.parse(message)
+      data = sJSON.parse(isBinary ? message : message.toString())
     } catch (e) {
       this.sendMessage(GQL_ERROR, null, 'Message must be a JSON string')
       return
diff --git a/package.json b/package.json
index 5434590..3d5bda6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "mercurius",
-  "version": "8.3.0",
+  "version": "8.4.0",
   "description": "Fastify GraphQL adapter with gateway and subscription support",
   "main": "index.js",
   "types": "index.d.ts",
@@ -71,7 +71,7 @@
     "single-user-cache": "^0.5.0",
     "tiny-lru": "^7.0.6",
     "undici": "^4.1.0",
-    "ws": "^7.5.2"
+    "ws": "^8.2.2"
   },
   "tsd": {
     "directory": "test/types"


Test patch:
diff --git a/test/federation.js b/test/federation.js
index 8045e59..ff3b1a2 100644
--- a/test/federation.js
+++ b/test/federation.js
@@ -1025,3 +1025,65 @@ test('federation schema is built correctly with type extension', async (t) => {
     }
   })
 })
+
+test('basic federation support with \'schema\' in the schema', async (t) => {
+  const app = Fastify()
+  const schema = `
+    extend type Query {
+      me: User
+    }
+
+    type User @key(fields: "id") {
+      id: ID!
+      name: String
+      username: String
+    }
+
+    schema {
+      query: Query
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      me: () => {
+        return {
+          id: '1',
+          name: 'John',
+          username: '@john'
+        }
+      }
+    },
+    User: {
+      __resolveReference: (object) => {
+        return {
+          id: object.id,
+          name: 'John',
+          username: '@john'
+        }
+      }
+    }
+  }
+
+  app.register(GQL, {
+    schema,
+    resolvers,
+    federationMetadata: true
+  })
+
+  await app.ready()
+
+  const query = '{ _service { sdl } }'
+  const res = await app.inject({
+    method: 'GET',
+    url: `/graphql?query=${query}`
+  })
+
+  t.same(JSON.parse(res.body), {
+    data: {
+      _service: {
+        sdl: schema
+      }
+    }
+  })
+})
diff --git a/test/subscription-client.js b/test/subscription-client.js
index 4d85dae..2510383 100644
--- a/test/subscription-client.js
+++ b/test/subscription-client.js
@@ -11,8 +11,8 @@ test('subscription client calls the publish method with the correct payload', (t
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         ws.send(JSON.stringify({ id: undefined, type: 'connection_ack' }))
       } else if (data.type === 'start') {
@@ -46,8 +46,8 @@ test('subscription client calls the publish method with null after GQL_COMPLETE
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         ws.send(JSON.stringify({ id: undefined, type: 'connection_ack' }))
       } else if (data.type === 'start') {
@@ -99,13 +99,16 @@ test('subscription client tries to reconnect when server closes', (t) => {
   function connectionCallback () {
     if (shouldCloseServer) {
       server.close()
+      for (const ws of server.clients) {
+        ws.terminate()
+      }
       shouldCloseServer = false
       server = new WS.Server({ port }, () => {
         createSubscription()
       })
       server.on('connection', function connection (ws) {
-        ws.on('message', (message) => {
-          const data = JSON.parse(message)
+        ws.on('message', function incoming (message, isBinary) {
+          const data = JSON.parse(isBinary ? message : message.toString())
           if (data.type === 'connection_init') {
             ws.send(JSON.stringify({ id: undefined, type: 'connection_ack' }))
           } else if (data.type === 'start') {
@@ -150,8 +153,8 @@ test('subscription client multiple subscriptions is handled by one operation', t
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         ws.send(JSON.stringify({ id: undefined, type: 'connection_ack' }))
       } else if (data.type === 'start') {
@@ -182,8 +185,8 @@ test('subscription client multiple subscriptions unsubscribe removes only one su
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'stop') {
         ws.send(JSON.stringify({ id: '1', type: 'complete' }))
       }
@@ -220,8 +223,8 @@ test('subscription client closes the connection after GQL_CONNECTION_ERROR type
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         ws.send(JSON.stringify({ id: '1', type: 'connection_error' }))
       }
@@ -248,8 +251,8 @@ test('subscription client connectionInitPayload is correctly passed', (t) => {
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         t.same(data.payload, connectionInitPayload)
         client.close()
@@ -294,8 +297,8 @@ test('subscription client sending empty object payload on connection init', (t)
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         t.same(data.payload, {})
         ws.send(JSON.stringify({ id: '1', type: 'complete' }))
@@ -329,8 +332,8 @@ test('subscription client not throwing error on GQL_CONNECTION_KEEP_ALIVE type p
   })
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'start') {
         ws.send(JSON.stringify({ id: '1', type: 'complete' }))
       }
@@ -369,8 +372,8 @@ test('subscription client should throw on createSubscription if connection is no
   const port = server.address().port
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         ws.send(JSON.stringify({ id: undefined, type: 'connection_error' }))
       }
@@ -400,8 +403,8 @@ test('subscription client should pass the error payload to failedConnectionCallb
   const errorPayload = { message: 'error' }
 
   server.on('connection', function connection (ws) {
-    ws.on('message', function incoming (message) {
-      const data = JSON.parse(message)
+    ws.on('message', function incoming (message, isBinary) {
+      const data = JSON.parse(isBinary ? message : message.toString())
       if (data.type === 'connection_init') {
         ws.send(JSON.stringify({ id: undefined, type: 'connection_error', payload: errorPayload }))
       }