From ac8976627a0e65bb57ccec3c2786d12fdad994e2 Mon Sep 17 00:00:00 2001 From: Tobias Fuhrimann Date: Thu, 24 Aug 2017 01:15:13 +0200 Subject: [PATCH 1/4] Add GraphiQL support --- README.md | 7 +- graphiql.go | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++ handler.go | 31 ++++++--- 3 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 graphiql.go diff --git a/README.md b/README.md index 4ba1ef4..a6986c5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) diff --git a/graphiql.go b/graphiql.go new file mode 100644 index 0000000..ebc5d7d --- /dev/null +++ b/graphiql.go @@ -0,0 +1,190 @@ +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 + result, err := json.MarshalIndent(graphql.Do(params), "", " ") + 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" }} + + + + + + GraphiQL + + + + + + + + + + + + +{{ end }} +` diff --git a/handler.go b/handler.go index 23d12f2..a61a872 100644 --- a/handler.go +++ b/handler.go @@ -20,8 +20,9 @@ const ( type Handler struct { Schema *graphql.Schema - - pretty bool + + pretty bool + graphiql bool } type RequestOptions struct { Query string `json:"query" url:"query" schema:"query"` @@ -129,7 +130,14 @@ func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r * } result := graphql.Do(params) - + if h.graphiql { + acceptHeader := r.Header.Get("Accept") + if !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") { + renderGraphiQL(w, params) + return + } + } + if h.pretty { w.WriteHeader(http.StatusOK) buff, _ := json.MarshalIndent(result, "", "\t") @@ -138,7 +146,7 @@ func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r * } else { w.WriteHeader(http.StatusOK) buff, _ := json.Marshal(result) - + w.Write(buff) } } @@ -149,14 +157,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, } } @@ -169,7 +179,8 @@ func New(p *Config) *Handler { } return &Handler{ - Schema: p.Schema, - pretty: p.Pretty, + Schema: p.Schema, + pretty: p.Pretty, + graphiql: p.GraphiQL, } } From 70ee617a8078e32e3eae0c8d0f9356259e0f96f3 Mon Sep 17 00:00:00 2001 From: Tobias Fuhrimann Date: Sun, 27 Aug 2017 13:21:36 +0200 Subject: [PATCH 2/4] Add tests for GraphiQL --- graphiql.go | 4 +++ graphiql_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ handler.go | 2 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 graphiql_test.go diff --git a/graphiql.go b/graphiql.go index ebc5d7d..99288fd 100644 --- a/graphiql.go +++ b/graphiql.go @@ -39,6 +39,10 @@ func renderGraphiQL(w http.ResponseWriter, params graphql.Params) { // Create result string result, err := json.MarshalIndent(graphql.Do(params), "", " ") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } resString := string(result) p := graphiqlPage{ diff --git a/graphiql_test.go b/graphiql_test.go new file mode 100644 index 0000000..86a588e --- /dev/null +++ b/graphiql_test.go @@ -0,0 +1,77 @@ +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 + expectedStatusCode int + expectedContentType string + expectedBodyContains string + }{ + "renders GraphiQL": { + graphiqlEnabled: true, + accept: "text/html", + expectedStatusCode: http.StatusOK, + expectedContentType: "text/html; charset=utf-8", + expectedBodyContains: "", + }, + "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", + }, + } + + for tcID, tc := range cases { + t.Run(tcID, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "", 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) + } + }) + } +} diff --git a/handler.go b/handler.go index 53a9f3c..f1dd155 100644 --- a/handler.go +++ b/handler.go @@ -139,7 +139,7 @@ func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r * } // 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) From ce8e677f513b7192838b23f41bab8f110cfcb32b Mon Sep 17 00:00:00 2001 From: Tobias Fuhrimann Date: Sun, 27 Aug 2017 14:14:53 +0200 Subject: [PATCH 3/4] Account for empty request --- graphiql.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/graphiql.go b/graphiql.go index 99288fd..ace949b 100644 --- a/graphiql.go +++ b/graphiql.go @@ -38,12 +38,17 @@ func renderGraphiQL(w http.ResponseWriter, params graphql.Params) { } // Create result string - result, err := json.MarshalIndent(graphql.Do(params), "", " ") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + 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) } - resString := string(result) p := graphiqlPage{ GraphiqlVersion: graphiqlVersion, From e2a07d5e726778e209628af73ad2c2a810dbf985 Mon Sep 17 00:00:00 2001 From: Tobias Fuhrimann Date: Sun, 27 Aug 2017 14:34:03 +0200 Subject: [PATCH 4/4] Take raw into account --- graphiql_test.go | 15 ++++++++++++++- handler.go | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/graphiql_test.go b/graphiql_test.go index 86a588e..25ad929 100644 --- a/graphiql_test.go +++ b/graphiql_test.go @@ -14,6 +14,7 @@ func TestRenderGraphiQL(t *testing.T) { cases := map[string]struct { graphiqlEnabled bool accept string + url string expectedStatusCode int expectedContentType string expectedBodyContains string @@ -37,11 +38,23 @@ func TestRenderGraphiQL(t *testing.T) { 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, "", nil) + req, err := http.NewRequest(http.MethodGet, tc.url, nil) if err != nil { t.Error(err) } diff --git a/handler.go b/handler.go index f1dd155..6666f54 100644 --- a/handler.go +++ b/handler.go @@ -132,7 +132,8 @@ func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r * if h.graphiql { acceptHeader := r.Header.Get("Accept") - if !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") { + _, raw := r.URL.Query()["raw"] + if !raw && !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") { renderGraphiQL(w, params) return }