Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit 936acbe

Browse files
committed
Add rudimentary batched query support
1 parent 6dcf37a commit 936acbe

File tree

4 files changed

+146
-101
lines changed

4 files changed

+146
-101
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"lint": "eslint src",
5454
"check": "flow check",
5555
"build": "rm -rf dist/* && babel src --ignore __tests__ --out-dir dist",
56-
"watch": "babel --optional runtime resources/watch.js | node",
56+
"watch": "babel resources/watch.js | node",
5757
"cover": "babel-node node_modules/.bin/isparta cover --root src --report html node_modules/.bin/_mocha -- $npm_package_options_mocha",
5858
"cover:lcov": "babel-node node_modules/.bin/isparta cover --root src --report lcovonly node_modules/.bin/_mocha -- $npm_package_options_mocha",
5959
"preversion": "npm test"
@@ -62,6 +62,7 @@
6262
"accepts": "^1.3.0",
6363
"content-type": "^1.0.0",
6464
"http-errors": "^1.3.0",
65+
"object-assign": "^4.1.0",
6566
"raw-body": "^2.1.0"
6667
},
6768
"devDependencies": {

src/__tests__/http-test.js

+28-1
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,33 @@ describe('test harness', () => {
400400
);
401401
});
402402

403+
it('allows batched POST with JSON encoding', async () => {
404+
const app = server();
405+
406+
app.use(urlString(), graphqlHTTP({
407+
schema: TestSchema
408+
}));
409+
410+
const response = await request(app)
411+
.post(urlString()).send([
412+
{
413+
query: '{test}'
414+
},
415+
{
416+
query: 'query helloWho($who: String){ test(who: $who) }',
417+
variables: JSON.stringify({ who: 'Dolly' })
418+
},
419+
{
420+
query: 'query helloWho($who: String){ test(who: $who) }',
421+
variables: JSON.stringify({ who: 'Bob' })
422+
}
423+
]);
424+
425+
expect(response.text).to.equal(
426+
'[{"data":{"test":"Hello World"}},{"data":{"test":"Hello Dolly"}},{"data":{"test":"Hello Bob"}}]'
427+
);
428+
});
429+
403430
it('Allows sending a mutation via POST', async () => {
404431
const app = server();
405432

@@ -1016,7 +1043,7 @@ describe('test harness', () => {
10161043
request(app)
10171044
.post(urlString())
10181045
.set('Content-Type', 'application/json')
1019-
.send('[]')
1046+
.send('3')
10201047
);
10211048

10221049
expect(error.response.status).to.equal(400);

src/index.js

+99-81
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
specifiedRules
2020
} from 'graphql';
2121
import httpError from 'http-errors';
22+
import assign from 'object-assign';
2223
import url from 'url';
2324

2425
import { parseBody } from './parseBody';
@@ -103,7 +104,7 @@ export default function graphqlHTTP(options: Options): Middleware {
103104
let validationRules;
104105

105106
// Promises are used as a mechanism for capturing any thrown errors during
106-
// the asyncronous process below.
107+
// the asynchronous process below.
107108

108109
// Resolve the Options to get OptionsData.
109110
new Promise(resolve => {
@@ -150,102 +151,123 @@ export default function graphqlHTTP(options: Options): Middleware {
150151
// Parse the Request body.
151152
return parseBody(request);
152153
}).then(bodyData => {
153-
const urlData = request.url && url.parse(request.url, true).query || {};
154-
showGraphiQL = graphiql && canDisplayGraphiQL(request, urlData, bodyData);
155-
156-
// Get GraphQL params from the request and POST body data.
157-
const params = getGraphQLParams(urlData, bodyData);
158-
query = params.query;
159-
variables = params.variables;
160-
operationName = params.operationName;
161-
162-
// If there is no query, but GraphiQL will be displayed, do not produce
163-
// a result, otherwise return a 400: Bad Request.
164-
if (!query) {
165-
if (showGraphiQL) {
166-
return null;
154+
function executeQuery(requestData) {
155+
// Get GraphQL params from the request and POST body data.
156+
const params = getGraphQLParams(requestData);
157+
query = params.query;
158+
variables = params.variables;
159+
operationName = params.operationName;
160+
161+
// If there is no query, but GraphiQL will be displayed, do not produce
162+
// a result, otherwise return a 400: Bad Request.
163+
if (!query) {
164+
if (showGraphiQL) {
165+
return null;
166+
}
167+
throw httpError(400, 'Must provide query string.');
167168
}
168-
throw httpError(400, 'Must provide query string.');
169-
}
170169

171-
// GraphQL source.
172-
const source = new Source(query, 'GraphQL request');
173-
174-
// Parse source to AST, reporting any syntax error.
175-
let documentAST;
176-
try {
177-
documentAST = parse(source);
178-
} catch (syntaxError) {
179-
// Return 400: Bad Request if any syntax errors errors exist.
180-
response.statusCode = 400;
181-
return { errors: [ syntaxError ] };
182-
}
170+
// GraphQL source.
171+
const source = new Source(query, 'GraphQL request');
172+
173+
// Parse source to AST, reporting any syntax error.
174+
let documentAST;
175+
try {
176+
documentAST = parse(source);
177+
} catch (syntaxError) {
178+
// Return 400: Bad Request if any syntax errors errors exist.
179+
response.statusCode = 400;
180+
return { errors: [ syntaxError ] };
181+
}
183182

184-
// Validate AST, reporting any errors.
185-
const validationErrors = validate(schema, documentAST, validationRules);
186-
if (validationErrors.length > 0) {
187-
// Return 400: Bad Request if any validation errors exist.
188-
response.statusCode = 400;
189-
return { errors: validationErrors };
190-
}
183+
// Validate AST, reporting any errors.
184+
const validationErrors = validate(schema, documentAST, validationRules);
185+
if (validationErrors.length > 0) {
186+
// Return 400: Bad Request if any validation errors exist.
187+
response.statusCode = 400;
188+
return { errors: validationErrors };
189+
}
191190

192-
// Only query operations are allowed on GET requests.
193-
if (request.method === 'GET') {
194-
// Determine if this GET request will perform a non-query.
195-
const operationAST = getOperationAST(documentAST, operationName);
196-
if (operationAST && operationAST.operation !== 'query') {
197-
// If GraphiQL can be shown, do not perform this query, but
198-
// provide it to GraphiQL so that the requester may perform it
199-
// themselves if desired.
200-
if (showGraphiQL) {
201-
return null;
191+
// Only query operations are allowed on GET requests.
192+
if (request.method === 'GET') {
193+
// Determine if this GET request will perform a non-query.
194+
const operationAST = getOperationAST(documentAST, operationName);
195+
if (operationAST && operationAST.operation !== 'query') {
196+
// If GraphiQL can be shown, do not perform this query, but
197+
// provide it to GraphiQL so that the requester may perform it
198+
// themselves if desired.
199+
if (showGraphiQL) {
200+
return null;
201+
}
202+
203+
// Otherwise, report a 405: Method Not Allowed error.
204+
response.setHeader('Allow', 'POST');
205+
throw httpError(
206+
405,
207+
`Can only perform a ${operationAST.operation} operation ` +
208+
'from a POST request.'
209+
);
202210
}
211+
}
203212

204-
// Otherwise, report a 405: Method Not Allowed error.
205-
response.setHeader('Allow', 'POST');
206-
throw httpError(
207-
405,
208-
`Can only perform a ${operationAST.operation} operation ` +
209-
'from a POST request.'
213+
// Perform the execution, reporting any errors creating the context.
214+
try {
215+
return execute(
216+
schema,
217+
documentAST,
218+
rootValue,
219+
context,
220+
variables,
221+
operationName
210222
);
223+
} catch (contextError) {
224+
// Return 400: Bad Request if any execution context errors exist.
225+
response.statusCode = 400;
226+
return { errors: [ contextError ] };
211227
}
212228
}
213-
// Perform the execution, reporting any errors creating the context.
214-
try {
215-
return execute(
216-
schema,
217-
documentAST,
218-
rootValue,
219-
context,
220-
variables,
221-
operationName
222-
);
223-
} catch (contextError) {
224-
// Return 400: Bad Request if any execution context errors exist.
225-
response.statusCode = 400;
226-
return { errors: [ contextError ] };
229+
230+
if (Array.isArray(bodyData)) {
231+
// Body is an array. This is a batched query, so don't show GraphiQL.
232+
showGraphiQL = false;
233+
return Promise.all(bodyData.map(executeQuery));
227234
}
235+
236+
const urlData = request.url && url.parse(request.url, true).query || {};
237+
const requestData = assign(urlData, bodyData);
238+
showGraphiQL = graphiql && canDisplayGraphiQL(request, requestData);
239+
240+
return executeQuery(requestData);
228241
}).catch(error => {
229242
// If an error was caught, report the httpError status, or 500.
230243
response.statusCode = error.status || 500;
231244
return { errors: [ error ] };
232-
}).then(result => {
245+
}).then(results => {
246+
function formatResultErrors(result) {
247+
if (result && result.errors) {
248+
result.errors = result.errors.map(formatErrorFn || formatError);
249+
}
250+
}
251+
233252
// Format any encountered errors.
234-
if (result && result.errors) {
235-
result.errors = result.errors.map(formatErrorFn || formatError);
253+
if (Array.isArray(results)) {
254+
results.forEach(formatResultErrors);
255+
} else {
256+
formatResultErrors(results);
236257
}
258+
237259
// If allowed to show GraphiQL, present it instead of JSON.
238260
if (showGraphiQL) {
239261
const data = renderGraphiQL({
240262
query, variables,
241-
operationName, result
263+
operationName, results
242264
});
243265
response.setHeader('Content-Type', 'text/html');
244266
response.write(data);
245267
response.end();
246268
} else {
247269
// Otherwise, present JSON directly.
248-
const data = JSON.stringify(result, null, pretty ? 2 : 0);
270+
const data = JSON.stringify(results, null, pretty ? 2 : 0);
249271
response.setHeader('Content-Type', 'application/json');
250272
response.write(data);
251273
response.end();
@@ -263,12 +285,12 @@ type GraphQLParams = {
263285
/**
264286
* Helper function to get the GraphQL params from the request.
265287
*/
266-
function getGraphQLParams(urlData: Object, bodyData: Object): GraphQLParams {
288+
function getGraphQLParams(requestData: Object): GraphQLParams {
267289
// GraphQL Query string.
268-
const query = urlData.query || bodyData.query;
290+
const query = requestData.query;
269291

270292
// Parse the variables if needed.
271-
let variables = urlData.variables || bodyData.variables;
293+
let variables = requestData.variables;
272294
if (variables && typeof variables === 'string') {
273295
try {
274296
variables = JSON.parse(variables);
@@ -278,21 +300,17 @@ function getGraphQLParams(urlData: Object, bodyData: Object): GraphQLParams {
278300
}
279301

280302
// Name of GraphQL operation to execute.
281-
const operationName = urlData.operationName || bodyData.operationName;
303+
const operationName = requestData.operationName;
282304

283305
return { query, variables, operationName };
284306
}
285307

286308
/**
287309
* Helper function to determine if GraphiQL can be displayed.
288310
*/
289-
function canDisplayGraphiQL(
290-
request: Request,
291-
urlData: Object,
292-
bodyData: Object
293-
): boolean {
311+
function canDisplayGraphiQL(request: Request, requestData: Object): boolean {
294312
// If `raw` exists, GraphiQL mode is not enabled.
295-
const raw = urlData.raw !== undefined || bodyData.raw !== undefined;
313+
const raw = requestData.raw !== undefined;
296314
// Allowed to show GraphiQL if not requested as raw and this request
297315
// prefers HTML over JSON.
298316
return !raw && accepts(request).types([ 'json', 'html' ]) === 'html';

src/parseBody.js

+17-18
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,25 @@ export function parseBody(req: Request): Promise<Object> {
5858
}
5959

6060
function jsonEncodedParser(body) {
61-
if (jsonObjRegex.test(body)) {
62-
/* eslint-disable no-empty */
63-
try {
64-
return JSON.parse(body);
65-
} catch (error) {
66-
// Do nothing
61+
/* eslint-disable no-empty */
62+
try {
63+
const bodyParsed = JSON.parse(body);
64+
65+
if (Array.isArray(bodyParsed)) {
66+
// Ensure that every array element is an object.
67+
if (bodyParsed.every(element => (
68+
element instanceof Object && !Array.isArray(element)
69+
))) {
70+
return bodyParsed;
71+
}
72+
} if (bodyParsed instanceof Object) {
73+
return bodyParsed;
6774
}
68-
/* eslint-enable no-empty */
75+
} catch (error) {
76+
// Do nothing
6977
}
78+
/* eslint-enable no-empty */
79+
7080
throw httpError(400, 'POST body sent invalid JSON.');
7181
}
7282

@@ -78,17 +88,6 @@ function graphqlParser(body) {
7888
return { query: body };
7989
}
8090

81-
/**
82-
* RegExp to match an Object-opening brace "{" as the first non-space
83-
* in a string. Allowed whitespace is defined in RFC 7159:
84-
*
85-
* x20 Space
86-
* x09 Horizontal tab
87-
* x0A Line feed or New line
88-
* x0D Carriage return
89-
*/
90-
const jsonObjRegex = /^[\x20\x09\x0a\x0d]*\{/;
91-
9291
// Read and parse a request body.
9392
function read(req, typeInfo, parseFn, resolve, reject) {
9493
const charset = (typeInfo.parameters.charset || 'utf-8').toLowerCase();

0 commit comments

Comments
 (0)