"use strict";
var tslib_1 = require("tslib");
var path_1 = require("path");
var fs_1 = require("fs");
var graphql_1 = require("graphql");
var extendedFormatError_1 = require("../extendedFormatError");
var setupServerSentEvents_1 = require("./setupServerSentEvents");
var withPostGraphQLContext_1 = require("../withPostGraphQLContext");
var chalk = require('chalk');
var Debugger = require('debug'); // tslint:disable-line variable-name
var httpError = require('http-errors');
var parseUrl = require('parseurl');
var finalHandler = require('finalhandler');
var bodyParser = require('body-parser');
var sendFile = require('send');
var POSTGRAPHQL_ENV = process.env.POSTGRAPHQL_ENV;
var debugGraphql = new Debugger('postgraphql:graphql');
var debugRequest = new Debugger('postgraphql:request');
exports.graphiqlDirectory = path_1.resolve(__dirname, '../graphiql/public');
/**
 * The favicon file in `Buffer` format. We can send a `Buffer` directly to the
 * client.
 *
 * @type {Promise<Buffer>}
 */
var favicon = new Promise(function (resolve, reject) {
    fs_1.readFile(path_1.resolve(__dirname, '../../../resources/favicon.ico'), function (error, data) {
        if (error)
            reject(error);
        else
            resolve(data);
    });
});
/**
 * The GraphiQL HTML file as a string. We need it to be a string, because we
 * will use a regular expression to replace some variables.
 *
 * @type {Promise<string>}
 */
var origGraphiqlHtml = new Promise(function (resolve, reject) {
    fs_1.readFile(path_1.resolve(__dirname, '../graphiql/public/index.html'), 'utf8', function (error, data) {
        if (error)
            reject(error);
        else
            resolve(data);
    });
});
/**
 * Creates a GraphQL request handler, this is untyped besides some JSDoc types
 * for intellisense.
 *
 * @param {GraphQLSchema} graphqlSchema
 */
function createPostGraphQLHttpRequestHandler(options) {
    var _this = this;
    var getGqlSchema = options.getGqlSchema, pgPool = options.pgPool, pgSettings = options.pgSettings, pgDefaultRole = options.pgDefaultRole, jwtSecret = options.jwtSecret, jwtAudiences = options.jwtAudiences, jwtRole = options.jwtRole, jwtVerifyOptions = options.jwtVerifyOptions, additionalGraphQLContextFromRequest = options.additionalGraphQLContextFromRequest;
    if (pgDefaultRole && typeof pgSettings === 'function') {
        throw new Error('pgDefaultRole cannot be combined with pgSettings(req) - please remove pgDefaultRole and instead always return a `role` key from pgSettings(req).');
    }
    if (pgDefaultRole && pgSettings && typeof pgSettings === 'object' && Object.keys(pgSettings).map(function (s) { return s.toLowerCase(); }).indexOf('role') >= 0) {
        throw new Error('pgDefaultRole cannot be combined with pgSettings.role - please use one or the other.');
    }
    // Gets the route names for our GraphQL endpoint, and our GraphiQL endpoint.
    var graphqlRoute = options.graphqlRoute || '/graphql';
    var graphiqlRoute = options.graphiql === true ? options.graphiqlRoute || '/graphiql' : null;
    // Throw an error of the GraphQL and GraphiQL routes are the same.
    if (graphqlRoute === graphiqlRoute)
        throw new Error("Cannot use the same route, '" + graphqlRoute + "', for both GraphQL and GraphiQL. Please use different routes.");
    // Formats an error using the default GraphQL `formatError` function, and
    // custom formatting using some other options.
    var formatError = function (error) {
        // Get the appropriate formatted error object, including any extended error
        // fields if the user wants them.
        var formattedError = options.extendedErrors && options.extendedErrors.length ?
            extendedFormatError_1.extendedFormatError(error, options.extendedErrors) : graphql_1.formatError(error);
        // If the user wants to see the error’s stack, let’s add it to the
        // formatted error.
        if (options.showErrorStack)
            formattedError.stack = options.showErrorStack === 'json' ? error.stack.split('\n') : error.stack;
        return formattedError;
    };
    // Define a list of middlewares that will get run before our request handler.
    // Note though that none of these middlewares will intercept a request (i.e.
    // not call `next`). Middlewares that handle a request like favicon
    // middleware will result in a promise that never resolves, and we don’t
    // want that.
    var bodyParserMiddlewares = [
        // Parse JSON bodies.
        bodyParser.json({ limit: options.bodySizeLimit }),
        // Parse URL encoded bodies (forms).
        bodyParser.urlencoded({ extended: false }),
        // Parse `application/graphql` content type bodies as text.
        bodyParser.text({ type: 'application/graphql' }),
    ];
    // Takes the original GraphiQL HTML file and replaces the default config object.
    var graphiqlHtml = origGraphiqlHtml.then(function (html) { return html.replace(/window\.POSTGRAPHQL_CONFIG\s*=\s*\{[^]*\}/, "window.POSTGRAPHQL_CONFIG={graphqlUrl:'" + graphqlRoute + "',streamUrl:" + (options.watchPg ? '\'/_postgraphql/stream\'' : 'null') + "}"); });
    /**
     * The actual request handler. It’s an async function so it will return a
     * promise when complete. If the function doesn’t handle anything, it calls
     * `next` to let the next middleware try and handle it.
     *
     * @param {IncomingMessage} req
     * @param {ServerResponse} res
     */
    var requestHandler = function (req, res, next) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
        var _a, _b, _c, assetPath, assetPathRelative_1, _d, _e, _f, params, result, queryDocumentAst, queryTimeStart, pgRole, gqlSchema_1, source, validationErrors, jwtToken, additionalContext_1, _g, _h, _j, _k, _l, error_1, prettyQuery, errorCount, timeDiff, ms;
        return tslib_1.__generator(this, function (_m) {
            switch (_m.label) {
                case 0:
                    // Add our CORS headers to be good web citizens (there are perf
                    // implications though so be careful!)
                    //
                    // Always enable CORS when developing PostGraphQL because GraphiQL will be
                    // on port 5783.
                    if (options.enableCors || POSTGRAPHQL_ENV === 'development')
                        addCORSHeaders(res);
                    if (!options.graphiql) return [3 /*break*/, 6];
                    if (!(parseUrl(req).pathname === '/favicon.ico')) return [3 /*break*/, 2];
                    // If this is the wrong method, we should let the client know.
                    if (!(req.method === 'GET' || req.method === 'HEAD')) {
                        res.statusCode = req.method === 'OPTIONS' ? 200 : 405;
                        res.setHeader('Allow', 'GET, HEAD, OPTIONS');
                        res.end();
                        return [2 /*return*/];
                    }
                    // Otherwise we are good and should pipe the favicon to the browser.
                    res.statusCode = 200;
                    res.setHeader('Cache-Control', 'public, max-age=86400');
                    res.setHeader('Content-Type', 'image/x-icon');
                    // End early if the method is `HEAD`.
                    if (req.method === 'HEAD') {
                        res.end();
                        return [2 /*return*/];
                    }
                    _b = (_a = res).end;
                    return [4 /*yield*/, favicon];
                case 1:
                    _b.apply(_a, [_m.sent()]);
                    return [2 /*return*/];
                case 2:
                    if (!parseUrl(req).pathname.startsWith('/_postgraphql/graphiql/')) return [3 /*break*/, 4];
                    // If using the incorrect method, let the user know.
                    if (!(req.method === 'GET' || req.method === 'HEAD')) {
                        res.statusCode = req.method === 'OPTIONS' ? 200 : 405;
                        res.setHeader('Allow', 'GET, HEAD, OPTIONS');
                        res.end();
                        return [2 /*return*/];
                    }
                    assetPath = path_1.resolve(path_1.join(exports.graphiqlDirectory, parseUrl(req).pathname.slice('/_postgraphql/graphiql/'.length)));
                    assetPathRelative_1 = path_1.relative(exports.graphiqlDirectory, assetPath);
                    // Block any attempts at path traversal issues
                    if (assetPath.substr(0, exports.graphiqlDirectory.length) !== exports.graphiqlDirectory || assetPathRelative_1.substr(0, 2) === '..') {
                        res.statusCode = 403;
                        res.end();
                        return [2 /*return*/];
                    }
                    // Don’t allow certain files generated by `create-react-app` to be
                    // inspected.
                    if (assetPathRelative_1 === 'index.html' || assetPathRelative_1 === 'asset-manifest.json') {
                        res.statusCode = 404;
                        res.end();
                        return [2 /*return*/];
                    }
                    // Sends the asset at this path. Defaults to a `statusCode` of 200.
                    res.statusCode = 200;
                    return [4 /*yield*/, new Promise(function (resolve, reject) {
                            var stream = sendFile(req, assetPathRelative_1, {
                                index: false,
                                root: exports.graphiqlDirectory,
                                dotfiles: 'ignore',
                            })
                                .on('end', resolve)
                                .on('error', reject)
                                .pipe(res);
                        })];
                case 3:
                    _m.sent();
                    return [2 /*return*/];
                case 4:
                    // ======================================================================
                    // GraphiQL Watch Stream
                    // ======================================================================
                    // Setup an event stream so we can broadcast events to graphiql, etc.
                    if (parseUrl(req).pathname === '/_postgraphql/stream') {
                        if (req.headers.accept !== 'text/event-stream') {
                            res.statusCode = 405;
                            res.end();
                            return [2 /*return*/];
                        }
                        setupServerSentEvents_1.default(req, res, options);
                        return [2 /*return*/];
                    }
                    if (!(parseUrl(req).pathname === graphiqlRoute)) return [3 /*break*/, 6];
                    // If we are developing PostGraphQL, instead just redirect.
                    if (POSTGRAPHQL_ENV === 'development') {
                        res.writeHead(302, { Location: 'http://localhost:5783' });
                        res.end();
                        return [2 /*return*/];
                    }
                    // If using the incorrect method, let the user know.
                    if (!(req.method === 'GET' || req.method === 'HEAD')) {
                        res.statusCode = req.method === 'OPTIONS' ? 200 : 405;
                        res.setHeader('Allow', 'GET, HEAD, OPTIONS');
                        res.end();
                        return [2 /*return*/];
                    }
                    res.statusCode = 200;
                    res.setHeader('Content-Type', 'text/html; charset=utf-8');
                    // End early if the method is `HEAD`.
                    if (req.method === 'HEAD') {
                        res.end();
                        return [2 /*return*/];
                    }
                    // Actually renders GraphiQL.
                    _e = (_d = res).end;
                    return [4 /*yield*/, graphiqlHtml];
                case 5:
                    // Actually renders GraphiQL.
                    _e.apply(_d, [_m.sent()]);
                    return [2 /*return*/];
                case 6:
                    // Don’t handle any requests if this is not the correct route.
                    if (parseUrl(req).pathname !== graphqlRoute)
                        return [2 /*return*/, next()];
                    // ========================================================================
                    // Execute GraphQL Queries
                    // ========================================================================
                    // If we didn’t call `next` above, all requests will return 200 by default!
                    res.statusCode = 200;
                    // Don’t execute our GraphQL stuffs for `OPTIONS` requests.
                    if (req.method === 'OPTIONS') {
                        res.statusCode = 200;
                        res.end();
                        return [2 /*return*/];
                    }
                    queryTimeStart = process.hrtime();
                    debugRequest('GraphQL query request has begun.');
                    _m.label = 7;
                case 7:
                    _m.trys.push([7, 17, 18, 19]);
                    return [4 /*yield*/, getGqlSchema()];
                case 8:
                    gqlSchema_1 = _m.sent();
                    // Run all of our middleware by converting them into promises and
                    // chaining them together. Remember that if we have a middleware that
                    // never calls `next`, we will have a promise that never resolves! Avoid
                    // those middlewares.
                    //
                    // Note that we also run our middleware after we make sure we are on the
                    // correct route. This is so that if our middleware modifies the `req` or
                    // `res` objects, only we downstream will see the modifications.
                    //
                    // We also run our middleware inside the `try` so that we get the GraphQL
                    // error reporting style for syntax errors.
                    return [4 /*yield*/, bodyParserMiddlewares.reduce(function (promise, middleware) { return (promise.then(function () { return new Promise(function (resolve, reject) {
                            middleware(req, res, function (error) {
                                if (error)
                                    reject(error);
                                else
                                    resolve();
                            });
                        }); })); }, Promise.resolve())];
                case 9:
                    // Run all of our middleware by converting them into promises and
                    // chaining them together. Remember that if we have a middleware that
                    // never calls `next`, we will have a promise that never resolves! Avoid
                    // those middlewares.
                    //
                    // Note that we also run our middleware after we make sure we are on the
                    // correct route. This is so that if our middleware modifies the `req` or
                    // `res` objects, only we downstream will see the modifications.
                    //
                    // We also run our middleware inside the `try` so that we get the GraphQL
                    // error reporting style for syntax errors.
                    _m.sent();
                    // If this is not one of the correct methods, throw an error.
                    if (req.method !== 'POST') {
                        res.setHeader('Allow', 'POST, OPTIONS');
                        throw httpError(405, 'Only `POST` requests are allowed.');
                    }
                    // Get the parameters we will use to run a GraphQL request. `params` may
                    // include:
                    //
                    // - `query`: The required GraphQL query string.
                    // - `variables`: An optional JSON object containing GraphQL variables.
                    // - `operationName`: The optional name of the GraphQL operation we will
                    //   be executing.
                    params = typeof req.body === 'string' ? { query: req.body } : req.body;
                    // Validate our params object a bit.
                    if (params == null)
                        throw httpError(400, 'Must provide an object parameters, not nullish value.');
                    if (typeof params !== 'object')
                        throw httpError(400, "Expected parameter object, not value of type '" + typeof params + "'.");
                    if (Array.isArray(params))
                        throw httpError(501, 'Batching queries as an array is currently unsupported. Please provide a single query object.');
                    if (!params.query)
                        throw httpError(400, 'Must provide a query string.');
                    // If variables is a string, we assume it is a JSON string and that it
                    // needs to be parsed.
                    if (typeof params.variables === 'string') {
                        // If variables is just an empty string, we should set it to null and
                        // ignore it.
                        if (params.variables === '') {
                            params.variables = null;
                        }
                        else {
                            try {
                                params.variables = JSON.parse(params.variables);
                            }
                            catch (error) {
                                error.statusCode = 400;
                                throw error;
                            }
                        }
                    }
                    // Throw an error if `variables` is not an object.
                    if (params.variables != null && typeof params.variables !== 'object')
                        throw httpError(400, "Variables must be an object, not '" + typeof params.variables + "'.");
                    // Throw an error if `operationName` is not a string.
                    if (params.operationName != null && typeof params.operationName !== 'string')
                        throw httpError(400, "Operation name must be a string, not '" + typeof params.operationName + "'.");
                    source = new graphql_1.Source(params.query, 'GraphQL Http Request');
                    // Catch an errors while parsing so that we can set the `statusCode` to
                    // 400. Otherwise we don’t need to parse this way.
                    try {
                        queryDocumentAst = graphql_1.parse(source);
                    }
                    catch (error) {
                        res.statusCode = 400;
                        throw error;
                    }
                    debugRequest('GraphQL query is parsed.');
                    validationErrors = graphql_1.validate(gqlSchema_1, queryDocumentAst);
                    // If we have some validation errors, don’t execute the query. Instead
                    // send the errors to the client with a `400` code.
                    if (validationErrors.length > 0) {
                        res.statusCode = 400;
                        result = { errors: validationErrors };
                        return [2 /*return*/];
                    }
                    debugRequest('GraphQL query is validated.');
                    // Lazily log the query. If this debugger isn’t enabled, don’t run it.
                    if (debugGraphql.enabled)
                        debugGraphql(graphql_1.print(queryDocumentAst).replace(/\s+/g, ' ').trim());
                    jwtToken = options.jwtSecret ? getJwtToken(req) : null;
                    if (!(typeof additionalGraphQLContextFromRequest === 'function')) return [3 /*break*/, 11];
                    return [4 /*yield*/, additionalGraphQLContextFromRequest(req, res)];
                case 10:
                    _g = _m.sent();
                    return [3 /*break*/, 12];
                case 11:
                    _g = {};
                    _m.label = 12;
                case 12:
                    additionalContext_1 = _g;
                    _h = withPostGraphQLContext_1.default;
                    _k = {
                        pgPool: pgPool,
                        jwtToken: jwtToken,
                        jwtSecret: jwtSecret,
                        jwtAudiences: jwtAudiences,
                        jwtRole: jwtRole,
                        jwtVerifyOptions: jwtVerifyOptions,
                        pgDefaultRole: pgDefaultRole
                    };
                    if (!(typeof pgSettings === 'function')) return [3 /*break*/, 14];
                    return [4 /*yield*/, pgSettings(req)];
                case 13:
                    _l = _m.sent();
                    return [3 /*break*/, 15];
                case 14:
                    _l = pgSettings;
                    _m.label = 15;
                case 15: return [4 /*yield*/, _h.apply(void 0, [(_k.pgSettings = _l,
                            _k), function (context) {
                            pgRole = context.pgRole;
                            var graphqlContext = Object.assign({}, additionalContext_1, context);
                            return graphql_1.execute(gqlSchema_1, queryDocumentAst, null, graphqlContext, params.variables, params.operationName);
                        }])];
                case 16:
                    result = _m.sent();
                    return [3 /*break*/, 19];
                case 17:
                    error_1 = _m.sent();
                    // Set our status code and send the client our results!
                    if (res.statusCode === 200)
                        res.statusCode = error_1.status || error_1.statusCode || 500;
                    result = { errors: [error_1] };
                    // If the status code is 500, let’s log our error.
                    if (res.statusCode === 500)
                        // tslint:disable-next-line no-console
                        console.error(error_1.stack);
                    return [3 /*break*/, 19];
                case 18:
                    debugRequest('GraphQL query has been executed.');
                    // Format our errors so the client doesn’t get the full thing.
                    if (result && result.errors)
                        result.errors = result.errors.map(formatError);
                    res.setHeader('Content-Type', 'application/json; charset=utf-8');
                    res.end(JSON.stringify(result));
                    debugRequest('GraphQL query request finished.');
                    // Log the query. If this debugger isn’t enabled, don’t run it.
                    if (queryDocumentAst && !options.disableQueryLog) {
                        prettyQuery = graphql_1.print(queryDocumentAst).replace(/\s+/g, ' ').trim();
                        errorCount = (result.errors || []).length;
                        timeDiff = process.hrtime(queryTimeStart);
                        ms = Math.round((timeDiff[0] * 1e9 + timeDiff[1]) * 10e-7 * 100) / 100;
                        // If we have enabled the query log for the Http handler, use that.
                        // tslint:disable-next-line no-console
                        console.log(chalk[errorCount === 0 ? 'green' : 'red'](errorCount + " error(s)") + " " + (pgRole != null ? "as " + chalk.magenta(pgRole) + " " : '') + "in " + chalk.grey(ms + "ms") + " :: " + prettyQuery);
                    }
                    return [7 /*endfinally*/];
                case 19: return [2 /*return*/];
            }
        });
    }); };
    /**
     * A polymorphic request handler that should detect what `http` framework is
     * being used and specifically handle that framework.
     *
     * Supported frameworks include:
     *
     * - Native Node.js `http`.
     * - `connect`.
     * - `express`.
     * - `koa` (2.0).
     */
    return function (a, b, c) {
        // If are arguments look like the arguments to koa middleware, this is
        // `koa` middleware.
        if (a.req && a.res && typeof b === 'function') {
            // Set the correct `koa` variable names…
            var ctx = a;
            var next = b;
            // Execute our request handler. If an error is thrown, we don’t call
            // `next` with an error. Instead we return the promise and let `koa`
            // handle the error.
            return requestHandler(ctx.req, ctx.res, next);
        }
        else {
            // Set the correct `connect` style variable names. If there was no `next`
            // defined (likely the case if the client is using `http`) we use the
            // final handler.
            var req = a;
            var res = b;
            var next_1 = c || finalHandler(req, res);
            // Execute our request handler.
            requestHandler(req, res, next_1).then(
            // If the request was fulfilled, noop.
            function () { }, 
            // If the request errored out, call `next` with the error.
            function (error) { return next_1(error); });
        }
    };
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = createPostGraphQLHttpRequestHandler;
/**
 * Adds CORS to a request. See [this][1] flowchart for an explanation of how
 * CORS works. Note that these headers are set for all requests, CORS
 * algorithms normally run a preflight request using the `OPTIONS` method to
 * get these headers.
 *
 * Note though, that enabling CORS will incur extra costs when it comes to the
 * preflight requests. It is much better if you choose to use a proxy and
 * bypass CORS altogether.
 *
 * [1]: http://www.html5rocks.com/static/images/cors_server_flowchart.png
 */
function addCORSHeaders(res) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Request-Method', 'HEAD, GET, POST');
    res.setHeader('Access-Control-Allow-Headers', [
        'Origin',
        'X-Requested-With',
        // Used by `express-graphql` to determine whether to expose the GraphiQL
        // interface (`text/html`) or not.
        'Accept',
        // Used by PostGraphQL for auth purposes.
        'Authorization',
        // The `Content-*` headers are used when making requests with a body,
        // like in a POST request.
        'Content-Type',
        'Content-Length',
    ].join(', '));
}
/**
 * Parses the `Bearer` auth scheme token out of the `Authorization` header as
 * defined by [RFC7235][1].
 *
 * ```
 * Authorization = credentials
 * credentials   = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
 * token68       = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" )*"="
 * ```
 *
 * [1]: https://tools.ietf.org/html/rfc7235
 *
 * @private
 */
var authorizationBearerRex = /^\s*bearer\s+([a-z0-9\-._~+/]+=*)\s*$/i;
/**
 * Gets the JWT token from the Http request’s headers. Specifically the
 * `Authorization` header in the `Bearer` format. Will throw an error if the
 * header is in the incorrect format, but will not throw an error if the header
 * does not exist.
 *
 * @private
 * @param {IncomingMessage} request
 * @returns {string | null}
 */
function getJwtToken(request) {
    var authorization = request.headers.authorization;
    // If there was no authorization header, just return null.
    if (authorization == null)
        return null;
    var match = authorizationBearerRex.exec(authorization);
    // If we did not match the authorization header with our expected format,
    // throw a 400 error.
    if (!match)
        throw httpError(400, 'Authorization header is not of the correct bearer scheme format.');
    // Return the token from our match.
    return match[1];
}
//# sourceMappingURL=data:application/json;base64, 