Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GraphiQL support #26

Merged
merged 5 commits into from
Sep 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Golang HTTP.Handler for [graphl-go](https://github.com/graphql-go/graphql)

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

### Usage
Expand All @@ -20,12 +20,13 @@ func main() {

// define GraphQL schema using relay library helpers
schema := graphql.NewSchema(...)

h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: true,
})

// serve HTTP
http.Handle("/graphql", h)
http.ListenAndServe(":8080", nil)
Expand Down
199 changes: 199 additions & 0 deletions graphiql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package handler

import (
"encoding/json"
"html/template"
"net/http"

"github.com/graphql-go/graphql"
)

// page is the page data structure of the rendered GraphiQL page
type graphiqlPage struct {
GraphiqlVersion string
QueryString string
ResultString string
VariablesString string
OperationName string
}

// renderGraphiQL renders the GraphiQL GUI
func renderGraphiQL(w http.ResponseWriter, params graphql.Params) {
t := template.New("GraphiQL")
t, err := t.Parse(graphiqlTemplate)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Create variables string
vars, err := json.MarshalIndent(params.VariableValues, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
varsString := string(vars)
if varsString == "null" {
varsString = ""
}

// Create result string
var resString string
if params.RequestString == "" {
resString = ""
} else {
result, err := json.MarshalIndent(graphql.Do(params), "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resString = string(result)
}

p := graphiqlPage{
GraphiqlVersion: graphiqlVersion,
QueryString: params.RequestString,
ResultString: resString,
VariablesString: varsString,
OperationName: params.OperationName,
}

err = t.ExecuteTemplate(w, "index", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}

// graphiqlVersion is the current version of GraphiQL
const graphiqlVersion = "0.11.3"

// tmpl is the page template to render GraphiQL
const graphiqlTemplate = `
{{ define "index" }}
<!--
The request to this GraphQL server provided the header "Accept: text/html"
and as a result has been presented GraphiQL - an in-browser IDE for
exploring GraphQL.

If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>GraphiQL</title>
<meta name="robots" content="noindex" />
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
</style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{ .GraphiqlVersion }}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.4.2/react.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphiql@{{ .GraphiqlVersion }}/graphiql.min.js"></script>
</head>
<body>
<script>
// Collect the URL parameters
var parameters = {};
window.location.search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});

// Produce a Location query string from a parameter object.
function locationQuery(params) {
return '?' + Object.keys(params).filter(function (key) {
return Boolean(params[key]);
}).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(params[key]);
}).join('&');
}

// Derive a fetch URL from the current URL, sans the GraphQL parameters.
var graphqlParamNames = {
query: true,
variables: true,
operationName: true
};

var otherParams = {};
for (var k in parameters) {
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
otherParams[k] = parameters[k];
}
}
var fetchURL = locationQuery(otherParams);

// Defines a GraphQL fetcher using the fetch API.
function graphQLFetcher(graphQLParams) {
return fetch(fetchURL, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}

// When the query and variables string is edited, update the URL bar so
// that it can be easily shared.
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}

function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}

function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}

function updateURL() {
history.replaceState(null, null, locationQuery(parameters));
}

// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
query: {{ .QueryString }},
response: {{ .ResultString }},
variables: {{ .VariablesString }},
operationName: {{ .OperationName }},
}),
document.body
);
</script>
</body>
</html>
{{ end }}
`
90 changes: 90 additions & 0 deletions graphiql_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package handler_test

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/graphql-go/graphql/testutil"
"github.com/graphql-go/handler"
)

func TestRenderGraphiQL(t *testing.T) {
cases := map[string]struct {
graphiqlEnabled bool
accept string
url string
expectedStatusCode int
expectedContentType string
expectedBodyContains string
}{
"renders GraphiQL": {
graphiqlEnabled: true,
accept: "text/html",
expectedStatusCode: http.StatusOK,
expectedContentType: "text/html; charset=utf-8",
expectedBodyContains: "<!DOCTYPE html>",
},
"doesn't render graphiQL if turned off": {
graphiqlEnabled: false,
accept: "text/html",
expectedStatusCode: http.StatusOK,
expectedContentType: "application/json; charset=utf-8",
},
"doesn't render GraphiQL if Content-Type application/json is present": {
graphiqlEnabled: true,
accept: "application/json,text/html",
expectedStatusCode: http.StatusOK,
expectedContentType: "application/json; charset=utf-8",
},
"doesn't render GraphiQL if Content-Type text/html is not present": {
graphiqlEnabled: true,
expectedStatusCode: http.StatusOK,
expectedContentType: "application/json; charset=utf-8",
},
"doesn't render GraphiQL if 'raw' query is present": {
graphiqlEnabled: true,
accept: "text/html",
url: "?raw",
expectedStatusCode: http.StatusOK,
expectedContentType: "application/json; charset=utf-8",
},
}

for tcID, tc := range cases {
t.Run(tcID, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, tc.url, nil)
if err != nil {
t.Error(err)
}

req.Header.Set("Accept", tc.accept)

h := handler.New(&handler.Config{
Schema: &testutil.StarWarsSchema,
GraphiQL: tc.graphiqlEnabled,
})

rr := httptest.NewRecorder()

h.ServeHTTP(rr, req)
resp := rr.Result()

statusCode := resp.StatusCode
if statusCode != tc.expectedStatusCode {
t.Fatalf("%s: wrong status code, expected %v, got %v", tcID, tc.expectedStatusCode, statusCode)
}

contentType := resp.Header.Get("Content-Type")
if contentType != tc.expectedContentType {
t.Fatalf("%s: wrong content type, expected %s, got %s", tcID, tc.expectedContentType, contentType)
}

body := rr.Body.String()
if !strings.Contains(body, tc.expectedBodyContains) {
t.Fatalf("%s: wrong body, expected %s to contain %s", tcID, body, tc.expectedBodyContains)
}
})
}
}
29 changes: 21 additions & 8 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ const (
type Handler struct {
Schema *graphql.Schema

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

if h.graphiql {
acceptHeader := r.Header.Get("Accept")
_, raw := r.URL.Query()["raw"]
if !raw && !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") {
renderGraphiQL(w, params)
return
}
}

// use proper JSON Header
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Content-Type", "application/json; charset=utf-8")

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

type Config struct {
Schema *graphql.Schema
Pretty bool
Schema *graphql.Schema
Pretty bool
GraphiQL bool
}

func NewConfig() *Config {
return &Config{
Schema: nil,
Pretty: true,
Schema: nil,
Pretty: true,
GraphiQL: true,
}
}

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

return &Handler{
Schema: p.Schema,
pretty: p.Pretty,
Schema: p.Schema,
pretty: p.Pretty,
graphiql: p.GraphiQL,
}
}