package handler import ( "encoding/json" "io/ioutil" "net/http" "net/url" "strings" "github.com/graphql-go/graphql" "context" ) const ( ContentTypeJSON = "application/json" ContentTypeGraphQL = "application/graphql" ContentTypeFormURLEncoded = "application/x-www-form-urlencoded" ) type Handler struct { Schema *graphql.Schema pretty bool graphiql bool } type RequestOptions struct { Query string `json:"query" url:"query" schema:"query"` Variables map[string]interface{} `json:"variables" url:"variables" schema:"variables"` OperationName string `json:"operationName" url:"operationName" schema:"operationName"` } // a workaround for getting`variables` as a JSON string type requestOptionsCompatibility struct { Query string `json:"query" url:"query" schema:"query"` Variables string `json:"variables" url:"variables" schema:"variables"` OperationName string `json:"operationName" url:"operationName" schema:"operationName"` } func getFromForm(values url.Values) *RequestOptions { query := values.Get("query") if query != "" { // get variables map variables := make(map[string]interface{}, len(values)) variablesStr := values.Get("variables") json.Unmarshal([]byte(variablesStr), &variables) return &RequestOptions{ Query: query, Variables: variables, OperationName: values.Get("operationName"), } } return nil } // RequestOptions Parses a http.Request into GraphQL request options struct func NewRequestOptions(r *http.Request) *RequestOptions { if reqOpt := getFromForm(r.URL.Query()); reqOpt != nil { return reqOpt } if r.Method != "POST" { return &RequestOptions{} } if r.Body == nil { return &RequestOptions{} } // TODO: improve Content-Type handling contentTypeStr := r.Header.Get("Content-Type") contentTypeTokens := strings.Split(contentTypeStr, ";") contentType := contentTypeTokens[0] switch contentType { case ContentTypeGraphQL: body, err := ioutil.ReadAll(r.Body) if err != nil { return &RequestOptions{} } return &RequestOptions{ Query: string(body), } case ContentTypeFormURLEncoded: if err := r.ParseForm(); err != nil { return &RequestOptions{} } if reqOpt := getFromForm(r.PostForm); reqOpt != nil { return reqOpt } return &RequestOptions{} case ContentTypeJSON: fallthrough default: var opts RequestOptions body, err := ioutil.ReadAll(r.Body) if err != nil { return &opts } err = json.Unmarshal(body, &opts) if err != nil { // Probably `variables` was sent as a string instead of an object. // So, we try to be polite and try to parse that as a JSON string var optsCompatible requestOptionsCompatibility json.Unmarshal(body, &optsCompatible) json.Unmarshal([]byte(optsCompatible.Variables), &opts.Variables) } return &opts } } // ContextHandler provides an entrypoint into executing graphQL queries with a // user-provided context. func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { // get query opts := NewRequestOptions(r) // execute graphql query params := graphql.Params{ Schema: *h.Schema, RequestString: opts.Query, VariableValues: opts.Variables, OperationName: opts.OperationName, Context: ctx, } 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; charset=utf-8") if h.pretty { w.WriteHeader(http.StatusOK) buff, _ := json.MarshalIndent(result, "", "\t") w.Write(buff) } else { w.WriteHeader(http.StatusOK) buff, _ := json.Marshal(result) w.Write(buff) } } // ServeHTTP provides an entrypoint into executing graphQL queries. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.ContextHandler(r.Context(), w, r) } type Config struct { Schema *graphql.Schema Pretty bool GraphiQL bool } func NewConfig() *Config { return &Config{ Schema: nil, Pretty: true, GraphiQL: true, } } func New(p *Config) *Handler { if p == nil { p = NewConfig() } if p.Schema == nil { panic("undefined GraphQL schema") } return &Handler{ Schema: p.Schema, pretty: p.Pretty, graphiql: p.GraphiQL, } }