index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. "use strict";
  2. var _url = _interopRequireDefault(require("url"));
  3. var _accepts = _interopRequireDefault(require("accepts"));
  4. var _httpErrors = _interopRequireDefault(require("http-errors"));
  5. var _graphql = require("graphql");
  6. var _parseBody = require("./parseBody");
  7. var _renderGraphiQL = require("./renderGraphiQL");
  8. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  9. /**
  10. * Middleware for express; takes an options object or function as input to
  11. * configure behavior, and returns an express middleware.
  12. */
  13. module.exports = graphqlHTTP;
  14. function graphqlHTTP(options) {
  15. if (!options) {
  16. throw new Error('GraphQL middleware requires options.');
  17. }
  18. return function graphqlMiddleware(request, response) {
  19. // Higher scoped variables are referred to at various stages in the
  20. // asynchronous state machine below.
  21. let context;
  22. let params;
  23. let pretty;
  24. let formatErrorFn = _graphql.formatError;
  25. let validateFn = _graphql.validate;
  26. let executeFn = _graphql.execute;
  27. let parseFn = _graphql.parse;
  28. let extensionsFn;
  29. let showGraphiQL = false;
  30. let query;
  31. let documentAST;
  32. let variables;
  33. let operationName; // Promises are used as a mechanism for capturing any thrown errors during
  34. // the asynchronous process below.
  35. // Parse the Request to get GraphQL request parameters.
  36. return getGraphQLParams(request).then(graphQLParams => {
  37. params = graphQLParams; // Then, resolve the Options to get OptionsData.
  38. return resolveOptions(params);
  39. }, error => {
  40. // When we failed to parse the GraphQL parameters, we still need to get
  41. // the options object, so make an options call to resolve just that.
  42. const dummyParams = {
  43. query: null,
  44. variables: null,
  45. operationName: null,
  46. raw: null
  47. };
  48. return resolveOptions(dummyParams).then(() => Promise.reject(error));
  49. }).then(optionsData => {
  50. // Assert that schema is required.
  51. if (!optionsData.schema) {
  52. throw new Error('GraphQL middleware options must contain a schema.');
  53. } // Collect information from the options data object.
  54. const schema = optionsData.schema;
  55. const rootValue = optionsData.rootValue;
  56. const fieldResolver = optionsData.fieldResolver;
  57. const typeResolver = optionsData.typeResolver;
  58. const validationRules = optionsData.validationRules || [];
  59. const graphiql = optionsData.graphiql;
  60. context = optionsData.context || request; // GraphQL HTTP only supports GET and POST methods.
  61. if (request.method !== 'GET' && request.method !== 'POST') {
  62. response.setHeader('Allow', 'GET, POST');
  63. throw (0, _httpErrors.default)(405, 'GraphQL only supports GET and POST requests.');
  64. } // Get GraphQL params from the request and POST body data.
  65. query = params.query;
  66. variables = params.variables;
  67. operationName = params.operationName;
  68. showGraphiQL = canDisplayGraphiQL(request, params) && graphiql; // If there is no query, but GraphiQL will be displayed, do not produce
  69. // a result, otherwise return a 400: Bad Request.
  70. if (!query) {
  71. if (showGraphiQL) {
  72. return null;
  73. }
  74. throw (0, _httpErrors.default)(400, 'Must provide query string.');
  75. } // Validate Schema
  76. const schemaValidationErrors = (0, _graphql.validateSchema)(schema);
  77. if (schemaValidationErrors.length > 0) {
  78. // Return 500: Internal Server Error if invalid schema.
  79. response.statusCode = 500;
  80. return {
  81. errors: schemaValidationErrors
  82. };
  83. } // GraphQL source.
  84. const source = new _graphql.Source(query, 'GraphQL request'); // Parse source to AST, reporting any syntax error.
  85. try {
  86. documentAST = parseFn(source);
  87. } catch (syntaxError) {
  88. // Return 400: Bad Request if any syntax errors errors exist.
  89. response.statusCode = 400;
  90. return {
  91. errors: [syntaxError]
  92. };
  93. } // Validate AST, reporting any errors.
  94. const validationErrors = validateFn(schema, documentAST, [..._graphql.specifiedRules, ...validationRules]);
  95. if (validationErrors.length > 0) {
  96. // Return 400: Bad Request if any validation errors exist.
  97. response.statusCode = 400;
  98. return {
  99. errors: validationErrors
  100. };
  101. } // Only query operations are allowed on GET requests.
  102. if (request.method === 'GET') {
  103. // Determine if this GET request will perform a non-query.
  104. const operationAST = (0, _graphql.getOperationAST)(documentAST, operationName);
  105. if (operationAST && operationAST.operation !== 'query') {
  106. // If GraphiQL can be shown, do not perform this query, but
  107. // provide it to GraphiQL so that the requester may perform it
  108. // themselves if desired.
  109. if (showGraphiQL) {
  110. return null;
  111. } // Otherwise, report a 405: Method Not Allowed error.
  112. response.setHeader('Allow', 'POST');
  113. throw (0, _httpErrors.default)(405, `Can only perform a ${operationAST.operation} operation from a POST request.`);
  114. }
  115. } // Perform the execution, reporting any errors creating the context.
  116. try {
  117. return executeFn({
  118. schema,
  119. document: documentAST,
  120. rootValue,
  121. contextValue: context,
  122. variableValues: variables,
  123. operationName,
  124. fieldResolver,
  125. typeResolver
  126. });
  127. } catch (contextError) {
  128. // Return 400: Bad Request if any execution context errors exist.
  129. response.statusCode = 400;
  130. return {
  131. errors: [contextError]
  132. };
  133. }
  134. }).then(result => {
  135. // Collect and apply any metadata extensions if a function was provided.
  136. // https://graphql.github.io/graphql-spec/#sec-Response-Format
  137. if (result && extensionsFn) {
  138. return Promise.resolve(extensionsFn({
  139. document: documentAST,
  140. variables,
  141. operationName,
  142. result,
  143. context
  144. })).then(extensions => {
  145. if (extensions && typeof extensions === 'object') {
  146. result.extensions = extensions;
  147. }
  148. return result;
  149. });
  150. }
  151. return result;
  152. }).catch(error => {
  153. // If an error was caught, report the httpError status, or 500.
  154. response.statusCode = error.status || 500;
  155. return {
  156. errors: [error]
  157. };
  158. }).then(result => {
  159. // If no data was included in the result, that indicates a runtime query
  160. // error, indicate as such with a generic status code.
  161. // Note: Information about the error itself will still be contained in
  162. // the resulting JSON payload.
  163. // https://graphql.github.io/graphql-spec/#sec-Data
  164. if (response.statusCode === 200 && result && !result.data) {
  165. response.statusCode = 500;
  166. } // Format any encountered errors.
  167. if (result && result.errors) {
  168. result.errors = result.errors.map(formatErrorFn);
  169. } // If allowed to show GraphiQL, present it instead of JSON.
  170. if (showGraphiQL) {
  171. const payload = (0, _renderGraphiQL.renderGraphiQL)({
  172. query,
  173. variables,
  174. operationName,
  175. result,
  176. options: typeof showGraphiQL !== 'boolean' ? showGraphiQL : {}
  177. });
  178. return sendResponse(response, 'text/html', payload);
  179. } // At this point, result is guaranteed to exist, as the only scenario
  180. // where it will not is when showGraphiQL is true.
  181. if (!result) {
  182. throw (0, _httpErrors.default)(500, 'Internal Error');
  183. } // If "pretty" JSON isn't requested, and the server provides a
  184. // response.json method (express), use that directly.
  185. // Otherwise use the simplified sendResponse method.
  186. if (!pretty && typeof response.json === 'function') {
  187. response.json(result);
  188. } else {
  189. const payload = JSON.stringify(result, null, pretty ? 2 : 0);
  190. sendResponse(response, 'application/json', payload);
  191. }
  192. });
  193. async function resolveOptions(requestParams) {
  194. const optionsResult = typeof options === 'function' ? options(request, response, requestParams) : options;
  195. const optionsData = await optionsResult; // Assert that optionsData is in fact an Object.
  196. if (!optionsData || typeof optionsData !== 'object') {
  197. throw new Error('GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.');
  198. }
  199. if (optionsData.formatError) {
  200. // eslint-disable-next-line no-console
  201. console.warn('`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.');
  202. }
  203. validateFn = optionsData.customValidateFn || validateFn;
  204. executeFn = optionsData.customExecuteFn || executeFn;
  205. parseFn = optionsData.customParseFn || parseFn;
  206. formatErrorFn = optionsData.customFormatErrorFn || optionsData.formatError || formatErrorFn;
  207. extensionsFn = optionsData.extensions;
  208. pretty = optionsData.pretty;
  209. return optionsData;
  210. }
  211. };
  212. }
  213. /**
  214. * Provided a "Request" provided by express or connect (typically a node style
  215. * HTTPClientRequest), Promise the GraphQL request parameters.
  216. */
  217. module.exports.getGraphQLParams = getGraphQLParams;
  218. async function getGraphQLParams(request) {
  219. const bodyData = await (0, _parseBody.parseBody)(request);
  220. const urlData = request.url && _url.default.parse(request.url, true).query || {};
  221. return parseGraphQLParams(urlData, bodyData);
  222. }
  223. /**
  224. * Helper function to get the GraphQL params from the request.
  225. */
  226. function parseGraphQLParams(urlData, bodyData) {
  227. // GraphQL Query string.
  228. let query = urlData.query || bodyData.query;
  229. if (typeof query !== 'string') {
  230. query = null;
  231. } // Parse the variables if needed.
  232. let variables = urlData.variables || bodyData.variables;
  233. if (variables && typeof variables === 'string') {
  234. try {
  235. variables = JSON.parse(variables);
  236. } catch (error) {
  237. throw (0, _httpErrors.default)(400, 'Variables are invalid JSON.');
  238. }
  239. } else if (typeof variables !== 'object') {
  240. variables = null;
  241. } // Name of GraphQL operation to execute.
  242. let operationName = urlData.operationName || bodyData.operationName;
  243. if (typeof operationName !== 'string') {
  244. operationName = null;
  245. }
  246. const raw = urlData.raw !== undefined || bodyData.raw !== undefined;
  247. return {
  248. query,
  249. variables,
  250. operationName,
  251. raw
  252. };
  253. }
  254. /**
  255. * Helper function to determine if GraphiQL can be displayed.
  256. */
  257. function canDisplayGraphiQL(request, params) {
  258. // If `raw` exists, GraphiQL mode is not enabled.
  259. // Allowed to show GraphiQL if not requested as raw and this request
  260. // prefers HTML over JSON.
  261. return !params.raw && (0, _accepts.default)(request).types(['json', 'html']) === 'html';
  262. }
  263. /**
  264. * Helper function for sending a response using only the core Node server APIs.
  265. */
  266. function sendResponse(response, type, data) {
  267. const chunk = Buffer.from(data, 'utf8');
  268. response.setHeader('Content-Type', type + '; charset=utf-8');
  269. response.setHeader('Content-Length', String(chunk.length));
  270. response.end(chunk);
  271. }