forked from nautilus/gateway
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhttp.go
200 lines (168 loc) · 5.83 KB
/
http.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package gateway
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"github.com/nautilus/graphql"
)
// QueryPOSTBody is the incoming payload when sending POST requests to the gateway
type QueryPOSTBody struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables"`
OperationName string `json:"operationName"`
}
func formatErrors(data map[string]interface{}, err error) map[string]interface{} {
// the final list of formatted errors
var errList graphql.ErrorList
// if the err is itself an error list
if list, ok := err.(graphql.ErrorList); ok {
errList = list
} else {
errList = graphql.ErrorList{
&graphql.Error{
Message: err.Error(),
},
}
}
return map[string]interface{}{
"data": data,
"errors": errList,
}
}
// GraphQLHandler returns a http.HandlerFunc that should be used as the
// primary endpoint for the gateway API. The endpoint will respond
// to queries on both GET and POST requests. POST requests can either be
// a single object with { query, variables, operationName } or a list
// of that object.
func (g *Gateway) GraphQLHandler(w http.ResponseWriter, r *http.Request) {
// this handler can handle multiple operations sent in the same query. Internally,
// it modules a single operation as a list of one.
operations := []*QueryPOSTBody{}
// the error we have encountered when extracting query input
var payloadErr error
// make our lives easier. track if we're in batch mode
batchMode := false
// if we got a GET request
if r.Method == http.MethodGet {
parameters := r.URL.Query()
// get the query parameter
if query, ok := parameters["query"]; ok {
// build a query obj
query := &QueryPOSTBody{
Query: query[0],
}
// include operationName
if variableInput, ok := parameters["variables"]; ok {
variables := map[string]interface{}{}
err := json.Unmarshal([]byte(variableInput[0]), &variables)
if err != nil {
payloadErr = errors.New("variables must be a json object")
}
// assign the variables to the payload
query.Variables = variables
}
// include operationName
if operationName, ok := parameters["operationName"]; ok {
query.OperationName = operationName[0]
}
// add the query to the list of operations
operations = append(operations, query)
} else {
// there was no query parameter
payloadErr = errors.New("must include query as parameter")
}
// or we got a POST request
} else if r.Method == http.MethodPost {
// read the full request body
body, err := ioutil.ReadAll(r.Body)
if err != nil {
payloadErr = fmt.Errorf("encountered error reading body: %s", err.Error())
}
// there are two possible options for receiving information from a post request
// the first is that the user provides an object in the form of { query, variables, operationName }
// the second option is a list of that object
singleQuery := &QueryPOSTBody{}
// if we were given a single object
if err = json.Unmarshal(body, &singleQuery); err == nil {
// add it to the list of operations
operations = append(operations, singleQuery)
// we weren't given an object
} else {
// but we could have been given a list
batch := []*QueryPOSTBody{}
if err = json.Unmarshal(body, &batch); err != nil {
payloadErr = fmt.Errorf("encountered error parsing body: %s", err.Error())
} else {
operations = batch
}
// we're in batch mode
batchMode = true
}
}
// if there was an error retrieving the payload
if payloadErr != nil {
// stringify the response
response, _ := json.Marshal(formatErrors(map[string]interface{}{}, payloadErr))
// send the error to the user
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write(response)
return
}
// we have to respond to each operation in the right order
results := []map[string]interface{}{}
// the status code to report
statusCode := http.StatusOK
for _, operation := range operations {
// the result of the operation
result := map[string]interface{}{}
// the result of the operation
if operation.Query == "" {
statusCode = http.StatusUnprocessableEntity
results = append(results, formatErrors(map[string]interface{}{}, errors.New("could not find query body")))
continue
}
// fire the query with the request context passed through to execution
result, err := g.Execute(r.Context(), operation.Query, operation.Variables)
if err != nil {
results = append(results, formatErrors(map[string]interface{}{}, err))
continue
}
// add this result to the list
results = append(results, map[string]interface{}{"data": result})
}
// the final result depends on whether we are executing in batch mode or not
var finalResponse interface{}
if batchMode {
finalResponse = results
} else {
finalResponse = results[0]
}
// serialized the response
response, err := json.Marshal(finalResponse)
if err != nil {
// if we couldn't serialize the response then we're in internal error territory
statusCode = http.StatusInternalServerError
response, err = json.Marshal(formatErrors(map[string]interface{}{}, err))
if err != nil {
response, _ = json.Marshal(formatErrors(map[string]interface{}{}, err))
}
}
// send the result to the user
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
fmt.Fprint(w, string(response))
}
// PlaygroundHandler returns a http.HandlerFunc which on GET requests shows
// the user an interface that they can use to interact with the API. On
// POSTs the endpoint executes the designated query
func (g *Gateway) PlaygroundHandler(w http.ResponseWriter, r *http.Request) {
// on POSTs, we have to send the request to the graphqlHandler
if r.Method == http.MethodPost {
g.GraphQLHandler(w, r)
return
}
// we are not handling a POST request so we have to show the user the playground
w.Write(playgroundContent)
}