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}