Repository: mcollina/fastify-gql (js)

Issue:
Proposal: Graphql Schema replacement
Hey, everyone
Does `fastify-gql` have method for replacing existing graphql schema with another one?

I'm using a microservice architecture with graphql and in some cases, I need to setup schema's autorefresh. (I'm using simple recursive setTimeout)

Example of code for Apollo-Server

```js
const refreshSchema = async () => {
	try {
		const schemaDerivedData = await graphqlServer.schemaDerivedData;

		graphqlServer.schemaDerivedData = Promise.resolve({
			...schemaDerivedData,
			schema: await generateSchema(),
		});
	} catch (err) {
		logger.warn(err, {
			method: 'server.initializeApplication::refreshSchema',
		});
	}

	setTimeout(refreshSchema, config.refreshTimeout);
};

setTimeout(refreshSchema, config.refreshTimeout);
```

Could I somehow replace the existing graphql schema with a new one?

# Implementation

```js
  fastifyGraphQl.replaceSchema = function (s) {
    if (!s || typeof s !== 'object') {
      throw new Error('Must provide valid Document AST')
    }

    schema = s
  }

```

Pull request:
feat: add .replaceSchema method
This PR closes #100 

`replaceSchema` works great with `makeSchemaExecutable`

Patch:
diff --git a/README.md b/README.md
index 3bad144..fb25b86 100644
--- a/README.md
+++ b/README.md
@@ -305,7 +305,7 @@ __fastify-gql__ supports the following options:
 * `loaders`: Object. See [defineLoaders](#defineLoaders) for more
   details.
 * `graphiql`: boolean | string. Serve
-  [GraphiQL](https://www.npmjs.com/package/graphiql) on `/graphiql` if `true` or `'graphiql'`, or 
+  [GraphiQL](https://www.npmjs.com/package/graphiql) on `/graphiql` if `true` or `'graphiql'`, or
   [GraphQL IDE](https://www.npmjs.com/package/graphql-playground-react) on `/playground` if `'playground'`
   if `routes` is `true`.
 * `jit`: Integer. The minimum number of execution a query needs to be
@@ -319,7 +319,7 @@ __fastify-gql__ supports the following options:
 * `queryDepth`: `Integer`. The maximum depth allowed for a single query.
 * `subscription`: Boolean | Object. Enable subscriptions. It is uses [mqemitter](https://github.com/mcollina/mqemitter) when it is true. To use a custom emitter set the value to an object containing the emitter.
   * `subscription.emitter`: Custom emitter
-  * `subscription.verifyClient`: `Function` A function which can be used to validate incoming connections. 
+  * `subscription.verifyClient`: `Function` A function which can be used to validate incoming connections.
 
 #### queryDepth example
 ```
@@ -490,6 +490,70 @@ async function run () {
 run()
 ```
 
+#### app.graphql.replaceSchema(schema)
+
+It is possible to replace schema and resolvers using `makeSchemaExecutable` function in separate fastify plugins, like so:
+
+```js
+const Fastify = require('fastify')
+const GQL = require('fastify-gql')
+const { makeExecutableSchema } = require('graphql-tools')
+
+const app = Fastify()
+
+app.register(GQL, {
+  schema: makeExecutableSchema({
+    typeDefs: `
+    type Query {
+      add(x: Int, y: Int): Int
+    }
+  `,
+    resolvers: {
+      Query: {
+        add: async (_, { x, y }) => x + y
+      }
+    }
+  })
+})
+
+app.register(async function (app) {
+  app.graphql.replaceSchema(
+    makeExecutableSchema({
+      typeDefs: `
+      type Query {
+        add(x: Int, y: Int, z: Int): Int
+      }
+    `,
+      resolvers: {
+        Query: {
+          add: async (_, { x, y, z }) => x + y + z
+        }
+      }
+    })
+  )
+})
+
+async function run () {
+  // needed so that graphql is defined
+
+  await app.ready()
+
+  const query = '{ add(x: 2, y: 2, z: 2) }'
+  const res = await app.graphql(query)
+
+  console.log(res)
+  // prints:
+  //
+  // {
+  //   data: {
+  //      add: 6
+  //   }
+  // }
+}
+
+run()
+```
+
 <a name="loaders"></a>
 #### app.graphql.defineLoaders(loaders)
 
diff --git a/index.d.ts b/index.d.ts
index 4684f79..5867a27 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -12,6 +12,11 @@ import graphql, { GraphQLSchema, GraphQLError, Source, DocumentNode, ExecutionRe
 declare namespace FastifyGQL {
 
   export interface Plugin<HttpResponse> {
+    /**
+     * Replace existing schema
+     * @param schema graphql schema
+     */
+    replaceSchema(schema: GraphQLSchema): void;
     /**
      * Extend existing schema
      * @param schema graphql schema
diff --git a/index.js b/index.js
index a27c3ad..e6026c9 100644
--- a/index.js
+++ b/index.js
@@ -104,6 +104,17 @@ module.exports = fp(async function (app, opts) {
 
   app.decorate('graphql', fastifyGraphQl)
 
+  fastifyGraphQl.replaceSchema = function (s) {
+    if (!s || typeof s !== 'object') {
+      throw new Error('Must provide valid Document AST')
+    }
+
+    schema = s
+
+    lru.clear()
+    lruErrors.clear()
+  }
+
   fastifyGraphQl.extendSchema = function (s) {
     if (typeof s === 'string') {
       s = parse(s)


Test patch:
diff --git a/test/app-decorator.js b/test/app-decorator.js
index ae40438..686bd27 100644
--- a/test/app-decorator.js
+++ b/test/app-decorator.js
@@ -148,6 +148,140 @@ test('operationName', async (t) => {
   })
 })
 
+test('replaceSchema with makeSchemaExecutable', async (t) => {
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema: makeExecutableSchema({
+      typeDefs: `
+      type Query {
+        add(x: Int, y: Int): Int
+      }
+    `,
+      resolvers: {
+        Query: {
+          add: async (_, { x, y }) => x + y
+        }
+      }
+    })
+  })
+
+  app.register(async function (app) {
+    app.graphql.replaceSchema(
+      makeExecutableSchema({
+        typeDefs: `
+        type Query {
+          add(x: Int, y: Int, z: Int): Int
+        }
+      `,
+        resolvers: {
+          Query: {
+            add: async (_, { x, y, z }) => x + y + z
+          }
+        }
+      })
+    )
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  const query = '{ add(x: 2, y: 2, z: 2) }'
+  const res = await app.graphql(query)
+
+  t.deepEqual(res, {
+    data: {
+      add: 6
+    }
+  })
+})
+
+test('replaceSchema (clearing cache)', async (t) => {
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema: makeExecutableSchema({
+      typeDefs: `
+      type Query {
+        add(x: Int, y: Int): Int
+        subtract(x: Int, y: Int): Int
+      }
+    `,
+      resolvers: {
+        Query: {
+          add: async (_, { x, y }) => x + y,
+          subtract: async (_, { x, y }) => x - y
+        }
+      }
+    })
+  })
+
+  // needed so that graphql is defined
+  await app.ready()
+
+  let query
+
+  query = '{ subtract(x: 4, y: 2) }'
+  const res = await app.graphql(query)
+
+  t.deepEqual(res, {
+    data: {
+      subtract: 2
+    }
+  })
+
+  app.graphql.replaceSchema(
+    makeExecutableSchema({
+      typeDefs: `
+      type Query {
+        add(x: Int, y: Int): Int
+      }
+    `,
+      resolvers: {
+        Query: {
+          add: async (_, { x, y }) => x + y
+        }
+      }
+    })
+  )
+
+  query = '{ subtract(x: 4, y: 2) }'
+  try {
+    await app.graphql(query)
+  } catch (err) {
+    t.equal(err.errors[0].message, 'Cannot query field "subtract" on type "Query".')
+  }
+})
+
+test('replaceSchema with makeSchemaExecutable (schema should be provided)', async (t) => {
+  const app = Fastify()
+
+  app.register(GQL, {
+    schema: makeExecutableSchema({
+      typeDefs: `
+      type Query {
+        add(x: Int, y: Int): Int
+      }
+    `,
+      resolvers: {
+        Query: {
+          add: async (_, { x, y }) => x + y
+        }
+      }
+    })
+  })
+
+  app.register(async function (app) {
+    app.graphql.replaceSchema()
+  })
+
+  try {
+    await app.ready()
+  } catch (error) {
+    t.equal(error.message, 'Must provide valid Document AST')
+  }
+})
+
 test('extendSchema and defineResolvers for query', async (t) => {
   const app = Fastify()
   const schema = `
@@ -222,7 +356,7 @@ test('extendSchema and defineResolvers throws without mutation definition', asyn
     extend type Query {
       add(x: Int, y: Int): Int
     }
-    
+
     type Mutation {
       sub(x: Int, y: Int): Int
     }