index.js.flow 16 KB

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