Skip to content

Commit 66b9d3a

Browse files
committed
feat: Add support to customize the graphiql interface
1 parent 340722f commit 66b9d3a

7 files changed

+362
-39
lines changed

LICENSE

+17-23
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
BSD License
1+
Copyright (c) 2018, Neap pty ltd.
2+
All rights reserved.
23

3-
For google-graphql-functions software
4-
5-
Copyright (c) 2017, Neap pty ltd. All rights reserved.
6-
7-
Redistribution and use in source and binary forms, with or without modification,
8-
are permitted provided that the following conditions are met:
9-
10-
* Redistributions of source code must retain the above copyright notice, this
11-
list of conditions and the following disclaimer.
12-
13-
* Redistributions in binary form must reproduce the above copyright notice,
14-
this list of conditions and the following disclaimer in the documentation
15-
and/or other materials provided with the distribution.
16-
17-
* Neither the name Facebook nor the names of its contributors may be used to
18-
endorse or promote products derived from this software without specific
19-
prior written permission.
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are met:
6+
* Redistributions of source code must retain the above copyright
7+
notice, this list of conditions and the following disclaimer.
8+
* Redistributions in binary form must reproduce the above copyright
9+
notice, this list of conditions and the following disclaimer in the
10+
documentation and/or other materials provided with the distribution.
11+
* Neither the name of Neap nor the
12+
names of its contributors may be used to endorse or promote products
13+
derived from this software without specific prior written permission.
2014

2115
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
2216
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
2317
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25-
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18+
DISCLAIMED. IN NO EVENT SHALL NEAP PTY LTD BE LIABLE FOR ANY
19+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
2620
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27-
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28-
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2923
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30-
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ npm install; \
1515
npm start
1616
```
1717

18+
This will serve 2 endpoints:
19+
20+
- [http://localhost:4000](http://localhost:4000): This is the GraphQL endpoint that your client can start querying.
21+
- [http://localhost:4000/graphiql](http://localhost:4000/graphiql): This is the GraphiQL Web UI that you can use to test and query your GraphQL server.
22+
1823
Deploying that api to [Zeit Now](https://zeit.co/now) will take between 15 seconds to 1.5 minute (depending on whether you need to login/creating a free Zeit account or not).
1924

2025
_If you haven't installed Zeit now-CLI yet or you need to login/create an account, then copy/paste this in your terminal:_

src/index.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2017, Neap pty ltd.
2+
* Copyright (c) 2018, Neap pty ltd.
33
* All rights reserved.
44
*
55
* This source code is licensed under the BSD-style license found in the
@@ -23,7 +23,7 @@ const httpError = require('http-errors')
2323
const url = require('url')
2424

2525
const parseBody = require('./parseBody')
26-
const renderGraphiQL = require('./renderGraphiQL')
26+
const { renderGraphiQL } = require('./renderGraphiQL')
2727

2828
const graphqlHandler = options => {
2929
if (!options)
@@ -64,6 +64,7 @@ function graphqlHTTP(options) {
6464
let validationRules
6565
let endpointURL
6666
let schemaAST
67+
let graphiQlOptions
6768

6869
// Promises are used as a mechanism for capturing any thrown errors during
6970
// the asynchronous process below.
@@ -101,6 +102,7 @@ function graphqlHTTP(options) {
101102
extensionsFn = optionsData.extensions
102103
endpointURL = optionsData.endpointURL
103104
schemaAST = optionsData.schemaAST
105+
graphiQlOptions = optionsData.graphiQlOptions
104106

105107
validationRules = graphql.specifiedRules
106108
if (optionsData.validationRules) {
@@ -233,7 +235,7 @@ function graphqlHTTP(options) {
233235
}
234236
// If allowed to show GraphiQL, present it instead of JSON.
235237
if (showGraphiQL) {
236-
const payload = renderGraphiQL({ query, variables, operationName, result, schemaAST })
238+
const payload = renderGraphiQL({ query, variables, operationName, result, schemaAST }, graphiQlOptions)
237239
response.setHeader('Content-Type', 'text/html; charset=utf-8')
238240
sendResponse(response, payload)
239241
} else {
@@ -264,7 +266,7 @@ function getGraphQLParams(request) {
264266
*/
265267
function parseGraphQLParams(urlData, bodyData) {
266268
// GraphQL Query string.
267-
let query = urlData.query || bodyData.query
269+
let query = bodyData.query || urlData.query
268270
if (typeof query !== 'string') {
269271
query = null
270272
}
@@ -282,7 +284,7 @@ function parseGraphQLParams(urlData, bodyData) {
282284
}
283285

284286
// Name of GraphQL operation to execute.
285-
let operationName = urlData.operationName || bodyData.operationName
287+
let operationName = bodyData.operationName || urlData.operationName
286288
if (typeof operationName !== 'string') {
287289
operationName = null
288290
}

src/parseBody.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2017, Neap pty ltd.
2+
* Copyright (c) 2018, Neap pty ltd.
33
* All rights reserved.
44
*
55
* This source code is licensed under the BSD-style license found in the

src/renderGraphiQL.js

+215-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2017, Neap pty ltd.
2+
* Copyright (c) 2018, Neap pty ltd.
33
* All rights reserved.
44
*
55
* This source code is licensed under the BSD-style license found in the
@@ -9,12 +9,12 @@
99
* This file incorporates work covered by the following copyright and
1010
* permission notice:
1111
*
12-
* Copyright (c) 2015, Facebook, Inc.
13-
* All rights reserved.
12+
* Copyright (c) 2015, Facebook, Inc.
13+
* All rights reserved.
1414
*
15-
* This source code is licensed under the BSD-style license found in the
16-
* LICENSE file in the root directory of this source tree. An additional grant
17-
* of patent rights can be found in the PATENTS file in the same directory.
15+
* This source code is licensed under the BSD-style license found in the
16+
* LICENSE file in the root directory of this source tree. An additional grant
17+
* of patent rights can be found in the PATENTS file in the same directory.
1818
*/
1919

2020
'use strict'
@@ -24,27 +24,233 @@ Object.defineProperty(exports, '__esModule', {
2424
})
2525

2626
// Current latest version of GraphiQL.
27-
var GRAPHIQL_VERSION = '0.11.2'
27+
// var GRAPHIQL_VERSION = '0.11.2'
2828

2929
// Ensures string values are safe to be used within a <script> tag.
3030

3131
function safeSerialize(data) {
3232
return data ? JSON.stringify(data).replace(/\//g, '\\/') : 'undefined'
3333
}
3434

35+
const getFuncArgNames = fn => {
36+
if (fn && typeof(fn) == 'function') {
37+
const fnStr = fn.toString().trim()
38+
const arrowFn = fnStr.indexOf('function ') != 0
39+
if (arrowFn) {
40+
const arrowFnLeft = fnStr.split('=>')[0].trim()
41+
// single argument function with no parenthesis (e.g. x => ...)
42+
return arrowFnLeft.indexOf('(') != 0
43+
? [arrowFnLeft]
44+
// Zero, one or many arguments function (e.g. () => ... or (x) => ... or (x,y,z) => ....)
45+
: arrowFnLeft.replace(/\(|\)/g,'').split(',').map(x => x.trim()).filter(x => x)
46+
}
47+
else
48+
return ((fnStr.match(/\((.*?)\)/) || [])[0] || '').replace(/\(|\)/g,'').split(',').map(x => x.trim()).filter(x => x)
49+
}
50+
else
51+
return []
52+
}
53+
54+
const stringifyFnBody = fn => {
55+
if (fn && typeof(fn) == 'function') {
56+
const fnStr = fn.toString().trim()
57+
const stdFuncStyle = fnStr.indexOf('function ') == 0
58+
let body
59+
if (stdFuncStyle)
60+
body = fnStr.replace(/\s*function\s*(.*?){/,'').trim()
61+
else {
62+
const arrowFnLeft = fnStr.split('=>')[0].trim()
63+
body = arrowFnLeft.indexOf('(') != 0
64+
? fnStr.replace(/(.*?)=>\s*/, '')
65+
: fnStr.replace(/^\s*\((.*?)\)\s*=>\s*/, '').trim()
66+
}
67+
68+
const bodyStartsWithCurlyBracket = body.indexOf('{') == 0
69+
// Remove trailing curly bracket
70+
if (stdFuncStyle || bodyStartsWithCurlyBracket)
71+
body = body.replace(/}$/,'')
72+
73+
// Remove opening curly bracket
74+
if (bodyStartsWithCurlyBracket)
75+
body = body.replace(/^{/,'')
76+
77+
return body
78+
}
79+
else
80+
return null
81+
}
82+
83+
const scriptifyFunc = fn => {
84+
const jsCode = stringifyFnBody(fn)
85+
return jsCode ? `<script>\n${jsCode}\n</script>` : ''
86+
}
87+
3588
/**
3689
* When express-graphql receives a request which does not Accept JSON, but does
3790
* Accept HTML, it may present GraphiQL, the in-browser GraphQL explorer IDE.
3891
*
3992
* When shown, it will be pre-populated with the result of having executed the
4093
* requested query.
4194
*/
42-
module.exports = function renderGraphiQL(data) {
95+
const renderGraphiQL = (data, custom={}) => {
4396
var queryString = data.query
4497
var variablesString = data.variables ? JSON.stringify(data.variables, null, 2) : null
4598
var resultString = data.result ? JSON.stringify(data.result, null, 2) : null
4699
var operationName = data.operationName
47100

101+
const head = custom.head || {}
102+
const cssFiles = []
103+
const scriptFiles = []
104+
cssFiles.push(head.graphiqlCss || '//cdn.jsdelivr.net/npm/graphiql@0.11.2/graphiql.css')
105+
scriptFiles.push(head.fetchJs || '//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js')
106+
scriptFiles.push(head.reactJs || '//cdn.jsdelivr.net/react/15.4.2/react.min.js')
107+
scriptFiles.push(head.reactDomJs || '//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js')
108+
scriptFiles.push(head.graphiqlJs || '//cdn.jsdelivr.net/npm/graphiql@0.11.2/graphiql.min.js')
109+
110+
const pageTitle = head.title || 'GraphiQL'
111+
const customScript = scriptifyFunc(custom.script)
112+
const onRequest = stringifyFnBody(custom.onRequest) || 'return headers'
113+
const onRequestArgs = getFuncArgNames(custom.onRequest).join(',') || 'headers'
114+
115+
const headScriptsAndCss = `
116+
${head.custom || ''}
117+
${cssFiles.map(f => `<link href="${f}" rel="stylesheet" />`).join('\n ')}
118+
${scriptFiles.map(f => `<script src="${f}"></script>`).join('\n ')}`
119+
48120
/* eslint-disable max-len */
49-
return '<!--\nThe request to this GraphQL server provided the header "Accept: text/html"\nand as a result has been presented GraphiQL - an in-browser IDE for\nexploring GraphQL.\n\nIf you wish to receive JSON, provide the header "Accept: application/json" or\nadd "&raw" to the end of the URL within a browser.\n-->\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset="utf-8" />\n <title>GraphiQL</title>\n <meta name="robots" content="noindex" />\n <style>\n html, body {\n height: 100%;\n margin: 0;\n overflow: hidden;\n width: 100%;\n }\n </style>\n <link href="//cdn.jsdelivr.net/npm/graphiql@' + GRAPHIQL_VERSION + '/graphiql.css" rel="stylesheet" />\n <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>\n <script src="//cdn.jsdelivr.net/react/15.4.2/react.min.js"></script>\n <script src="//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js"></script>\n <script src="//cdn.jsdelivr.net/npm/graphiql@' + GRAPHIQL_VERSION + '/graphiql.min.js"></script>\n</head>\n<body>\n <script>\n // Collect the URL parameters\n var parameters = {};\n window.location.search.substr(1).split(\'&\').forEach(function (entry) {\n var eq = entry.indexOf(\'=\');\n if (eq >= 0) {\n parameters[decodeURIComponent(entry.slice(0, eq))] =\n decodeURIComponent(entry.slice(eq + 1));\n }\n });\n\n // Produce a Location query string from a parameter object.\n function locationQuery(params) {\n return \'?\' + Object.keys(params).filter(function (key) {\n return Boolean(params[key]);\n }).map(function (key) {\n return encodeURIComponent(key) + \'=\' +\n encodeURIComponent(params[key]);\n }).join(\'&\');\n }\n\n // Derive a fetch URL from the current URL, sans the GraphQL parameters.\n var graphqlParamNames = {\n query: true,\n variables: true,\n operationName: true\n };\n\n var otherParams = {};\n for (var k in parameters) {\n if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {\n otherParams[k] = parameters[k];\n }\n }\n var fetchURL = locationQuery(otherParams);\n\n // Defines a GraphQL fetcher using the fetch API.\n function graphQLFetcher(graphQLParams) {\n return fetch(fetchURL, {\n method: \'post\',\n headers: {\n \'Accept\': \'application/json\',\n \'Content-Type\': \'application/json\'\n },\n body: JSON.stringify(graphQLParams),\n credentials: \'include\',\n }).then(function (response) {\n return response.text();\n }).then(function (responseBody) {\n try {\n return JSON.parse(responseBody);\n } catch (error) {\n return responseBody;\n }\n });\n }\n\n // When the query and variables string is edited, update the URL bar so\n // that it can be easily shared.\n function onEditQuery(newQuery) {\n parameters.query = newQuery;\n updateURL();\n }\n\n function onEditVariables(newVariables) {\n parameters.variables = newVariables;\n updateURL();\n }\n\n function onEditOperationName(newOperationName) {\n parameters.operationName = newOperationName;\n updateURL();\n }\n\n function updateURL() {\n history.replaceState(null, null, locationQuery(parameters));\n }\n\n // Render <GraphiQL /> into the body.\n ReactDOM.render(\n React.createElement(GraphiQL, {\n fetcher: graphQLFetcher,\n onEditQuery: onEditQuery,\n onEditVariables: onEditVariables,\n onEditOperationName: onEditOperationName,\n query: ' + safeSerialize(queryString) + ',\n response: ' + safeSerialize(resultString) + ',\n variables: ' + safeSerialize(variablesString) + ',\n operationName: ' + safeSerialize(operationName) + ',\n }),\n document.body\n );\n </script>\n</body>\n</html>'
121+
return `<!--
122+
The request to this GraphQL server provided the header "Accept: text/html"
123+
and as a result has been presented GraphiQL - an in-browser IDE for
124+
exploring GraphQL.
125+
126+
If you wish to receive JSON, provide the header "Accept: application/json" or
127+
add "&raw" to the end of the URL within a browser.
128+
-->
129+
<!DOCTYPE html>
130+
<html>
131+
<head>
132+
<meta charset="utf-8" />
133+
<title>${pageTitle}</title>
134+
<meta name="robots" content="noindex" />
135+
<style>
136+
html, body {
137+
height: 100%;
138+
margin: 0;
139+
overflow: hidden;
140+
width: 100%;
141+
}
142+
</style>
143+
${headScriptsAndCss}
144+
</head>
145+
<body>
146+
${customScript}
147+
<script>
148+
// Collect the URL parameters
149+
var parameters = {};
150+
window.location.search.substr(1).split('&').forEach(function (entry) {
151+
var eq = entry.indexOf('=');
152+
if (eq >= 0) {
153+
parameters[decodeURIComponent(entry.slice(0, eq))] =
154+
decodeURIComponent(entry.slice(eq + 1));
155+
}
156+
});
157+
158+
// Produce a Location query string from a parameter object.
159+
function locationQuery(params) {
160+
return '?' + Object.keys(params).filter(function (key) {
161+
return Boolean(params[key]);
162+
}).map(function (key) {
163+
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
164+
}).join('&');
165+
}
166+
167+
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
168+
var graphqlParamNames = {
169+
query: true,
170+
variables: true,
171+
operationName: true
172+
};
173+
174+
var otherParams = {};
175+
for (var k in parameters) {
176+
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
177+
otherParams[k] = parameters[k];
178+
}
179+
}
180+
var fetchURL = locationQuery(otherParams);
181+
182+
function updateHeaders(${onRequestArgs}) {
183+
${onRequest}
184+
}
185+
186+
function getHeaders() {
187+
return updateHeaders({
188+
'Accept': 'application/json',
189+
'Content-Type': 'application/json'
190+
})
191+
}
192+
193+
// Defines a GraphQL fetcher using the fetch API.
194+
function graphQLFetcher(graphQLParams) {
195+
return fetch(fetchURL, {
196+
method: 'post',
197+
headers: getHeaders(),
198+
body: JSON.stringify(graphQLParams),
199+
credentials: 'include',
200+
}).then(function (response) {
201+
return response.text();
202+
}).then(function (responseBody) {
203+
try {
204+
return JSON.parse(responseBody);
205+
} catch (error) {
206+
return responseBody;
207+
}
208+
});
209+
}
210+
211+
// When the query and variables string is edited, update the URL bar so
212+
// that it can be easily shared.
213+
function onEditQuery(newQuery) {
214+
parameters.query = newQuery;
215+
updateURL();
216+
}
217+
218+
function onEditVariables(newVariables) {
219+
parameters.variables = newVariables;
220+
updateURL();
221+
}
222+
223+
function onEditOperationName(newOperationName) {
224+
parameters.operationName = newOperationName;
225+
updateURL();
226+
}
227+
228+
function updateURL() {
229+
history.replaceState(null, null, locationQuery(parameters));
230+
}
231+
232+
// Render <GraphiQL /> into the body.
233+
ReactDOM.render(
234+
React.createElement(GraphiQL, {
235+
fetcher: graphQLFetcher,
236+
onEditQuery: onEditQuery,
237+
onEditVariables: onEditVariables,
238+
onEditOperationName: onEditOperationName,
239+
query: '${safeSerialize(queryString)}',
240+
response: '${safeSerialize(resultString)}',
241+
variables: '${safeSerialize(variablesString)}',
242+
operationName: '${safeSerialize(operationName)}',
243+
}),
244+
document.body
245+
);
246+
</script>
247+
</body>
248+
</html>`
50249
}
250+
251+
252+
module.exports = {
253+
renderGraphiQL,
254+
stringifyFnBody,
255+
getFuncArgNames
256+
}

test/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2017, Neap Pty Ltd.
2+
* Copyright (c) 2018, Neap Pty Ltd.
33
* All rights reserved.
44
*
55
* This source code is licensed under the BSD-style license found in the

0 commit comments

Comments
 (0)