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..ace949b --- /dev/null +++ b/graphiql.go @@ -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" }} + + + + + + GraphiQL + + + + + + + + + + + + +{{ end }} +` diff --git a/graphiql_test.go b/graphiql_test.go new file mode 100644 index 0000000..25ad929 --- /dev/null +++ b/graphiql_test.go @@ -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: "", + }, + "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) + } + }) + } +} diff --git a/handler.go b/handler.go index 4494ee9..6666f54 100644 --- a/handler.go +++ b/handler.go @@ -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"` @@ -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) @@ -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, } } @@ -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, } }