handler.go

  1package handler
  2
  3import (
  4	"encoding/json"
  5	"io/ioutil"
  6	"net/http"
  7	"net/url"
  8	"strings"
  9
 10	"github.com/graphql-go/graphql"
 11
 12	"context"
 13)
 14
 15const (
 16	ContentTypeJSON           = "application/json"
 17	ContentTypeGraphQL        = "application/graphql"
 18	ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
 19)
 20
 21type Handler struct {
 22	Schema *graphql.Schema
 23	pretty   bool
 24	graphiql bool
 25}
 26type RequestOptions struct {
 27	Query         string                 `json:"query" url:"query" schema:"query"`
 28	Variables     map[string]interface{} `json:"variables" url:"variables" schema:"variables"`
 29	OperationName string                 `json:"operationName" url:"operationName" schema:"operationName"`
 30}
 31
 32// a workaround for getting`variables` as a JSON string
 33type requestOptionsCompatibility struct {
 34	Query         string `json:"query" url:"query" schema:"query"`
 35	Variables     string `json:"variables" url:"variables" schema:"variables"`
 36	OperationName string `json:"operationName" url:"operationName" schema:"operationName"`
 37}
 38
 39func getFromForm(values url.Values) *RequestOptions {
 40	query := values.Get("query")
 41	if query != "" {
 42		// get variables map
 43		variables := make(map[string]interface{}, len(values))
 44		variablesStr := values.Get("variables")
 45		json.Unmarshal([]byte(variablesStr), &variables)
 46
 47		return &RequestOptions{
 48			Query:         query,
 49			Variables:     variables,
 50			OperationName: values.Get("operationName"),
 51		}
 52	}
 53
 54	return nil
 55}
 56
 57// RequestOptions Parses a http.Request into GraphQL request options struct
 58func NewRequestOptions(r *http.Request) *RequestOptions {
 59	if reqOpt := getFromForm(r.URL.Query()); reqOpt != nil {
 60		return reqOpt
 61	}
 62
 63	if r.Method != "POST" {
 64		return &RequestOptions{}
 65	}
 66
 67	if r.Body == nil {
 68		return &RequestOptions{}
 69	}
 70
 71	// TODO: improve Content-Type handling
 72	contentTypeStr := r.Header.Get("Content-Type")
 73	contentTypeTokens := strings.Split(contentTypeStr, ";")
 74	contentType := contentTypeTokens[0]
 75
 76	switch contentType {
 77	case ContentTypeGraphQL:
 78		body, err := ioutil.ReadAll(r.Body)
 79		if err != nil {
 80			return &RequestOptions{}
 81		}
 82		return &RequestOptions{
 83			Query: string(body),
 84		}
 85	case ContentTypeFormURLEncoded:
 86		if err := r.ParseForm(); err != nil {
 87			return &RequestOptions{}
 88		}
 89
 90		if reqOpt := getFromForm(r.PostForm); reqOpt != nil {
 91			return reqOpt
 92		}
 93
 94		return &RequestOptions{}
 95
 96	case ContentTypeJSON:
 97		fallthrough
 98	default:
 99		var opts RequestOptions
100		body, err := ioutil.ReadAll(r.Body)
101		if err != nil {
102			return &opts
103		}
104		err = json.Unmarshal(body, &opts)
105		if err != nil {
106			// Probably `variables` was sent as a string instead of an object.
107			// So, we try to be polite and try to parse that as a JSON string
108			var optsCompatible requestOptionsCompatibility
109			json.Unmarshal(body, &optsCompatible)
110			json.Unmarshal([]byte(optsCompatible.Variables), &opts.Variables)
111		}
112		return &opts
113	}
114}
115
116// ContextHandler provides an entrypoint into executing graphQL queries with a
117// user-provided context.
118func (h *Handler) ContextHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
119	// get query
120	opts := NewRequestOptions(r)
121
122	// execute graphql query
123	params := graphql.Params{
124		Schema:         *h.Schema,
125		RequestString:  opts.Query,
126		VariableValues: opts.Variables,
127		OperationName:  opts.OperationName,
128		Context:        ctx,
129	}
130	result := graphql.Do(params)
131
132	if h.graphiql {
133		acceptHeader := r.Header.Get("Accept")
134		_, raw := r.URL.Query()["raw"]
135		if !raw && !strings.Contains(acceptHeader, "application/json") && strings.Contains(acceptHeader, "text/html") {
136			renderGraphiQL(w, params)
137			return
138		}
139	}
140
141	// use proper JSON Header
142	w.Header().Add("Content-Type", "application/json; charset=utf-8")
143
144	if h.pretty {
145		w.WriteHeader(http.StatusOK)
146		buff, _ := json.MarshalIndent(result, "", "\t")
147
148		w.Write(buff)
149	} else {
150		w.WriteHeader(http.StatusOK)
151		buff, _ := json.Marshal(result)
152
153		w.Write(buff)
154	}
155}
156
157// ServeHTTP provides an entrypoint into executing graphQL queries.
158func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
159	h.ContextHandler(r.Context(), w, r)
160}
161
162type Config struct {
163	Schema   *graphql.Schema
164	Pretty   bool
165	GraphiQL bool
166}
167
168func NewConfig() *Config {
169	return &Config{
170		Schema:   nil,
171		Pretty:   true,
172		GraphiQL: true,
173	}
174}
175
176func New(p *Config) *Handler {
177	if p == nil {
178		p = NewConfig()
179	}
180	if p.Schema == nil {
181		panic("undefined GraphQL schema")
182	}
183
184	return &Handler{
185		Schema:   p.Schema,
186		pretty:   p.Pretty,
187		graphiql: p.GraphiQL,
188	}
189}