Skip to content

Commit 0a18558

Browse files
committed
Add support for batching several requests into one
Batch format compatible with ReactRelayNetworkLayer (https://github.com/nodkz/react-relay-network-layer)
1 parent d348ec8 commit 0a18558

File tree

3 files changed

+129
-31
lines changed

3 files changed

+129
-31
lines changed

graphene_django/tests/test_views.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,23 @@
88
from urllib.parse import urlencode
99

1010

11-
def url_string(**url_params):
12-
string = '/graphql'
13-
11+
def url_string(string='/graphql', **url_params):
1412
if url_params:
1513
string += '?' + urlencode(url_params)
1614

1715
return string
1816

1917

18+
def batch_url_string(**url_params):
19+
return url_string('/graphql/batch', **url_params)
20+
21+
2022
def response_json(response):
2123
return json.loads(response.content.decode())
2224

2325

2426
j = lambda **kwargs: json.dumps(kwargs)
27+
jl = lambda **kwargs: json.dumps([kwargs])
2528

2629

2730
def test_graphiql_is_enabled(client):
@@ -169,6 +172,17 @@ def test_allows_post_with_json_encoding(client):
169172
}
170173

171174

175+
def test_batch_allows_post_with_json_encoding(client):
176+
response = client.post(batch_url_string(), jl(id=1, query='{test}'), 'application/json')
177+
178+
assert response.status_code == 200
179+
assert response_json(response) == [{
180+
'id': 1,
181+
'payload': { 'data': {'test': "Hello World"} },
182+
'status': 200,
183+
}]
184+
185+
172186
def test_allows_sending_a_mutation_via_post(client):
173187
response = client.post(url_string(), j(query='mutation TestMutation { writeTest { test } }'), 'application/json')
174188

@@ -199,6 +213,22 @@ def test_supports_post_json_query_with_string_variables(client):
199213
}
200214

201215

216+
217+
def test_batch_supports_post_json_query_with_string_variables(client):
218+
response = client.post(batch_url_string(), jl(
219+
id=1,
220+
query='query helloWho($who: String){ test(who: $who) }',
221+
variables=json.dumps({'who': "Dolly"})
222+
), 'application/json')
223+
224+
assert response.status_code == 200
225+
assert response_json(response) == [{
226+
'id': 1,
227+
'payload': { 'data': {'test': "Hello Dolly"} },
228+
'status': 200,
229+
}]
230+
231+
202232
def test_supports_post_json_query_with_json_variables(client):
203233
response = client.post(url_string(), j(
204234
query='query helloWho($who: String){ test(who: $who) }',
@@ -211,6 +241,21 @@ def test_supports_post_json_query_with_json_variables(client):
211241
}
212242

213243

244+
def test_batch_supports_post_json_query_with_json_variables(client):
245+
response = client.post(batch_url_string(), jl(
246+
id=1,
247+
query='query helloWho($who: String){ test(who: $who) }',
248+
variables={'who': "Dolly"}
249+
), 'application/json')
250+
251+
assert response.status_code == 200
252+
assert response_json(response) == [{
253+
'id': 1,
254+
'payload': { 'data': {'test': "Hello Dolly"} },
255+
'status': 200,
256+
}]
257+
258+
214259
def test_supports_post_url_encoded_query_with_string_variables(client):
215260
response = client.post(url_string(), urlencode(dict(
216261
query='query helloWho($who: String){ test(who: $who) }',
@@ -285,6 +330,33 @@ def test_allows_post_with_operation_name(client):
285330
}
286331

287332

333+
def test_batch_allows_post_with_operation_name(client):
334+
response = client.post(batch_url_string(), jl(
335+
id=1,
336+
query='''
337+
query helloYou { test(who: "You"), ...shared }
338+
query helloWorld { test(who: "World"), ...shared }
339+
query helloDolly { test(who: "Dolly"), ...shared }
340+
fragment shared on QueryRoot {
341+
shared: test(who: "Everyone")
342+
}
343+
''',
344+
operationName='helloWorld'
345+
), 'application/json')
346+
347+
assert response.status_code == 200
348+
assert response_json(response) == [{
349+
'id': 1,
350+
'payload': {
351+
'data': {
352+
'test': 'Hello World',
353+
'shared': 'Hello Everyone'
354+
}
355+
},
356+
'status': 200,
357+
}]
358+
359+
288360
def test_allows_post_with_get_operation_name(client):
289361
response = client.post(url_string(
290362
operationName='helloWorld'

graphene_django/tests/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from ..views import GraphQLView
44

55
urlpatterns = [
6+
url(r'^graphql/batch', GraphQLView.as_view(batch=True)),
67
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
78
]

graphene_django/views.py

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ class GraphQLView(View):
6262
middleware = None
6363
root_value = None
6464
pretty = False
65+
batch = False
6566

66-
def __init__(self, schema=None, executor=None, middleware=None, root_value=None, graphiql=False, pretty=False):
67+
def __init__(self, schema=None, executor=None, middleware=None, root_value=None, graphiql=False, pretty=False,
68+
batch=False):
6769
if not schema:
6870
schema = graphene_settings.SCHEMA
6971

@@ -77,8 +79,10 @@ def __init__(self, schema=None, executor=None, middleware=None, root_value=None,
7779
self.root_value = root_value
7880
self.pretty = pretty
7981
self.graphiql = graphiql
82+
self.batch = batch
8083

8184
assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
85+
assert not all((graphiql, batch)), 'Use either graphiql or batch processing'
8286

8387
# noinspection PyUnusedLocal
8488
def get_root_value(self, request):
@@ -99,32 +103,12 @@ def dispatch(self, request, *args, **kwargs):
99103
data = self.parse_body(request)
100104
show_graphiql = self.graphiql and self.can_display_graphiql(request, data)
101105

102-
query, variables, operation_name = self.get_graphql_params(request, data)
103-
104-
execution_result = self.execute_graphql_request(
105-
request,
106-
data,
107-
query,
108-
variables,
109-
operation_name,
110-
show_graphiql
111-
)
112-
113-
if execution_result:
114-
response = {}
115-
116-
if execution_result.errors:
117-
response['errors'] = [self.format_error(e) for e in execution_result.errors]
118-
119-
if execution_result.invalid:
120-
status_code = 400
121-
else:
122-
status_code = 200
123-
response['data'] = execution_result.data
124-
125-
result = self.json_encode(request, response, pretty=show_graphiql)
106+
if self.batch:
107+
responses = [self.get_response(request, entry) for entry in data]
108+
result = '[{}]'.format(','.join([response[0] for response in responses]))
109+
status_code = max(responses, key=lambda response: response[1])[1]
126110
else:
127-
result = None
111+
result, status_code = self.get_response(request, data, show_graphiql)
128112

129113
if show_graphiql:
130114
return self.render_graphiql(
@@ -150,6 +134,43 @@ def dispatch(self, request, *args, **kwargs):
150134
})
151135
return response
152136

137+
def get_response(self, request, data, show_graphiql=False):
138+
query, variables, operation_name, id = self.get_graphql_params(request, data)
139+
140+
execution_result = self.execute_graphql_request(
141+
request,
142+
data,
143+
query,
144+
variables,
145+
operation_name,
146+
show_graphiql
147+
)
148+
149+
if execution_result:
150+
response = {}
151+
152+
if execution_result.errors:
153+
response['errors'] = [self.format_error(e) for e in execution_result.errors]
154+
155+
if execution_result.invalid:
156+
status_code = 400
157+
else:
158+
status_code = 200
159+
response['data'] = execution_result.data
160+
161+
if self.batch:
162+
response = {
163+
'id': id,
164+
'payload': response,
165+
'status': status_code,
166+
}
167+
168+
result = self.json_encode(request, response, pretty=show_graphiql)
169+
else:
170+
result = None
171+
172+
return result, status_code
173+
153174
def render_graphiql(self, request, **data):
154175
return render(request, self.graphiql_template, data)
155176

@@ -170,7 +191,10 @@ def parse_body(self, request):
170191
elif content_type == 'application/json':
171192
try:
172193
request_json = json.loads(request.body.decode('utf-8'))
173-
assert isinstance(request_json, dict)
194+
if self.batch:
195+
assert isinstance(request_json, list)
196+
else:
197+
assert isinstance(request_json, dict)
174198
return request_json
175199
except:
176200
raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.'))
@@ -242,6 +266,7 @@ def request_wants_html(cls, request):
242266
def get_graphql_params(request, data):
243267
query = request.GET.get('query') or data.get('query')
244268
variables = request.GET.get('variables') or data.get('variables')
269+
id = request.GET.get('id') or data.get('id')
245270

246271
if variables and isinstance(variables, six.text_type):
247272
try:
@@ -251,7 +276,7 @@ def get_graphql_params(request, data):
251276

252277
operation_name = request.GET.get('operationName') or data.get('operationName')
253278

254-
return query, variables, operation_name
279+
return query, variables, operation_name, id
255280

256281
@staticmethod
257282
def format_error(error):

0 commit comments

Comments
 (0)