329 lines
14 KiB
JavaScript
329 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
* strict
|
|
*/
|
|
|
|
var _accepts = require('accepts');
|
|
|
|
var _accepts2 = _interopRequireDefault(_accepts);
|
|
|
|
var _graphql = require('graphql');
|
|
|
|
var _httpErrors = require('http-errors');
|
|
|
|
var _httpErrors2 = _interopRequireDefault(_httpErrors);
|
|
|
|
var _url = require('url');
|
|
|
|
var _url2 = _interopRequireDefault(_url);
|
|
|
|
var _parseBody = require('./parseBody');
|
|
|
|
var _renderGraphiQL = require('./renderGraphiQL');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
/**
|
|
* Middleware for express; takes an options object or function as input to
|
|
* configure behavior, and returns an express middleware.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Used to configure the graphqlHTTP middleware by providing a schema
|
|
* and other configuration options.
|
|
*
|
|
* Options can be provided as an Object, a Promise for an Object, or a Function
|
|
* that returns an Object or a Promise for an Object.
|
|
*/
|
|
|
|
|
|
/**
|
|
* All information about a GraphQL request.
|
|
*/
|
|
module.exports = graphqlHTTP;
|
|
function graphqlHTTP(options) {
|
|
if (!options) {
|
|
throw new Error('GraphQL middleware requires options.');
|
|
}
|
|
|
|
return function graphqlMiddleware(request, response) {
|
|
// Higher scoped variables are referred to at various stages in the
|
|
// asynchronous state machine below.
|
|
var context = void 0;
|
|
var params = void 0;
|
|
var pretty = void 0;
|
|
var formatErrorFn = void 0;
|
|
var extensionsFn = void 0;
|
|
var showGraphiQL = void 0;
|
|
var query = void 0;
|
|
|
|
var documentAST = void 0;
|
|
var variables = void 0;
|
|
var operationName = void 0;
|
|
|
|
// Promises are used as a mechanism for capturing any thrown errors during
|
|
// the asynchronous process below.
|
|
|
|
// Parse the Request to get GraphQL request parameters.
|
|
return getGraphQLParams(request).then(function (graphQLParams) {
|
|
params = graphQLParams;
|
|
// Then, resolve the Options to get OptionsData.
|
|
return resolveOptions(params);
|
|
}, function (error) {
|
|
// When we failed to parse the GraphQL parameters, we still need to get
|
|
// the options object, so make an options call to resolve just that.
|
|
var dummyParams = {
|
|
query: null,
|
|
variables: null,
|
|
operationName: null,
|
|
raw: null
|
|
};
|
|
return resolveOptions(dummyParams).then(function () {
|
|
return Promise.reject(error);
|
|
});
|
|
}).then(function (optionsData) {
|
|
// Assert that schema is required.
|
|
if (!optionsData.schema) {
|
|
throw new Error('GraphQL middleware options must contain a schema.');
|
|
}
|
|
|
|
// Collect information from the options data object.
|
|
var schema = optionsData.schema;
|
|
var rootValue = optionsData.rootValue;
|
|
var fieldResolver = optionsData.fieldResolver;
|
|
var graphiql = optionsData.graphiql;
|
|
|
|
context = optionsData.context || request;
|
|
|
|
var validationRules = _graphql.specifiedRules;
|
|
if (optionsData.validationRules) {
|
|
validationRules = validationRules.concat(optionsData.validationRules);
|
|
}
|
|
|
|
// GraphQL HTTP only supports GET and POST methods.
|
|
if (request.method !== 'GET' && request.method !== 'POST') {
|
|
response.setHeader('Allow', 'GET, POST');
|
|
throw (0, _httpErrors2.default)(405, 'GraphQL only supports GET and POST requests.');
|
|
}
|
|
|
|
// Get GraphQL params from the request and POST body data.
|
|
query = params.query;
|
|
variables = params.variables;
|
|
operationName = params.operationName;
|
|
showGraphiQL = graphiql && canDisplayGraphiQL(request, params);
|
|
|
|
// 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 (0, _httpErrors2.default)(400, 'Must provide query string.');
|
|
}
|
|
|
|
// Validate Schema
|
|
var schemaValidationErrors = (0, _graphql.validateSchema)(schema);
|
|
if (schemaValidationErrors.length > 0) {
|
|
// Return 500: Internal Server Error if invalid schema.
|
|
response.statusCode = 500;
|
|
return { errors: schemaValidationErrors };
|
|
}
|
|
|
|
// GraphQL source.
|
|
var source = new _graphql.Source(query, 'GraphQL request');
|
|
|
|
// Parse source to AST, reporting any syntax error.
|
|
try {
|
|
documentAST = (0, _graphql.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.
|
|
var validationErrors = (0, _graphql.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.
|
|
var operationAST = (0, _graphql.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 (0, _httpErrors2.default)(405, 'Can only perform a ' + operationAST.operation + ' operation ' + 'from a POST request.');
|
|
}
|
|
}
|
|
// Perform the execution, reporting any errors creating the context.
|
|
try {
|
|
return (0, _graphql.execute)(schema, documentAST, rootValue, context, variables, operationName, fieldResolver);
|
|
} catch (contextError) {
|
|
// Return 400: Bad Request if any execution context errors exist.
|
|
response.statusCode = 400;
|
|
return { errors: [contextError] };
|
|
}
|
|
}).then(function (result) {
|
|
// Collect and apply any metadata extensions if a function was provided.
|
|
// http://facebook.github.io/graphql/#sec-Response-Format
|
|
if (result && extensionsFn) {
|
|
return Promise.resolve(extensionsFn({
|
|
document: documentAST,
|
|
variables: variables,
|
|
operationName: operationName,
|
|
result: result,
|
|
context: context
|
|
})).then(function (extensions) {
|
|
if (extensions && (typeof extensions === 'undefined' ? 'undefined' : _typeof(extensions)) === 'object') {
|
|
result.extensions = extensions;
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
return result;
|
|
}).catch(function (error) {
|
|
// If an error was caught, report the httpError status, or 500.
|
|
response.statusCode = error.status || 500;
|
|
return { errors: [error] };
|
|
}).then(function (result) {
|
|
// If no data was included in the result, that indicates a runtime query
|
|
// error, indicate as such with a generic status code.
|
|
// Note: Information about the error itself will still be contained in
|
|
// the resulting JSON payload.
|
|
// http://facebook.github.io/graphql/#sec-Data
|
|
if (response.statusCode === 200 && result && !result.data) {
|
|
response.statusCode = 500;
|
|
}
|
|
// Format any encountered errors.
|
|
if (result && result.errors) {
|
|
result.errors = result.errors.map(formatErrorFn || _graphql.formatError);
|
|
}
|
|
|
|
// If allowed to show GraphiQL, present it instead of JSON.
|
|
if (showGraphiQL) {
|
|
var payload = (0, _renderGraphiQL.renderGraphiQL)({
|
|
query: query,
|
|
variables: variables,
|
|
operationName: operationName,
|
|
result: result
|
|
});
|
|
return sendResponse(response, 'text/html', payload);
|
|
}
|
|
|
|
// At this point, result is guaranteed to exist, as the only scenario
|
|
// where it will not is when showGraphiQL is true.
|
|
if (!result) {
|
|
throw (0, _httpErrors2.default)(500, 'Internal Error');
|
|
}
|
|
|
|
// If "pretty" JSON isn't requested, and the server provides a
|
|
// response.json method (express), use that directly.
|
|
// Otherwise use the simplified sendResponse method.
|
|
if (!pretty && typeof response.json === 'function') {
|
|
response.json(result);
|
|
} else {
|
|
var _payload = JSON.stringify(result, null, pretty ? 2 : 0);
|
|
sendResponse(response, 'application/json', _payload);
|
|
}
|
|
});
|
|
|
|
function resolveOptions(requestParams) {
|
|
return Promise.resolve(typeof options === 'function' ? options(request, response, requestParams) : options).then(function (optionsData) {
|
|
// Assert that optionsData is in fact an Object.
|
|
if (!optionsData || (typeof optionsData === 'undefined' ? 'undefined' : _typeof(optionsData)) !== 'object') {
|
|
throw new Error('GraphQL middleware option function must return an options object ' + 'or a promise which will be resolved to an options object.');
|
|
}
|
|
|
|
formatErrorFn = optionsData.formatError;
|
|
extensionsFn = optionsData.extensions;
|
|
pretty = optionsData.pretty;
|
|
return optionsData;
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Provided a "Request" provided by express or connect (typically a node style
|
|
* HTTPClientRequest), Promise the GraphQL request parameters.
|
|
*/
|
|
module.exports.getGraphQLParams = getGraphQLParams;
|
|
function getGraphQLParams(request) {
|
|
return (0, _parseBody.parseBody)(request).then(function (bodyData) {
|
|
var urlData = request.url && _url2.default.parse(request.url, true).query || {};
|
|
return parseGraphQLParams(urlData, bodyData);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper function to get the GraphQL params from the request.
|
|
*/
|
|
function parseGraphQLParams(urlData, bodyData) {
|
|
// GraphQL Query string.
|
|
var query = urlData.query || bodyData.query;
|
|
if (typeof query !== 'string') {
|
|
query = null;
|
|
}
|
|
|
|
// Parse the variables if needed.
|
|
var variables = urlData.variables || bodyData.variables;
|
|
if (variables && typeof variables === 'string') {
|
|
try {
|
|
variables = JSON.parse(variables);
|
|
} catch (error) {
|
|
throw (0, _httpErrors2.default)(400, 'Variables are invalid JSON.');
|
|
}
|
|
} else if ((typeof variables === 'undefined' ? 'undefined' : _typeof(variables)) !== 'object') {
|
|
variables = null;
|
|
}
|
|
|
|
// Name of GraphQL operation to execute.
|
|
var operationName = urlData.operationName || bodyData.operationName;
|
|
if (typeof operationName !== 'string') {
|
|
operationName = null;
|
|
}
|
|
|
|
var raw = urlData.raw !== undefined || bodyData.raw !== undefined;
|
|
|
|
return { query: query, variables: variables, operationName: operationName, raw: raw };
|
|
}
|
|
|
|
/**
|
|
* Helper function to determine if GraphiQL can be displayed.
|
|
*/
|
|
function canDisplayGraphiQL(request, params) {
|
|
// If `raw` exists, GraphiQL mode is not enabled.
|
|
// Allowed to show GraphiQL if not requested as raw and this request
|
|
// prefers HTML over JSON.
|
|
return !params.raw && (0, _accepts2.default)(request).types(['json', 'html']) === 'html';
|
|
}
|
|
|
|
/**
|
|
* Helper function for sending a response using only the core Node server APIs.
|
|
*/
|
|
function sendResponse(response, type, data) {
|
|
var chunk = new Buffer(data, 'utf8');
|
|
response.setHeader('Content-Type', type + '; charset=utf-8');
|
|
response.setHeader('Content-Length', String(chunk.length));
|
|
response.end(chunk);
|
|
} |