Repository: mcollina/fastify-gql (js)

Issue:
Add graphql playground alongside with graphiql
Hi. There's graphql playground that does pretty much the same things that graphiql does. However it has more features. Here's demo page: https://www.graphqlbin.com/v2/6RQ6TM

It has standalone package https://www.npmjs.com/package/graphql-playground-html that contains several html files https://github.com/prisma/graphql-playground/tree/master/packages/graphql-playground-html that could be used just like as graphiql.html. We could copy one of them into static folder just like it's done in fastify-swagger plugin.

Also it doesn't use service worker which allows to explore it without having https (e.g. on some domain like example.loc that will point to 127.0.0.1). In such case graphiql doesn't work.

I could send PR with this changes, but I need a suggestion which route it should expose: `/gql-playground` or `graphiql-playground` or something else.

Pull request:
Feat/add graphql playground
Added `.npmignore` to remove unnecessary files from release.
Added `package-lock.json` to `.gitignore`.
Added GraphQL IDE aka GraphQL Playground. 

Closes #52.

Patch:
diff --git a/.gitignore b/.gitignore
index b2d11c1..9b00a76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,3 +62,4 @@ typings/
 
 # clinic
 *clinic*
+package-lock.json
diff --git a/.travis.yml b/.travis.yml
index 7442a81..27c2c2e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,7 @@
 language: node_js
 
 node_js:
+  - "12"
   - "10"
   - "8"
 
diff --git a/README.md b/README.md
index ce8bdeb..1506e95 100644
--- a/README.md
+++ b/README.md
@@ -100,9 +100,10 @@ __fastify-gql__ supports the following options:
 * `resolvers`: Object. The graphql resolvers.
 * `loaders`: Object. See [defineLoaders](#defineLoaders) for more
   details.
-* `graphiql`: boolean. Serve
-  [GraphiQL](https://www.npmjs.com/package/graphiql) on `/graphiql` if
-  `routes` is `true`.
+* `graphiql`: boolean | string. Serve
+  [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`: Intenger. The minimum number of execution a query needs to be
   executed before being jit'ed.
 * `routes`: boolean. Serves the Default: `true`. A graphql endpoint is
@@ -151,6 +152,12 @@ payload must conform to the following JSON schema:
 Serves [GraphiQL](https://www.npmjs.com/package/graphiql) if enabled by
 the options.
 
+#### GET /playground
+
+Serves [GraphQL IDE](https://www.npmjs.com/package/graphql-playground-react) if enabled by
+the options.
+
+
 ### decorators
 
 __fastify-gql__ adds the following decorators.
diff --git a/package.json b/package.json
index cf49935..25b0bec 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fastify-gql",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "description": "Fastify barebone GraphQL adapter",
   "main": "index.js",
   "scripts": {
@@ -23,13 +23,13 @@
     "pre-commit": "^1.2.2",
     "snazzy": "^8.0.0",
     "standard": "^12.0.1",
-    "tap": "^12.5.3"
+    "tap": "^14.1.6"
   },
   "dependencies": {
     "fastify-plugin": "^1.5.0",
     "fastify-static": "^2.3.2",
-    "graphql": "^14.1.1",
-    "graphql-jit": "0.1.3",
+    "graphql": "^14.3.0",
+    "graphql-jit": "^0.2.0",
     "http-errors": "^1.7.2",
     "single-user-cache": "^0.3.0",
     "tiny-lru": "^6.0.0"
diff --git a/routes.js b/routes.js
index 6bbddc3..1662252 100644
--- a/routes.js
+++ b/routes.js
@@ -152,9 +152,15 @@ module.exports = async function (app, opts) {
       root: join(__dirname, 'static'),
       wildcard: false
     })
-
-    app.get('/graphiql', (req, reply) => {
-      reply.redirect(`${app.prefix}/graphiql.html`)
-    })
+    if (opts.graphiql === true || opts.graphiql === 'graphiql') {
+      app.get('/graphiql', (req, reply) => {
+        reply.redirect(`${app.prefix}/graphiql.html`)
+      })
+    }
+    if (opts.graphiql === 'playground') {
+      app.get('/playground', (req, reply) => {
+        reply.redirect(`${app.prefix}/playground.html`)
+      })
+    }
   }
 }
diff --git a/static/playground.html b/static/playground.html
new file mode 100644
index 0000000..34c4107
--- /dev/null
+++ b/static/playground.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta
+      name="viewport"
+      content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui"
+    />
+    <title>GraphQL Playground</title>
+    <link
+      rel="stylesheet"
+      href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css"
+    />
+    <link
+      rel="shortcut icon"
+      href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png"
+    />
+    <script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
+  </head>
+
+  <body>
+    <div id="root">
+      <style>
+        body {
+          background-color: rgb(23, 42, 58);
+          font-family: Open Sans, sans-serif;
+          height: 90vh;
+        }
+        #root {
+          height: 100%;
+          width: 100%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+        }
+        .loading {
+          font-size: 32px;
+          font-weight: 200;
+          color: rgba(255, 255, 255, 0.6);
+          margin-left: 20px;
+        }
+        img {
+          width: 78px;
+          height: 78px;
+        }
+        .title {
+          font-weight: 400;
+        }
+      </style>
+      <img src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/logo.png" alt="" />
+      <div class="loading">
+        Loading
+        <span class="title">GraphQL Playground</span>
+      </div>
+    </div>
+    <script>
+      window.addEventListener('load', function(event) {
+        GraphQLPlayground.init(document.getElementById('root'), {
+          endpoint: '/graphql',
+        });
+      });
+    </script>
+  </body>
+</html>


Test patch:
diff --git a/tap-snapshots/test-reply-decorator.js-TAP.test.js b/tap-snapshots/test-reply-decorator.js-TAP.test.js
index bdc45ab..0bd660f 100644
--- a/tap-snapshots/test-reply-decorator.js-TAP.test.js
+++ b/tap-snapshots/test-reply-decorator.js-TAP.test.js
@@ -5,6 +5,6 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/reply-decorator.js TAP reply decorator set status code to 400 with bad query > undefined 1`] = `
+exports[`test/reply-decorator.js TAP reply decorator set status code to 400 with bad query > must match snapshot 1`] = `
 {"errors":[{"message":"Syntax Error: Expected Name, found <EOF>","locations":[{"line":1,"column":18}]}]}
 `
diff --git a/tap-snapshots/test-routes.js-TAP.test.js b/tap-snapshots/test-routes.js-TAP.test.js
index 6acd216..8840d91 100644
--- a/tap-snapshots/test-routes.js-TAP.test.js
+++ b/tap-snapshots/test-routes.js-TAP.test.js
@@ -5,7 +5,7 @@
  * Make sure to inspect the output below.  Do not ignore changes!
  */
 'use strict'
-exports[`test/routes.js TAP GET return 500 on resolver error > undefined 1`] = `
+exports[`test/routes.js TAP GET return 500 on resolver error > must match snapshot 1`] = `
 {
   "errors": [
     {
@@ -24,42 +24,42 @@ exports[`test/routes.js TAP GET return 500 on resolver error > undefined 1`] = `
 }
 `
 
-exports[`test/routes.js TAP POST return 500 on resolver error > undefined 1`] = `
+exports[`test/routes.js TAP POST return 400 on error > must match snapshot 1`] = `
 {
   "errors": [
     {
-      "message": "this is a dummy error",
+      "message": "Syntax Error: Expected Name, found <EOF>",
       "locations": [
         {
           "line": 1,
-          "column": 2
+          "column": 18
         }
-      ],
-      "path": [
-        "add"
       ]
     }
   ]
 }
 `
 
-exports[`test/routes.js TAP POST return 400 on error > undefined 1`] = `
+exports[`test/routes.js TAP POST return 500 on resolver error > must match snapshot 1`] = `
 {
   "errors": [
     {
-      "message": "Syntax Error: Expected Name, found <EOF>",
+      "message": "this is a dummy error",
       "locations": [
         {
           "line": 1,
-          "column": 18
+          "column": 2
         }
+      ],
+      "path": [
+        "add"
       ]
     }
   ]
 }
 `
 
-exports[`test/routes.js TAP mutation with GET errors > undefined 1`] = `
+exports[`test/routes.js TAP mutation with GET errors > must match snapshot 1`] = `
 {
   "errors": [
     {
diff --git a/test/routes.js b/test/routes.js
index 524acb8..b7ead3d 100644
--- a/test/routes.js
+++ b/test/routes.js
@@ -573,7 +573,7 @@ test('GET graphiql endpoint', async (t) => {
 test('GET graphiql endpoint with prefix', async (t) => {
   const app = Fastify()
   app.register(GQL, {
-    graphiql: true,
+    graphiql: 'graphiql',
     prefix: '/test-prefix'
   })
 
@@ -603,6 +603,53 @@ test('GET graphiql endpoint with prefixed wrapper', async (t) => {
   t.strictEqual(res.headers.location, '/test-wrapper-prefix/graphiql.html')
 })
 
+test('GET graphql playground endpoint', async (t) => {
+  const app = Fastify()
+  app.register(GQL, {
+    graphiql: 'playground'
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/playground'
+  })
+  t.strictEqual(res.statusCode, 302)
+  t.strictEqual(res.headers.location, '/playground.html')
+})
+
+test('GET graphql playground endpoint with prefix', async (t) => {
+  const app = Fastify()
+  app.register(GQL, {
+    graphiql: 'playground',
+    prefix: '/test-prefix'
+  })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/test-prefix/playground'
+  })
+
+  t.strictEqual(res.statusCode, 302)
+  t.strictEqual(res.headers.location, '/test-prefix/playground.html')
+})
+
+test('GET graphql playground endpoint with prefixed wrapper', async (t) => {
+  const app = Fastify()
+  app.register(async function (app, opts) {
+    app.register(GQL, {
+      graphiql: 'playground'
+    })
+  }, { prefix: '/test-wrapper-prefix' })
+
+  const res = await app.inject({
+    method: 'GET',
+    url: '/test-wrapper-prefix/playground'
+  })
+
+  t.strictEqual(res.statusCode, 302)
+  t.strictEqual(res.headers.location, '/test-wrapper-prefix/playground.html')
+})
+
 test('GET graphql endpoint with prefix', async (t) => {
   const app = Fastify()
   const schema = `