Repository: mercurius-js/mercurius
Pull request #900 (2022-10-27 22:46:56)

Prevent parsing schema exceptions when importing directives 
This fix prevents the app from breaking if schema contains [directives imports](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#importing-directives) like this:

```gql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0",
        import: ["@key", "@shareable"])
```

I'm not sure if those imports actually work with Mercurius, but are a valid syntax, so shouldn't break it.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5f57390..1f8e9bc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,7 +10,7 @@ jobs:
     steps:
       - uses: actions/checkout@v3
       - name: Use Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v3.5.0
+        uses: actions/setup-node@v3.5.1
         with:
           node-version: ${{ matrix.node-version }}
       - name: Install Dependencies
diff --git a/docs/api/options.md b/docs/api/options.md
index 3f89d56..cf57686 100644
--- a/docs/api/options.md
+++ b/docs/api/options.md
@@ -52,6 +52,7 @@
 
 - `jit`: Integer. The minimum number of execution a query needs to be
   executed before being jit'ed.
+  - Default: `0`, jit is disabled.
 - `routes`: boolean. Serves the Default: `true`. A graphql endpoint is
   exposed at `/graphql`.
 - `path`: string. Change default graphql `/graphql` route to another one.
diff --git a/index.d.ts b/index.d.ts
index 39df195..5673216 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -437,12 +437,40 @@ export interface MercuriusSchemaOptions {
   schemaTransforms?: ((originalSchema: GraphQLSchema) => GraphQLSchema) | Array<(originalSchema: GraphQLSchema) => GraphQLSchema>;
 }
 
+export interface MercuriusGraphiQLOptions {
+  /**
+   * Expose the graphiql app, default `true`
+   */
+  enabled?: boolean;
+  /**
+   * The list of plugin to add to GraphiQL
+   */
+  plugins?: Array<{
+    /**
+     * The name of the plugin, it should be the same exported in the `umd`
+     */
+    name: string;
+    /**
+     * The props to be passed to the plugin
+     */
+    props?: Object;
+    /**
+     * The urls of the plugin, it's downloaded at runtime. (eg. https://unpkg.com/myplugin/....)
+     */
+    umdUrl: string;
+    /**
+     * A function name exported by the plugin to read/enrich the fetch response
+     */
+    fetcherWrapper?: string;
+  }>
+}
+
 export interface MercuriusCommonOptions {
   /**
    * Serve GraphiQL on /graphiql if true or 'graphiql' and if routes is true
    */
-  graphiql?: boolean | 'graphiql';
-  ide?: boolean | 'graphiql';
+  graphiql?: boolean | 'graphiql' | MercuriusGraphiQLOptions;
+  ide?: boolean | 'graphiql' | MercuriusGraphiQLOptions;
   /**
    * The minimum number of execution a query needs to be executed before being jit'ed.
    * @default 0 - disabled
diff --git a/lib/federation.js b/lib/federation.js
index 6c1258e..11d6131 100644
--- a/lib/federation.js
+++ b/lib/federation.js
@@ -92,7 +92,7 @@ function getStubTypes (schemaDefinitions, isGateway) {
   const directiveDefinitions = []
 
   for (const definition of schemaDefinitions) {
-    if (definition.kind === 'SchemaDefinition') {
+    if (definition.kind === 'SchemaDefinition' || definition.kind === 'SchemaExtension') {
       continue
     }
 
diff --git a/lib/gateway/build-gateway.js b/lib/gateway/build-gateway.js
index bdac04c..758792f 100644
--- a/lib/gateway/build-gateway.js
+++ b/lib/gateway/build-gateway.js
@@ -313,7 +313,7 @@ async function buildGateway (gatewayOpts, app) {
     throw new MER_ERR_GQL_GATEWAY_INIT('No valid service SDLs were provided')
   }
 
-  const schema = buildFederatedSchema(serviceSDLs.join(''), true)
+  const schema = buildFederatedSchema(serviceSDLs.join(' '), true)
 
   const typeToServiceMap = {}
   const typeFieldsToService = {}
@@ -419,7 +419,7 @@ async function buildGateway (gatewayOpts, app) {
     async refresh (isRetry) {
       const failedMandatoryServices = []
       if (this._serviceSDLs === undefined) {
-        this._serviceSDLs = serviceSDLs.join('')
+        this._serviceSDLs = serviceSDLs.join(' ')
       }
 
       const $refreshResult = await Promise.allSettled(
@@ -458,7 +458,7 @@ async function buildGateway (gatewayOpts, app) {
 
       const _serviceSDLs = Object.values(serviceMap)
         .map((service) => service.schemaDefinition)
-        .join('')
+        .join(' ')
 
       if (this._serviceSDLs === _serviceSDLs) {
         return null


Test patch:
diff --git a/test/federation.js b/test/federation.js
index 2c55103..4c1bcfa 100644
--- a/test/federation.js
+++ b/test/federation.js
@@ -1087,3 +1087,45 @@ test('basic federation support with \'schema\' in the schema', async (t) => {
     }
   })
 })
+
+test('should support directives import syntax', async (t) => {
+  const app = Fastify()
+
+  const schema = `
+    extend schema
+      @link(url: "https://specs.apollo.dev/federation/v2.0",
+        import: ["@key", "@shareable", "@override"])
+
+    extend type Query {
+      hello: String
+    }
+  `
+
+  const resolvers = {
+    Query: {
+      hello: () => 'world'
+    }
+  }
+
+  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/gateway/schema.js b/test/gateway/schema.js
index 13bdecd..e2fce8f 100644
--- a/test/gateway/schema.js
+++ b/test/gateway/schema.js
@@ -302,6 +302,65 @@ test('It builds the gateway schema correctly', async (t) => {
   })
 })
 
+test('Should merge schemas correctly', async (t) => {
+  const [helloService, helloServicePort] = await createService(
+    t,
+    'extend type Query { hello: String } directive @customDirective on FIELD_DEFINITION',
+    { Query: { hello: () => 'World' } }
+  )
+
+  const [worldService, worldServicePort] = await createService(
+    t,
+    'extend type Query { world: String }',
+    { Query: { world: () => 'Hello' } }
+  )
+
+  const gateway = Fastify()
+
+  t.teardown(async () => {
+    await gateway.close()
+    await helloService.close()
+    await worldService.close()
+  })
+
+  gateway.register(GQL, {
+    gateway: {
+      services: [{
+        name: 'hello',
+        url: `http://localhost:${helloServicePort}/graphql`
+      }, {
+        name: 'world',
+        url: `http://localhost:${worldServicePort}/graphql`
+      }]
+    }
+  })
+
+  const query = `
+    query {
+      hello
+      world
+    }
+  `
+
+  const res = await gateway.inject({
+    method: 'POST',
+    headers: {
+      'content-type': 'application/json'
+    },
+    url: '/graphql',
+    body: JSON.stringify({
+      query
+    })
+  })
+
+  t.same(JSON.parse(res.body), {
+    data: {
+      hello: 'World',
+      world: 'Hello'
+    }
+  })
+})
+
 test('It support variable inside nested arguments', async (t) => {
   const user = {
     id: 'u1',
diff --git a/test/types/index.ts b/test/types/index.ts
index 175a51c..3e56828 100644
--- a/test/types/index.ts
+++ b/test/types/index.ts
@@ -879,3 +879,40 @@ app.graphql.addHook('onResolution', async function (_execution, context) {
   expectType<number | undefined>(context.operationsCount)
   expectType<string>(context.__currentQuery)
 })
+
+// Test graphiql configuration using an object as params
+app.register(mercurius, { schema, resolvers, graphiql: { plugins: [] } })
+
+app.register(mercurius, { schema, resolvers, ide: { enabled: false } })
+
+app.register(mercurius, {
+  schema,
+  resolvers,
+  graphiql: {
+    enabled: true,
+    plugins: [
+      {
+        fetcherWrapper: 'testFetchWrapper',
+        umdUrl: 'http://some-url',
+        props: { foo: 'bar' },
+        name: 'pluginName'
+      }
+    ]
+  }
+})
+
+expectError(() => {
+  app.register(mercurius, {
+    schema,
+    resolvers,
+    graphiql: {
+      enabled: true,
+      plugins: [
+        {
+          fetcherWrapper: 'testFetchWrapper',
+          props: { foo: 'bar' }
+        }
+      ]
+    }
+  })
+})