"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);
}
