index.js 14 KB

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