Skip to content

Commit fb144b8

Browse files
authored
Merge pull request #26 from mastertinner/feature/graphiql
Add GraphiQL support
2 parents 43051ba + e2a07d5 commit fb144b8

File tree

4 files changed

+314
-11
lines changed

4 files changed

+314
-11
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Golang HTTP.Handler for [graphl-go](https://github.com/graphql-go/graphql)
44

55
### Notes:
6-
This is based on alpha version of `graphql-go` and `graphql-relay-go`.
6+
This is based on alpha version of `graphql-go` and `graphql-relay-go`.
77
Be sure to watch both repositories for latest changes.
88

99
### Usage
@@ -20,12 +20,13 @@ func main() {
2020

2121
// define GraphQL schema using relay library helpers
2222
schema := graphql.NewSchema(...)
23-
23+
2424
h := handler.New(&handler.Config{
2525
Schema: &schema,
2626
Pretty: true,
27+
GraphiQL: true,
2728
})
28-
29+
2930
// serve HTTP
3031
http.Handle("/graphql", h)
3132
http.ListenAndServe(":8080", nil)

graphiql.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package handler
2+
3+
import (
4+
"encoding/json"
5+
"html/template"
6+
"net/http"
7+
8+
"github.com/graphql-go/graphql"
9+
)
10+
11+
// page is the page data structure of the rendered GraphiQL page
12+
type graphiqlPage struct {
13+
GraphiqlVersion string
14+
QueryString string
15+
ResultString string
16+
VariablesString string
17+
OperationName string
18+
}
19+
20+
// renderGraphiQL renders the GraphiQL GUI
21+
func renderGraphiQL(w http.ResponseWriter, params graphql.Params) {
22+
t := template.New("GraphiQL")
23+
t, err := t.Parse(graphiqlTemplate)
24+
if err != nil {
25+
http.Error(w, err.Error(), http.StatusInternalServerError)
26+
return
27+
}
28+
29+
// Create variables string
30+
vars, err := json.MarshalIndent(params.VariableValues, "", " ")
31+
if err != nil {
32+
http.Error(w, err.Error(), http.StatusInternalServerError)
33+
return
34+
}
35+
varsString := string(vars)
36+
if varsString == "null" {
37+
varsString = ""
38+
}
39+
40+
// Create result string
41+
var resString string
42+
if params.RequestString == "" {
43+
resString = ""
44+
} else {
45+
result, err := json.MarshalIndent(graphql.Do(params), "", " ")
46+
if err != nil {
47+
http.Error(w, err.Error(), http.StatusInternalServerError)
48+
return
49+
}
50+
resString = string(result)
51+
}
52+
53+
p := graphiqlPage{
54+
GraphiqlVersion: graphiqlVersion,
55+
QueryString: params.RequestString,
56+
ResultString: resString,
57+
VariablesString: varsString,
58+
OperationName: params.OperationName,
59+
}
60+
61+
err = t.ExecuteTemplate(w, "index", p)
62+
if err != nil {
63+
http.Error(w, err.Error(), http.StatusInternalServerError)
64+
}
65+
return
66+
}
67+
68+
// graphiqlVersion is the current version of GraphiQL
69+
const graphiqlVersion = "0.11.3"
70+
71+
// tmpl is the page template to render GraphiQL
72+
const graphiqlTemplate = `
73+
{{ define "index" }}
74+
<!--
75+
The request to this GraphQL server provided the header "Accept: text/html"
76+
and as a result has been presented GraphiQL - an in-browser IDE for
77+
exploring GraphQL.
78+
79+
If you wish to receive JSON, provide the header "Accept: application/json" or
80+
add "&raw" to the end of the URL within a browser.
81+
-->
82+
<!DOCTYPE html>
83+
<html>
84+
<head>
85+
<meta charset="utf-8" />
86+
<title>GraphiQL</title>
87+
<meta name="robots" content="noindex" />
88+
<style>
89+
html, body {
90+
height: 100%;
91+
margin: 0;
92+
overflow: hidden;
93+
width: 100%;
94+
}
95+
</style>
96+
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ .GraphiqlVersion }}/graphiql.css" rel="stylesheet" />
97+
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
98+
<script src="//cdn.jsdelivr.net/react/15.4.2/react.min.js"></script>
99+
<script src="//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js"></script>
100+
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ .GraphiqlVersion }}/graphiql.min.js"></script>
101+
</head>
102+
<body>
103+
<script>
104+
// Collect the URL parameters
105+
var parameters = {};
106+
window.location.search.substr(1).split('&').forEach(function (entry) {
107+
var eq = entry.indexOf('=');
108+
if (eq >= 0) {
109+
parameters[decodeURIComponent(entry.slice(0, eq))] =
110+
decodeURIComponent(entry.slice(eq + 1));
111+
}
112+
});
113+
114+
// Produce a Location query string from a parameter object.
115+
function locationQuery(params) {
116+
return '?' + Object.keys(params).filter(function (key) {
117+
return Boolean(params[key]);
118+
}).map(function (key) {
119+
return encodeURIComponent(key) + '=' +
120+
encodeURIComponent(params[key]);
121+
}).join('&');
122+
}
123+
124+
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
125+
var graphqlParamNames = {
126+
query: true,
127+
variables: true,
128+
operationName: true
129+
};
130+
131+
var otherParams = {};
132+
for (var k in parameters) {
133+
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
134+
otherParams[k] = parameters[k];
135+
}
136+
}
137+
var fetchURL = locationQuery(otherParams);
138+
139+
// Defines a GraphQL fetcher using the fetch API.
140+
function graphQLFetcher(graphQLParams) {
141+
return fetch(fetchURL, {
142+
method: 'post',
143+
headers: {
144+
'Accept': 'application/json',
145+
'Content-Type': 'application/json'
146+
},
147+
body: JSON.stringify(graphQLParams),
148+
credentials: 'include',
149+
}).then(function (response) {
150+
return response.text();
151+
}).then(function (responseBody) {
152+
try {
153+
return JSON.parse(responseBody);
154+
} catch (error) {
155+
return responseBody;
156+
}
157+
});
158+
}
159+
160+
// When the query and variables string is edited, update the URL bar so
161+
// that it can be easily shared.
162+
function onEditQuery(newQuery) {
163+
parameters.query = newQuery;
164+
updateURL();
165+
}
166+
167+
function onEditVariables(newVariables) {
168+
parameters.variables = newVariables;
169+
updateURL();
170+
}
171+
172+
function onEditOperationName(newOperationName) {
173+
parameters.operationName = newOperationName;
174+
updateURL();
175+
}
176+
177+
function updateURL() {
178+
history.replaceState(null, null, locationQuery(parameters));
179+
}
180+
181+
// Render <GraphiQL /> into the body.
182+
ReactDOM.render(
183+
React.createElement(GraphiQL, {
184+
fetcher: graphQLFetcher,
185+
onEditQuery: onEditQuery,
186+
onEditVariables: onEditVariables,
187+
onEditOperationName: onEditOperationName,
188+
query: {{ .QueryString }},
189+
response: {{ .ResultString }},
190+
variables: {{ .VariablesString }},
191+
operationName: {{ .OperationName }},
192+
}),
193+
document.body
194+
);
195+
</script>
196+
</body>
197+
</html>
198+
{{ end }}
199+
`

graphiql_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package handler_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
9+
"github.com/graphql-go/graphql/testutil"
10+
"github.com/graphql-go/handler"
11+
)
12+
13+
func TestRenderGraphiQL(t *testing.T) {
14+
cases := map[string]struct {
15+
graphiqlEnabled bool
16+
accept string
17+
url string
18+
expectedStatusCode int
19+
expectedContentType string
20+
expectedBodyContains string
21+
}{
22+
"renders GraphiQL": {
23+
graphiqlEnabled: true,
24+
accept: "text/html",
25+
expectedStatusCode: http.StatusOK,
26+
expectedContentType: "text/html; charset=utf-8",
27+
expectedBodyContains: "<!DOCTYPE html>",
28+
},
29+
"doesn't render graphiQL if turned off": {
30+
graphiqlEnabled: false,
31+
accept: "text/html",
32+
expectedStatusCode: http.StatusOK,
33+
expectedContentType: "application/json; charset=utf-8",
34+
},
35+
"doesn't render GraphiQL if Content-Type application/json is present": {
36+
graphiqlEnabled: true,
37+
accept: "application/json,text/html",
38+
expectedStatusCode: http.StatusOK,
39+
expectedContentType: "application/json; charset=utf-8",
40+
},
41+
"doesn't render GraphiQL if Content-Type text/html is not present": {
42+
graphiqlEnabled: true,
43+
expectedStatusCode: http.StatusOK,
44+
expectedContentType: "application/json; charset=utf-8",
45+
},
46+
"doesn't render GraphiQL if 'raw' query is present": {
47+
graphiqlEnabled: true,
48+
accept: "text/html",
49+
url: "?raw",
50+
expectedStatusCode: http.StatusOK,
51+
expectedContentType: "application/json; charset=utf-8",
52+
},
53+
}
54+
55+
for tcID, tc := range cases {
56+
t.Run(tcID, func(t *testing.T) {
57+
req, err := http.NewRequest(http.MethodGet, tc.url, nil)
58+
if err != nil {
59+
t.Error(err)
60+
}
61+
62+
req.Header.Set("Accept", tc.accept)
63+
64+
h := handler.New(&handler.Config{
65+
Schema: &testutil.StarWarsSchema,
66+
GraphiQL: tc.graphiqlEnabled,
67+
})
68+
69+
rr := httptest.NewRecorder()
70+
71+
h.ServeHTTP(rr, req)
72+
resp := rr.Result()
73+
74+
statusCode := resp.StatusCode
75+
if statusCode != tc.expectedStatusCode {
76+
t.Fatalf("%s: wrong status code, expected %v, got %v", tcID, tc.expectedStatusCode, statusCode)
77+
}
78+
79+
contentType := resp.Header.Get("Content-Type")
80+
if contentType != tc.expectedContentType {
81+
t.Fatalf("%s: wrong content type, expected %s, got %s", tcID, tc.expectedContentType, contentType)
82+
}
83+
84+
body := rr.Body.String()
85+
if !strings.Contains(body, tc.expectedBodyContains) {
86+
t.Fatalf("%s: wrong body, expected %s to contain %s", tcID, body, tc.expectedBodyContains)
87+
}
88+
})
89+
}
90+
}

handler.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const (
2121
type Handler struct {
2222
Schema *graphql.Schema
2323

24-
pretty bool
24+
pretty bool
25+
graphiql bool
2526
}
2627
type RequestOptions struct {
2728
Query string `json:"query" url:"query" schema:"query"`
@@ -129,8 +130,17 @@ func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r *
129130
}
130131
result := graphql.Do(params)
131132

133+
if h.graphiql {
134+
acceptHeader := r.Header.Get("Accept")
135+
_, raw := r.URL.Query()["raw"]
136+
if !raw && !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") {
137+
renderGraphiQL(w, params)
138+
return
139+
}
140+
}
141+
132142
// use proper JSON Header
133-
w.Header().Add("Content-Type", "application/json")
143+
w.Header().Add("Content-Type", "application/json; charset=utf-8")
134144

135145
if h.pretty {
136146
w.WriteHeader(http.StatusOK)
@@ -151,14 +161,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
151161
}
152162

153163
type Config struct {
154-
Schema *graphql.Schema
155-
Pretty bool
164+
Schema *graphql.Schema
165+
Pretty bool
166+
GraphiQL bool
156167
}
157168

158169
func NewConfig() *Config {
159170
return &Config{
160-
Schema: nil,
161-
Pretty: true,
171+
Schema: nil,
172+
Pretty: true,
173+
GraphiQL: true,
162174
}
163175
}
164176

@@ -171,7 +183,8 @@ func New(p *Config) *Handler {
171183
}
172184

173185
return &Handler{
174-
Schema: p.Schema,
175-
pretty: p.Pretty,
186+
Schema: p.Schema,
187+
pretty: p.Pretty,
188+
graphiql: p.GraphiQL,
176189
}
177190
}

0 commit comments

Comments
 (0)