Skip to content

Commit 0732752

Browse files
authored
Batch requests (#62)
* added test for multiple queries * tests pass * strengthened tests and made them pass * added test for body of batch response * tests pass
1 parent 507f10b commit 0732752

File tree

3 files changed

+191
-35
lines changed

3 files changed

+191
-35
lines changed

gateway.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ func New(sources []*graphql.RemoteSchema, configs ...Configurator) (*Gateway, er
158158
}
159159
}
160160

161+
// we should be able to ask for the id under a gateway field without going to another service
162+
// that requires that the gateway knows that it is a place it can get the `id`
163+
for _, field := range gateway.queryFields {
164+
urls.RegisterURL(field.Type.Name(), "id", internalSchemaLocation)
165+
}
166+
161167
// assign the computed values
162168
gateway.schema = schema
163169
gateway.fieldURLs = urls

http.go

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type QueryPOSTBody struct {
1717
OperationName string `json:"operationName"`
1818
}
1919

20-
func writeErrors(err error, w http.ResponseWriter) {
20+
func formatErrors(data map[string]interface{}, err error) map[string]interface{} {
2121
// the final list of formatted errors
2222
var errList graphql.ErrorList
2323

@@ -32,34 +32,36 @@ func writeErrors(err error, w http.ResponseWriter) {
3232
}
3333
}
3434

35-
response, err := json.Marshal(map[string]interface{}{
35+
return map[string]interface{}{
36+
"data": data,
3637
"errors": errList,
37-
})
38-
if err != nil {
39-
w.WriteHeader(http.StatusInternalServerError)
40-
writeErrors(err, w)
41-
return
4238
}
43-
44-
w.Write(response)
4539
}
4640

4741
// GraphQLHandler returns a http.HandlerFunc that should be used as the
4842
// primary endpoint for the gateway API. The endpoint will respond
49-
// to queries on both GET and POST requests.
43+
// to queries on both GET and POST requests. POST requests can either be
44+
// a single object with { query, variables, operationName } or a list
45+
// of that object.
5046
func (g *Gateway) GraphQLHandler(w http.ResponseWriter, r *http.Request) {
51-
// a place to store query params
52-
payload := QueryPOSTBody{}
47+
// this handler can handle multiple operations sent in the same query. Internally,
48+
// it modules a single operation as a list of one.
49+
operations := []*QueryPOSTBody{}
5350

5451
// the error we have encountered when extracting query input
5552
var payloadErr error
53+
// make our lives easier. track if we're in batch mode
54+
batchMode := false
5655

5756
// if we got a GET request
5857
if r.Method == http.MethodGet {
5958
parameters := r.URL.Query()
6059
// get the query parameter
6160
if query, ok := parameters["query"]; ok {
62-
payload.Query = query[0]
61+
// build a query obj
62+
query := &QueryPOSTBody{
63+
Query: query[0],
64+
}
6365

6466
// include operationName
6567
if variableInput, ok := parameters["variables"]; ok {
@@ -71,13 +73,17 @@ func (g *Gateway) GraphQLHandler(w http.ResponseWriter, r *http.Request) {
7173
}
7274

7375
// assign the variables to the payload
74-
payload.Variables = variables
76+
query.Variables = variables
7577
}
7678

7779
// include operationName
7880
if operationName, ok := parameters["operationName"]; ok {
79-
payload.OperationName = operationName[0]
81+
query.OperationName = operationName[0]
8082
}
83+
84+
// add the query to the list of operations
85+
operations = append(operations, query)
86+
8187
} else {
8288
// there was no query parameter
8389
payloadErr = errors.New("must include query as parameter")
@@ -90,43 +96,91 @@ func (g *Gateway) GraphQLHandler(w http.ResponseWriter, r *http.Request) {
9096
payloadErr = fmt.Errorf("encountered error reading body: %s", err.Error())
9197
}
9298

93-
err = json.Unmarshal(body, &payload)
94-
if err != nil {
95-
payloadErr = fmt.Errorf("encountered error parsing body: %s", err.Error())
99+
// there are two possible options for receiving information from a post request
100+
// the first is that the user provides an object in the form of { query, variables, operationName }
101+
// the second option is a list of that object
102+
103+
singleQuery := &QueryPOSTBody{}
104+
// if we were given a single object
105+
if err = json.Unmarshal(body, &singleQuery); err == nil {
106+
// add it to the list of operations
107+
operations = append(operations, singleQuery)
108+
// we weren't given an object
109+
} else {
110+
// but we could have been given a list
111+
batch := []*QueryPOSTBody{}
112+
113+
if err = json.Unmarshal(body, &batch); err != nil {
114+
payloadErr = fmt.Errorf("encountered error parsing body: %s", err.Error())
115+
} else {
116+
operations = batch
117+
}
118+
119+
// we're in batch mode
120+
batchMode = true
96121
}
97122
}
98123

99124
// if there was an error retrieving the payload
100125
if payloadErr != nil {
126+
// stringify the response
127+
response, _ := json.Marshal(formatErrors(map[string]interface{}{}, payloadErr))
128+
129+
// send the error to the user
101130
w.WriteHeader(http.StatusUnprocessableEntity)
102-
writeErrors(payloadErr, w)
131+
w.Write(response)
103132
return
104133
}
105134

106-
// if we dont have a query
107-
if payload.Query == "" {
108-
w.WriteHeader(http.StatusUnprocessableEntity)
109-
writeErrors(errors.New("could not find a query in request payload"), w)
110-
return
135+
// we have to respond to each operation in the right order
136+
results := []map[string]interface{}{}
137+
138+
// the status code to report
139+
statusCode := http.StatusOK
140+
141+
for _, operation := range operations {
142+
// the result of the operation
143+
result := map[string]interface{}{}
144+
145+
// the result of the operation
146+
if operation.Query == "" {
147+
statusCode = http.StatusUnprocessableEntity
148+
results = append(results, formatErrors(map[string]interface{}{}, errors.New("could not find query body")))
149+
continue
150+
}
151+
152+
// fire the query with the request context passed through to execution
153+
result, err := g.Execute(r.Context(), operation.Query, operation.Variables)
154+
if err != nil {
155+
results = append(results, formatErrors(map[string]interface{}{}, err))
156+
continue
157+
}
158+
159+
// add this result to the list
160+
results = append(results, map[string]interface{}{"data": result})
111161
}
112162

113-
// fire the query with the request context passed through to execution
114-
result, err := g.Execute(r.Context(), payload.Query, payload.Variables)
115-
if err != nil {
116-
writeErrors(err, w)
117-
return
163+
// the final result depends on whether we are executing in batch mode or not
164+
var finalResponse interface{}
165+
if batchMode {
166+
finalResponse = results
167+
} else {
168+
finalResponse = results[0]
118169
}
119170

120-
response, err := json.Marshal(map[string]interface{}{
121-
"data": result,
122-
})
171+
// serialized the response
172+
response, err := json.Marshal(finalResponse)
123173
if err != nil {
124-
w.WriteHeader(http.StatusInternalServerError)
125-
writeErrors(err, w)
126-
return
174+
// if we couldn't serialize the response then we're in internal error territory
175+
statusCode = http.StatusInternalServerError
176+
response, err = json.Marshal(formatErrors(map[string]interface{}{}, err))
177+
if err != nil {
178+
response, _ = json.Marshal(formatErrors(map[string]interface{}{}, err))
179+
}
127180
}
128181

129182
// send the result to the user
183+
w.WriteHeader(statusCode)
130184
fmt.Fprint(w, string(response))
131185
}
132186

http_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package gateway
22

33
import (
4+
"context"
5+
"encoding/json"
46
"errors"
57
"io/ioutil"
68
"net/http"
@@ -12,6 +14,7 @@ import (
1214

1315
"github.com/nautilus/graphql"
1416
"github.com/stretchr/testify/assert"
17+
"github.com/vektah/gqlparser/ast"
1518
)
1619

1720
func TestGraphQLHandler_postMissingQuery(t *testing.T) {
@@ -193,6 +196,99 @@ func TestPlaygroundHandler_postRequest(t *testing.T) {
193196
assert.Equal(t, http.StatusOK, response.StatusCode)
194197
}
195198

199+
func TestPlaygroundHandler_postRequestList(t *testing.T) {
200+
// and some schemas that the gateway wraps
201+
schema, err := graphql.LoadSchema(`
202+
type User {
203+
id: ID!
204+
}
205+
`)
206+
if err != nil {
207+
t.Error(err.Error())
208+
return
209+
}
210+
211+
// some fields to query
212+
aField := &QueryField{
213+
Name: "a",
214+
Type: ast.NamedType("User", &ast.Position{}),
215+
Resolver: func(ctx context.Context, arguments map[string]interface{}) (string, error) {
216+
return "a", nil
217+
},
218+
}
219+
bField := &QueryField{
220+
Name: "b",
221+
Type: ast.NamedType("User", &ast.Position{}),
222+
Resolver: func(ctx context.Context, arguments map[string]interface{}) (string, error) {
223+
return "b", nil
224+
},
225+
}
226+
227+
// instantiate the gateway
228+
gw, err := New([]*graphql.RemoteSchema{{URL: "url1", Schema: schema}}, WithQueryFields(aField, bField))
229+
if err != nil {
230+
t.Error(err.Error())
231+
return
232+
}
233+
234+
// we need to send a list of two queries ({ a } and { b }) and make sure they resolve in the right order
235+
236+
// the incoming request
237+
request := httptest.NewRequest("POST", "/graphql", strings.NewReader(`
238+
[
239+
{
240+
"query": "{ a { id } }"
241+
},
242+
{
243+
"query": "{ b { id } }"
244+
}
245+
]
246+
`))
247+
// a recorder so we can check what the handler responded with
248+
responseRecorder := httptest.NewRecorder()
249+
250+
// call the http hander
251+
gw.PlaygroundHandler(responseRecorder, request)
252+
// get the response from the handler
253+
response := responseRecorder.Result()
254+
255+
// make sure we got a successful response
256+
if !assert.Equal(t, http.StatusOK, response.StatusCode) {
257+
return
258+
}
259+
260+
// read the body
261+
body, err := ioutil.ReadAll(response.Body)
262+
if err != nil {
263+
t.Error(err.Error())
264+
return
265+
}
266+
267+
result := []map[string]interface{}{}
268+
err = json.Unmarshal(body, &result)
269+
if err != nil {
270+
t.Error(err.Error())
271+
return
272+
}
273+
274+
// we should have gotten 2 responses
275+
if !assert.Len(t, result, 2) {
276+
return
277+
}
278+
279+
// make sure there were no errors in the first query
280+
if firstQuery := result[0]; assert.Nil(t, firstQuery["errors"]) {
281+
// make sure it has the right id
282+
assert.Equal(t, map[string]interface{}{"a": map[string]interface{}{"id": "a"}}, firstQuery["data"])
283+
}
284+
285+
// make sure there were no errors in the second query
286+
if secondQuery := result[1]; assert.Nil(t, secondQuery["errors"]) {
287+
// make sure it has the right id
288+
assert.Equal(t, map[string]interface{}{"b": map[string]interface{}{"id": "b"}}, secondQuery["data"])
289+
}
290+
}
291+
196292
func TestPlaygroundHandler_getRequest(t *testing.T) {
197293
// a planner that always returns an error
198294
planner := &MockErrPlanner{Err: errors.New("Planning error")}

0 commit comments

Comments
 (0)