index.js.flow 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. // @flow strict
  2. import { type IncomingMessage, type ServerResponse } from 'http';
  3. import url from 'url';
  4. import accepts from 'accepts';
  5. import httpError from 'http-errors';
  6. import {
  7. Source,
  8. parse,
  9. validate,
  10. execute,
  11. formatError,
  12. validateSchema,
  13. getOperationAST,
  14. specifiedRules,
  15. type ASTVisitor,
  16. type DocumentNode,
  17. type ValidationRule,
  18. type ValidationContext,
  19. type ExecutionArgs,
  20. type ExecutionResult,
  21. type GraphQLError,
  22. type GraphQLSchema,
  23. type GraphQLFieldResolver,
  24. type GraphQLTypeResolver,
  25. } from 'graphql';
  26. import { parseBody } from './parseBody';
  27. import { renderGraphiQL, type GraphiQLOptions } from './renderGraphiQL';
  28. type $Request = IncomingMessage;
  29. type $Response = ServerResponse & {| json?: ?(data: mixed) => void |};
  30. /**
  31. * Used to configure the graphqlHTTP middleware by providing a schema
  32. * and other configuration options.
  33. *
  34. * Options can be provided as an Object, a Promise for an Object, or a Function
  35. * that returns an Object or a Promise for an Object.
  36. */
  37. export type Options =
  38. | ((
  39. request: $Request,
  40. response: $Response,
  41. params?: GraphQLParams,
  42. ) => OptionsResult)
  43. | OptionsResult;
  44. export type OptionsResult = OptionsData | Promise<OptionsData>;
  45. export type OptionsData = {|
  46. /**
  47. * A GraphQL schema from graphql-js.
  48. */
  49. schema: GraphQLSchema,
  50. /**
  51. * A value to pass as the context to this middleware.
  52. */
  53. context?: ?mixed,
  54. /**
  55. * An object to pass as the rootValue to the graphql() function.
  56. */
  57. rootValue?: ?mixed,
  58. /**
  59. * A boolean to configure whether the output should be pretty-printed.
  60. */
  61. pretty?: ?boolean,
  62. /**
  63. * An optional array of validation rules that will be applied on the document
  64. * in additional to those defined by the GraphQL spec.
  65. */
  66. validationRules?: ?$ReadOnlyArray<(ValidationContext) => ASTVisitor>,
  67. /**
  68. * An optional function which will be used to validate instead of default `validate`
  69. * from `graphql-js`.
  70. */
  71. customValidateFn?: ?(
  72. schema: GraphQLSchema,
  73. documentAST: DocumentNode,
  74. rules: $ReadOnlyArray<ValidationRule>,
  75. ) => $ReadOnlyArray<GraphQLError>,
  76. /**
  77. * An optional function which will be used to execute instead of default `execute`
  78. * from `graphql-js`.
  79. */
  80. customExecuteFn?: ?(args: ExecutionArgs) => Promise<ExecutionResult>,
  81. /**
  82. * An optional function which will be used to format any errors produced by
  83. * fulfilling a GraphQL operation. If no function is provided, GraphQL's
  84. * default spec-compliant `formatError` function will be used.
  85. */
  86. customFormatErrorFn?: ?(error: GraphQLError) => mixed,
  87. /**
  88. * An optional function which will be used to create a document instead of
  89. * the default `parse` from `graphql-js`.
  90. */
  91. customParseFn?: ?(source: Source) => DocumentNode,
  92. /**
  93. * `formatError` is deprecated and replaced by `customFormatErrorFn`. It will
  94. * be removed in version 1.0.0.
  95. */
  96. formatError?: ?(error: GraphQLError) => mixed,
  97. /**
  98. * An optional function for adding additional metadata to the GraphQL response
  99. * as a key-value object. The result will be added to "extensions" field in
  100. * the resulting JSON. This is often a useful place to add development time
  101. * info such as the runtime of a query or the amount of resources consumed.
  102. *
  103. * Information about the request is provided to be used.
  104. *
  105. * This function may be async.
  106. */
  107. extensions?: ?(info: RequestInfo) => { [key: string]: mixed, ... },
  108. /**
  109. * A boolean to optionally enable GraphiQL mode.
  110. * Alternatively, instead of `true` you can pass in an options object.
  111. */
  112. graphiql?: ?boolean | ?GraphiQLOptions,
  113. /**
  114. * A resolver function to use when one is not provided by the schema.
  115. * If not provided, the default field resolver is used (which looks for a
  116. * value or method on the source value with the field's name).
  117. */
  118. fieldResolver?: ?GraphQLFieldResolver<mixed, mixed>,
  119. /**
  120. * A type resolver function to use when none is provided by the schema.
  121. * If not provided, the default type resolver is used (which looks for a
  122. * `__typename` field or alternatively calls the `isTypeOf` method).
  123. */
  124. typeResolver?: ?GraphQLTypeResolver<mixed, mixed>,
  125. |};
  126. /**
  127. * All information about a GraphQL request.
  128. */
  129. export type RequestInfo = {|
  130. /**
  131. * The parsed GraphQL document.
  132. */
  133. document: ?DocumentNode,
  134. /**
  135. * The variable values used at runtime.
  136. */
  137. variables: ?{ +[name: string]: mixed, ... },
  138. /**
  139. * The (optional) operation name requested.
  140. */
  141. operationName: ?string,
  142. /**
  143. * The result of executing the operation.
  144. */
  145. result: ?mixed,
  146. /**
  147. * A value to pass as the context to the graphql() function.
  148. */
  149. context?: ?mixed,
  150. |};
  151. type Middleware = (request: $Request, response: $Response) => Promise<void>;
  152. /**
  153. * Middleware for express; takes an options object or function as input to
  154. * configure behavior, and returns an express middleware.
  155. */
  156. module.exports = graphqlHTTP;
  157. function graphqlHTTP(options: Options): Middleware {
  158. if (!options) {
  159. throw new Error('GraphQL middleware requires options.');
  160. }
  161. return function graphqlMiddleware(request: $Request, response: $Response) {
  162. // Higher scoped variables are referred to at various stages in the
  163. // asynchronous state machine below.
  164. let context;
  165. let params;
  166. let pretty;
  167. let formatErrorFn = formatError;
  168. let validateFn = validate;
  169. let executeFn = execute;
  170. let parseFn = parse;
  171. let extensionsFn;
  172. let showGraphiQL = false;
  173. let query;
  174. let documentAST;
  175. let variables;
  176. let operationName;
  177. // Promises are used as a mechanism for capturing any thrown errors during
  178. // the asynchronous process below.
  179. // Parse the Request to get GraphQL request parameters.
  180. return getGraphQLParams(request)
  181. .then(
  182. graphQLParams => {
  183. params = graphQLParams;
  184. // Then, resolve the Options to get OptionsData.
  185. return resolveOptions(params);
  186. },
  187. error => {
  188. // When we failed to parse the GraphQL parameters, we still need to get
  189. // the options object, so make an options call to resolve just that.
  190. const dummyParams = {
  191. query: null,
  192. variables: null,
  193. operationName: null,
  194. raw: null,
  195. };
  196. return resolveOptions(dummyParams).then(() => Promise.reject(error));
  197. },
  198. )
  199. .then(optionsData => {
  200. // Assert that schema is required.
  201. if (!optionsData.schema) {
  202. throw new Error('GraphQL middleware options must contain a schema.');
  203. }
  204. // Collect information from the options data object.
  205. const schema = optionsData.schema;
  206. const rootValue = optionsData.rootValue;
  207. const fieldResolver = optionsData.fieldResolver;
  208. const typeResolver = optionsData.typeResolver;
  209. const validationRules = optionsData.validationRules || [];
  210. const graphiql = optionsData.graphiql;
  211. context = optionsData.context || request;
  212. // GraphQL HTTP only supports GET and POST methods.
  213. if (request.method !== 'GET' && request.method !== 'POST') {
  214. response.setHeader('Allow', 'GET, POST');
  215. throw httpError(405, 'GraphQL only supports GET and POST requests.');
  216. }
  217. // Get GraphQL params from the request and POST body data.
  218. query = params.query;
  219. variables = params.variables;
  220. operationName = params.operationName;
  221. showGraphiQL = canDisplayGraphiQL(request, params) && graphiql;
  222. // If there is no query, but GraphiQL will be displayed, do not produce
  223. // a result, otherwise return a 400: Bad Request.
  224. if (!query) {
  225. if (showGraphiQL) {
  226. return null;
  227. }
  228. throw httpError(400, 'Must provide query string.');
  229. }
  230. // Validate Schema
  231. const schemaValidationErrors = validateSchema(schema);
  232. if (schemaValidationErrors.length > 0) {
  233. // Return 500: Internal Server Error if invalid schema.
  234. response.statusCode = 500;
  235. return { errors: schemaValidationErrors };
  236. }
  237. // GraphQL source.
  238. const source = new Source(query, 'GraphQL request');
  239. // Parse source to AST, reporting any syntax error.
  240. try {
  241. documentAST = parseFn(source);
  242. } catch (syntaxError) {
  243. // Return 400: Bad Request if any syntax errors errors exist.
  244. response.statusCode = 400;
  245. return { errors: [syntaxError] };
  246. }
  247. // Validate AST, reporting any errors.
  248. const validationErrors = validateFn(schema, documentAST, [
  249. ...specifiedRules,
  250. ...validationRules,
  251. ]);
  252. if (validationErrors.length > 0) {
  253. // Return 400: Bad Request if any validation errors exist.
  254. response.statusCode = 400;
  255. return { errors: validationErrors };
  256. }
  257. // Only query operations are allowed on GET requests.
  258. if (request.method === 'GET') {
  259. // Determine if this GET request will perform a non-query.
  260. const operationAST = getOperationAST(documentAST, operationName);
  261. if (operationAST && operationAST.operation !== 'query') {
  262. // If GraphiQL can be shown, do not perform this query, but
  263. // provide it to GraphiQL so that the requester may perform it
  264. // themselves if desired.
  265. if (showGraphiQL) {
  266. return null;
  267. }
  268. // Otherwise, report a 405: Method Not Allowed error.
  269. response.setHeader('Allow', 'POST');
  270. throw httpError(
  271. 405,
  272. `Can only perform a ${operationAST.operation} operation from a POST request.`,
  273. );
  274. }
  275. }
  276. // Perform the execution, reporting any errors creating the context.
  277. try {
  278. return executeFn({
  279. schema,
  280. document: documentAST,
  281. rootValue,
  282. contextValue: context,
  283. variableValues: variables,
  284. operationName,
  285. fieldResolver,
  286. typeResolver,
  287. });
  288. } catch (contextError) {
  289. // Return 400: Bad Request if any execution context errors exist.
  290. response.statusCode = 400;
  291. return { errors: [contextError] };
  292. }
  293. })
  294. .then(result => {
  295. // Collect and apply any metadata extensions if a function was provided.
  296. // https://graphql.github.io/graphql-spec/#sec-Response-Format
  297. if (result && extensionsFn) {
  298. return Promise.resolve(
  299. extensionsFn({
  300. document: documentAST,
  301. variables,
  302. operationName,
  303. result,
  304. context,
  305. }),
  306. ).then(extensions => {
  307. if (extensions && typeof extensions === 'object') {
  308. (result: any).extensions = extensions;
  309. }
  310. return result;
  311. });
  312. }
  313. return result;
  314. })
  315. .catch(error => {
  316. // If an error was caught, report the httpError status, or 500.
  317. response.statusCode = error.status || 500;
  318. return { errors: [error] };
  319. })
  320. .then(result => {
  321. // If no data was included in the result, that indicates a runtime query
  322. // error, indicate as such with a generic status code.
  323. // Note: Information about the error itself will still be contained in
  324. // the resulting JSON payload.
  325. // https://graphql.github.io/graphql-spec/#sec-Data
  326. if (response.statusCode === 200 && result && !result.data) {
  327. response.statusCode = 500;
  328. }
  329. // Format any encountered errors.
  330. if (result && result.errors) {
  331. (result: any).errors = result.errors.map(formatErrorFn);
  332. }
  333. // If allowed to show GraphiQL, present it instead of JSON.
  334. if (showGraphiQL) {
  335. const payload = renderGraphiQL({
  336. query,
  337. variables,
  338. operationName,
  339. result,
  340. options: typeof showGraphiQL !== 'boolean' ? showGraphiQL : {},
  341. });
  342. return sendResponse(response, 'text/html', payload);
  343. }
  344. // At this point, result is guaranteed to exist, as the only scenario
  345. // where it will not is when showGraphiQL is true.
  346. if (!result) {
  347. throw httpError(500, 'Internal Error');
  348. }
  349. // If "pretty" JSON isn't requested, and the server provides a
  350. // response.json method (express), use that directly.
  351. // Otherwise use the simplified sendResponse method.
  352. if (!pretty && typeof response.json === 'function') {
  353. response.json(result);
  354. } else {
  355. const payload = JSON.stringify(result, null, pretty ? 2 : 0);
  356. sendResponse(response, 'application/json', payload);
  357. }
  358. });
  359. async function resolveOptions(requestParams) {
  360. const optionsResult =
  361. typeof options === 'function'
  362. ? options(request, response, requestParams)
  363. : options;
  364. const optionsData = await optionsResult;
  365. // Assert that optionsData is in fact an Object.
  366. if (!optionsData || typeof optionsData !== 'object') {
  367. throw new Error(
  368. 'GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.',
  369. );
  370. }
  371. if (optionsData.formatError) {
  372. // eslint-disable-next-line no-console
  373. console.warn(
  374. '`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.',
  375. );
  376. }
  377. validateFn = optionsData.customValidateFn || validateFn;
  378. executeFn = optionsData.customExecuteFn || executeFn;
  379. parseFn = optionsData.customParseFn || parseFn;
  380. formatErrorFn =
  381. optionsData.customFormatErrorFn ||
  382. optionsData.formatError ||
  383. formatErrorFn;
  384. extensionsFn = optionsData.extensions;
  385. pretty = optionsData.pretty;
  386. return optionsData;
  387. }
  388. };
  389. }
  390. export type GraphQLParams = {|
  391. query: ?string,
  392. variables: ?{ +[name: string]: mixed, ... },
  393. operationName: ?string,
  394. raw: ?boolean,
  395. |};
  396. /**
  397. * Provided a "Request" provided by express or connect (typically a node style
  398. * HTTPClientRequest), Promise the GraphQL request parameters.
  399. */
  400. module.exports.getGraphQLParams = getGraphQLParams;
  401. async function getGraphQLParams(request: $Request): Promise<GraphQLParams> {
  402. const bodyData = await parseBody(request);
  403. const urlData = (request.url && url.parse(request.url, true).query) || {};
  404. return parseGraphQLParams(urlData, bodyData);
  405. }
  406. /**
  407. * Helper function to get the GraphQL params from the request.
  408. */
  409. function parseGraphQLParams(
  410. urlData: { [param: string]: mixed, ... },
  411. bodyData: { [param: string]: mixed, ... },
  412. ): GraphQLParams {
  413. // GraphQL Query string.
  414. let query = urlData.query || bodyData.query;
  415. if (typeof query !== 'string') {
  416. query = null;
  417. }
  418. // Parse the variables if needed.
  419. let variables = urlData.variables || bodyData.variables;
  420. if (variables && typeof variables === 'string') {
  421. try {
  422. variables = JSON.parse(variables);
  423. } catch (error) {
  424. throw httpError(400, 'Variables are invalid JSON.');
  425. }
  426. } else if (typeof variables !== 'object') {
  427. variables = null;
  428. }
  429. // Name of GraphQL operation to execute.
  430. let operationName = urlData.operationName || bodyData.operationName;
  431. if (typeof operationName !== 'string') {
  432. operationName = null;
  433. }
  434. const raw = urlData.raw !== undefined || bodyData.raw !== undefined;
  435. return { query, variables, operationName, raw };
  436. }
  437. /**
  438. * Helper function to determine if GraphiQL can be displayed.
  439. */
  440. function canDisplayGraphiQL(request: $Request, params: GraphQLParams): boolean {
  441. // If `raw` exists, GraphiQL mode is not enabled.
  442. // Allowed to show GraphiQL if not requested as raw and this request
  443. // prefers HTML over JSON.
  444. return !params.raw && accepts(request).types(['json', 'html']) === 'html';
  445. }
  446. /**
  447. * Helper function for sending a response using only the core Node server APIs.
  448. */
  449. function sendResponse(response: $Response, type: string, data: string): void {
  450. const chunk = Buffer.from(data, 'utf8');
  451. response.setHeader('Content-Type', type + '; charset=utf-8');
  452. response.setHeader('Content-Length', String(chunk.length));
  453. response.end(chunk);
  454. }