"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.graphqlHTTP = graphqlHTTP; exports.getGraphQLParams = getGraphQLParams; var _accepts = _interopRequireDefault(require("accepts")); var _httpErrors = _interopRequireDefault(require("http-errors")); var _graphql = require("graphql"); 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. */ function graphqlHTTP(options) { if (!options) { throw new Error('GraphQL middleware requires options.'); } return async function graphqlMiddleware(request, response) { var _result$errors; // Higher scoped variables are referred to at various stages in the asynchronous state machine below. let params; let showGraphiQL = false; let graphiqlOptions; let formatErrorFn = _graphql.formatError; let pretty = false; let result; try { var _optionsData$validati, _optionsData$graphiql, _optionsData$context, _optionsData$customPa, _optionsData$customEx, _optionsData$customVa, _optionsData$pretty2, _ref2, _optionsData$customFo2; // Parse the Request to get GraphQL request parameters. try { params = await getGraphQLParams(request); } catch (error) { var _optionsData$pretty, _ref, _optionsData$customFo; // 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. const optionsData = await resolveOptions(); pretty = (_optionsData$pretty = optionsData.pretty) !== null && _optionsData$pretty !== void 0 ? _optionsData$pretty : false; formatErrorFn = (_ref = (_optionsData$customFo = optionsData.customFormatErrorFn) !== null && _optionsData$customFo !== void 0 ? _optionsData$customFo : optionsData.formatError) !== null && _ref !== void 0 ? _ref : formatErrorFn; throw error; } // Then, resolve the Options to get OptionsData. const optionsData = await resolveOptions(params); // Collect information from the options data object. const schema = optionsData.schema; const rootValue = optionsData.rootValue; const validationRules = (_optionsData$validati = optionsData.validationRules) !== null && _optionsData$validati !== void 0 ? _optionsData$validati : []; const fieldResolver = optionsData.fieldResolver; const typeResolver = optionsData.typeResolver; const graphiql = (_optionsData$graphiql = optionsData.graphiql) !== null && _optionsData$graphiql !== void 0 ? _optionsData$graphiql : false; const extensionsFn = optionsData.extensions; const context = (_optionsData$context = optionsData.context) !== null && _optionsData$context !== void 0 ? _optionsData$context : request; const parseFn = (_optionsData$customPa = optionsData.customParseFn) !== null && _optionsData$customPa !== void 0 ? _optionsData$customPa : _graphql.parse; const executeFn = (_optionsData$customEx = optionsData.customExecuteFn) !== null && _optionsData$customEx !== void 0 ? _optionsData$customEx : _graphql.execute; const validateFn = (_optionsData$customVa = optionsData.customValidateFn) !== null && _optionsData$customVa !== void 0 ? _optionsData$customVa : _graphql.validate; pretty = (_optionsData$pretty2 = optionsData.pretty) !== null && _optionsData$pretty2 !== void 0 ? _optionsData$pretty2 : false; formatErrorFn = (_ref2 = (_optionsData$customFo2 = optionsData.customFormatErrorFn) !== null && _optionsData$customFo2 !== void 0 ? _optionsData$customFo2 : optionsData.formatError) !== null && _ref2 !== void 0 ? _ref2 : formatErrorFn; // Assert that schema is required. if (schema == null) { throw (0, _httpErrors.default)(500, 'GraphQL middleware options must contain a schema.'); } // GraphQL HTTP only supports GET and POST methods. if (request.method !== 'GET' && request.method !== 'POST') { throw (0, _httpErrors.default)(405, 'GraphQL only supports GET and POST requests.', { headers: { Allow: 'GET, POST' } }); } // Get GraphQL params from the request and POST body data. const { query, variables, operationName } = params; showGraphiQL = canDisplayGraphiQL(request, params) && graphiql !== false; if (typeof graphiql !== 'boolean') { graphiqlOptions = graphiql; } // If there is no query, but GraphiQL will be displayed, do not produce // a result, otherwise return a 400: Bad Request. if (query == null) { if (showGraphiQL) { return respondWithGraphiQL(response, graphiqlOptions); } throw (0, _httpErrors.default)(400, 'Must provide query string.'); } // Validate Schema const schemaValidationErrors = (0, _graphql.validateSchema)(schema); if (schemaValidationErrors.length > 0) { // Return 500: Internal Server Error if invalid schema. throw (0, _httpErrors.default)(500, 'GraphQL schema validation error.', { graphqlErrors: schemaValidationErrors }); } // Parse source to AST, reporting any syntax error. let documentAST; try { documentAST = parseFn(new _graphql.Source(query, 'GraphQL request')); } catch (syntaxError) { // Return 400: Bad Request if any syntax errors errors exist. throw (0, _httpErrors.default)(400, 'GraphQL syntax error.', { graphqlErrors: [syntaxError] }); } // Validate AST, reporting any errors. const validationErrors = validateFn(schema, documentAST, [..._graphql.specifiedRules, ...validationRules]); if (validationErrors.length > 0) { // Return 400: Bad Request if any validation errors exist. throw (0, _httpErrors.default)(400, 'GraphQL validation error.', { graphqlErrors: validationErrors }); } // Only query operations are allowed on GET requests. if (request.method === 'GET') { // Determine if this GET request will perform a non-query. const 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 respondWithGraphiQL(response, graphiqlOptions, params); } // Otherwise, report a 405: Method Not Allowed error. throw (0, _httpErrors.default)(405, `Can only perform a ${operationAST.operation} operation from a POST request.`, { headers: { Allow: 'POST' } }); } } // Perform the execution, reporting any errors creating the context. try { result = await executeFn({ schema, document: documentAST, rootValue, contextValue: context, variableValues: variables, operationName, fieldResolver, typeResolver }); } catch (contextError) { // Return 400: Bad Request if any execution context errors exist. throw (0, _httpErrors.default)(400, 'GraphQL execution context error.', { graphqlErrors: [contextError] }); } // Collect and apply any metadata extensions if a function was provided. // https://graphql.github.io/graphql-spec/#sec-Response-Format if (extensionsFn) { const extensions = await extensionsFn({ document: documentAST, variables, operationName, result, context }); if (extensions != null) { result = { ...result, extensions }; } } } catch (error) { var _error$status, _error$graphqlErrors; // If an error was caught, report the httpError status, or 500. response.statusCode = (_error$status = error.status) !== null && _error$status !== void 0 ? _error$status : 500; if (error.headers != null) { for (const [key, value] of Object.entries(error.headers)) { response.setHeader(key, value); } } result = { data: undefined, errors: (_error$graphqlErrors = error.graphqlErrors) !== null && _error$graphqlErrors !== void 0 ? _error$graphqlErrors : [error] }; } // 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. // https://graphql.github.io/graphql-spec/#sec-Data if (response.statusCode === 200 && result.data == null) { response.statusCode = 500; } // Format any encountered errors. const formattedResult = { ...result, errors: (_result$errors = result.errors) === null || _result$errors === void 0 ? void 0 : _result$errors.map(formatErrorFn) }; // If allowed to show GraphiQL, present it instead of JSON. if (showGraphiQL) { return respondWithGraphiQL(response, graphiqlOptions, params, formattedResult); } // 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(formattedResult); } else { const payload = JSON.stringify(formattedResult, null, pretty ? 2 : 0); sendResponse(response, 'application/json', payload); } async function resolveOptions(requestParams) { const optionsResult = await Promise.resolve(typeof options === 'function' ? options(request, response, requestParams) : options); // Assert that optionsData is in fact an Object. if (optionsResult == null || typeof optionsResult !== 'object') { throw new Error('GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.'); } if (optionsResult.formatError) { // eslint-disable-next-line no-console console.warn('`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.'); } return optionsResult; } }; } function respondWithGraphiQL(response, options, params, result) { const data = { query: params === null || params === void 0 ? void 0 : params.query, variables: params === null || params === void 0 ? void 0 : params.variables, operationName: params === null || params === void 0 ? void 0 : params.operationName, result }; const payload = (0, _renderGraphiQL.renderGraphiQL)(data, options); return sendResponse(response, 'text/html', payload); } /** * Provided a "Request" provided by express or connect (typically a node style * HTTPClientRequest), Promise the GraphQL request parameters. */ async function getGraphQLParams(request) { var _urlData$get, _urlData$get2; const urlData = new URLSearchParams(request.url.split('?')[1]); const bodyData = await (0, _parseBody.parseBody)(request); // GraphQL Query string. let query = (_urlData$get = urlData.get('query')) !== null && _urlData$get !== void 0 ? _urlData$get : bodyData.query; if (typeof query !== 'string') { query = null; } // Parse the variables if needed. let variables = (_urlData$get2 = urlData.get('variables')) !== null && _urlData$get2 !== void 0 ? _urlData$get2 : bodyData.variables; if (typeof variables === 'string') { try { variables = JSON.parse(variables); } catch (error) { throw (0, _httpErrors.default)(400, 'Variables are invalid JSON.'); } } else if (typeof variables !== 'object') { variables = null; } // Name of GraphQL operation to execute. let operationName = urlData.get('operationName') || bodyData.operationName; if (typeof operationName !== 'string') { operationName = null; } const raw = urlData.get('raw') != null || bodyData.raw !== undefined; return { query, variables, operationName, raw }; } /** * Helper function to determine if GraphiQL can be displayed. */ function canDisplayGraphiQL(request, params) { // If `raw` false, 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, _accepts.default)(request).types(['json', 'html']) === 'html'; } /** * Helper function for sending a response using only the core Node server APIs. */ function sendResponse(response, type, data) { const chunk = Buffer.from(data, 'utf8'); response.setHeader('Content-Type', type + '; charset=utf-8'); response.setHeader('Content-Length', String(chunk.length)); response.end(chunk); }