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

Commit

Permalink
Add rudimentary batched query support
Browse files Browse the repository at this point in the history
  • Loading branch information
taion committed Jul 19, 2016
1 parent 6dcf37a commit cd720a3
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 101 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"lint": "eslint src",
"check": "flow check",
"build": "rm -rf dist/* && babel src --ignore __tests__ --out-dir dist",
"watch": "babel --optional runtime resources/watch.js | node",
"watch": "babel resources/watch.js | node",
"cover": "babel-node node_modules/.bin/isparta cover --root src --report html node_modules/.bin/_mocha -- $npm_package_options_mocha",
"cover:lcov": "babel-node node_modules/.bin/isparta cover --root src --report lcovonly node_modules/.bin/_mocha -- $npm_package_options_mocha",
"preversion": "npm test"
Expand All @@ -62,6 +62,7 @@
"accepts": "^1.3.0",
"content-type": "^1.0.0",
"http-errors": "^1.3.0",
"object-assign": "^4.1.0",
"raw-body": "^2.1.0"
},
"devDependencies": {
Expand Down
29 changes: 28 additions & 1 deletion src/__tests__/http-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,33 @@ describe('test harness', () => {
);
});

it('allows batched POST with JSON encoding', async () => {
const app = server();

app.use(urlString(), graphqlHTTP({
schema: TestSchema
}));

const response = await request(app)
.post(urlString()).send([
{
query: '{test}'
},
{
query: 'query helloWho($who: String){ test(who: $who) }',
variables: JSON.stringify({ who: 'Dolly' })
},
{
query: 'query helloWho($who: String){ test(who: $who) }',
variables: JSON.stringify({ who: 'Bob' })
}
]);

expect(response.text).to.equal(
'[{"data":{"test":"Hello World"}},{"data":{"test":"Hello Dolly"}},{"data":{"test":"Hello Bob"}}]'
);
});

it('Allows sending a mutation via POST', async () => {
const app = server();

Expand Down Expand Up @@ -1016,7 +1043,7 @@ describe('test harness', () => {
request(app)
.post(urlString())
.set('Content-Type', 'application/json')
.send('[]')
.send('3')
);

expect(error.response.status).to.equal(400);
Expand Down
180 changes: 99 additions & 81 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
specifiedRules
} from 'graphql';
import httpError from 'http-errors';
import assign from 'object-assign';
import url from 'url';

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

// Promises are used as a mechanism for capturing any thrown errors during
// the asyncronous process below.
// the asynchronous process below.

// Resolve the Options to get OptionsData.
new Promise(resolve => {
Expand Down Expand Up @@ -150,102 +151,123 @@ export default function graphqlHTTP(options: Options): Middleware {
// Parse the Request body.
return parseBody(request);
}).then(bodyData => {
const urlData = request.url && url.parse(request.url, true).query || {};
showGraphiQL = graphiql && canDisplayGraphiQL(request, urlData, bodyData);

// Get GraphQL params from the request and POST body data.
const params = getGraphQLParams(urlData, bodyData);
query = params.query;
variables = params.variables;
operationName = params.operationName;

// If there is no query, but GraphiQL will be displayed, do not produce
// a result, otherwise return a 400: Bad Request.
if (!query) {
if (showGraphiQL) {
return null;
function executeQuery(requestData) {
// Get GraphQL params from the request and POST body data.
const params = getGraphQLParams(requestData);
query = params.query;
variables = params.variables;
operationName = params.operationName;

// If there is no query, but GraphiQL will be displayed, do not produce
// a result, otherwise return a 400: Bad Request.
if (!query) {
if (showGraphiQL) {
return null;
}
throw httpError(400, 'Must provide query string.');
}
throw httpError(400, 'Must provide query string.');
}

// GraphQL source.
const source = new Source(query, 'GraphQL request');

// Parse source to AST, reporting any syntax error.
let documentAST;
try {
documentAST = parse(source);
} catch (syntaxError) {
// Return 400: Bad Request if any syntax errors errors exist.
response.statusCode = 400;
return { errors: [ syntaxError ] };
}
// GraphQL source.
const source = new Source(query, 'GraphQL request');

// Parse source to AST, reporting any syntax error.
let documentAST;
try {
documentAST = parse(source);
} catch (syntaxError) {
// Return 400: Bad Request if any syntax errors errors exist.
response.statusCode = 400;
return { errors: [ syntaxError ] };
}

// Validate AST, reporting any errors.
const validationErrors = validate(schema, documentAST, validationRules);
if (validationErrors.length > 0) {
// Return 400: Bad Request if any validation errors exist.
response.statusCode = 400;
return { errors: validationErrors };
}
// Validate AST, reporting any errors.
const validationErrors = validate(schema, documentAST, validationRules);
if (validationErrors.length > 0) {
// Return 400: Bad Request if any validation errors exist.
response.statusCode = 400;
return { errors: validationErrors };
}

// Only query operations are allowed on GET requests.
if (request.method === 'GET') {
// Determine if this GET request will perform a non-query.
const operationAST = getOperationAST(documentAST, operationName);
if (operationAST && operationAST.operation !== 'query') {
// If GraphiQL can be shown, do not perform this query, but
// provide it to GraphiQL so that the requester may perform it
// themselves if desired.
if (showGraphiQL) {
return null;
// Only query operations are allowed on GET requests.
if (request.method === 'GET') {
// Determine if this GET request will perform a non-query.
const operationAST = getOperationAST(documentAST, operationName);
if (operationAST && operationAST.operation !== 'query') {
// If GraphiQL can be shown, do not perform this query, but
// provide it to GraphiQL so that the requester may perform it
// themselves if desired.
if (showGraphiQL) {
return null;
}

// Otherwise, report a 405: Method Not Allowed error.
response.setHeader('Allow', 'POST');
throw httpError(
405,
`Can only perform a ${operationAST.operation} operation ` +
'from a POST request.'
);
}
}

// Otherwise, report a 405: Method Not Allowed error.
response.setHeader('Allow', 'POST');
throw httpError(
405,
`Can only perform a ${operationAST.operation} operation ` +
'from a POST request.'
// Perform the execution, reporting any errors creating the context.
try {
return execute(
schema,
documentAST,
rootValue,
context,
variables,
operationName
);
} catch (contextError) {
// Return 400: Bad Request if any execution context errors exist.
response.statusCode = 400;
return { errors: [ contextError ] };
}
}
// Perform the execution, reporting any errors creating the context.
try {
return execute(
schema,
documentAST,
rootValue,
context,
variables,
operationName
);
} catch (contextError) {
// Return 400: Bad Request if any execution context errors exist.
response.statusCode = 400;
return { errors: [ contextError ] };

if (Array.isArray(bodyData)) {
// Body is an array. This is a batched query, so don't show GraphiQL.
showGraphiQL = false;
return Promise.all(bodyData.map(executeQuery));
}

const urlData = request.url && url.parse(request.url, true).query || {};
const requestData = assign(urlData, bodyData);
showGraphiQL = graphiql && canDisplayGraphiQL(request, requestData);

return executeQuery(requestData);
}).catch(error => {
// If an error was caught, report the httpError status, or 500.
response.statusCode = error.status || 500;
return { errors: [ error ] };
}).then(result => {
}).then(results => {
function formatResultErrors(result) {
if (result && result.errors) {
result.errors = result.errors.map(formatErrorFn || formatError);
}
}

// Format any encountered errors.
if (result && result.errors) {
result.errors = result.errors.map(formatErrorFn || formatError);
if (Array.isArray(results)) {
results.forEach(formatResultErrors);
} else {
formatResultErrors(results);
}

// If allowed to show GraphiQL, present it instead of JSON.
if (showGraphiQL) {
const data = renderGraphiQL({
query, variables,
operationName, result
operationName, result: results
});
response.setHeader('Content-Type', 'text/html');
response.write(data);
response.end();
} else {
// Otherwise, present JSON directly.
const data = JSON.stringify(result, null, pretty ? 2 : 0);
const data = JSON.stringify(results, null, pretty ? 2 : 0);
response.setHeader('Content-Type', 'application/json');
response.write(data);
response.end();
Expand All @@ -263,12 +285,12 @@ type GraphQLParams = {
/**
* Helper function to get the GraphQL params from the request.
*/
function getGraphQLParams(urlData: Object, bodyData: Object): GraphQLParams {
function getGraphQLParams(requestData: Object): GraphQLParams {
// GraphQL Query string.
const query = urlData.query || bodyData.query;
const query = requestData.query;

// Parse the variables if needed.
let variables = urlData.variables || bodyData.variables;
let variables = requestData.variables;
if (variables && typeof variables === 'string') {
try {
variables = JSON.parse(variables);
Expand All @@ -278,21 +300,17 @@ function getGraphQLParams(urlData: Object, bodyData: Object): GraphQLParams {
}

// Name of GraphQL operation to execute.
const operationName = urlData.operationName || bodyData.operationName;
const operationName = requestData.operationName;

return { query, variables, operationName };
}

/**
* Helper function to determine if GraphiQL can be displayed.
*/
function canDisplayGraphiQL(
request: Request,
urlData: Object,
bodyData: Object
): boolean {
function canDisplayGraphiQL(request: Request, requestData: Object): boolean {
// If `raw` exists, GraphiQL mode is not enabled.
const raw = urlData.raw !== undefined || bodyData.raw !== undefined;
const raw = requestData.raw !== undefined;
// Allowed to show GraphiQL if not requested as raw and this request
// prefers HTML over JSON.
return !raw && accepts(request).types([ 'json', 'html' ]) === 'html';
Expand Down
35 changes: 17 additions & 18 deletions src/parseBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,25 @@ export function parseBody(req: Request): Promise<Object> {
}

function jsonEncodedParser(body) {
if (jsonObjRegex.test(body)) {
/* eslint-disable no-empty */
try {
return JSON.parse(body);
} catch (error) {
// Do nothing
/* eslint-disable no-empty */
try {
const bodyParsed = JSON.parse(body);

if (Array.isArray(bodyParsed)) {
// Ensure that every array element is an object.
if (bodyParsed.every(element => (
element instanceof Object && !Array.isArray(element)
))) {
return bodyParsed;
}
} if (bodyParsed instanceof Object) {
return bodyParsed;
}
/* eslint-enable no-empty */
} catch (error) {
// Do nothing
}
/* eslint-enable no-empty */

throw httpError(400, 'POST body sent invalid JSON.');
}

Expand All @@ -78,17 +88,6 @@ function graphqlParser(body) {
return { query: body };
}

/**
* RegExp to match an Object-opening brace "{" as the first non-space
* in a string. Allowed whitespace is defined in RFC 7159:
*
* x20 Space
* x09 Horizontal tab
* x0A Line feed or New line
* x0D Carriage return
*/
const jsonObjRegex = /^[\x20\x09\x0a\x0d]*\{/;

// Read and parse a request body.
function read(req, typeInfo, parseFn, resolve, reject) {
const charset = (typeInfo.parameters.charset || 'utf-8').toLowerCase();
Expand Down

0 comments on commit cd720a3

Please sign in to comment.