UNPKG

postgraphile

Version:

A GraphQL schema created by reflection over a PostgreSQL schema 🐘 (previously known as PostGraphQL)

976 lines • 81.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isEmpty = void 0; /* eslint-disable @typescript-eslint/no-explicit-any,require-atomic-updates */ const graphql_1 = require("graphql"); const extendedFormatError_1 = require("../extendedFormatError"); const pluginHook_1 = require("../pluginHook"); const setupServerSentEvents_1 = require("./setupServerSentEvents"); const withPostGraphileContext_1 = require("../withPostGraphileContext"); const lru_1 = require("@graphile/lru"); const chalk_1 = require("chalk"); const Debugger = require("debug"); // tslint:disable-line variable-name const httpError = require("http-errors"); const parseUrl = require("parseurl"); const finalHandler = require("finalhandler"); const bodyParser = require("body-parser"); const crypto = require("crypto"); const isKoaApp = (a, b) => a.req && a.res && typeof b === 'function'; const CACHE_MULTIPLIER = 100000; const ALLOW_EXPLAIN_PLACEHOLDER = '__SHOULD_ALLOW_EXPLAIN__'; const noop = () => { /* noop */ }; const { createHash } = crypto; /** * The favicon file in `Buffer` format. We can send a `Buffer` directly to the * client. * * @type {Buffer} */ const favicon_ico_1 = require("../../assets/favicon.ico"); /** * 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. */ const graphiql_html_1 = require("../../assets/graphiql.html"); const subscriptions_1 = require("./subscriptions"); const frameworks_1 = require("./frameworks"); /** * When writing JSON to the browser, we need to be careful that it doesn't get * interpretted as HTML. */ const JS_ESCAPE_LOOKUP = { '<': '\\u003c', '>': '\\u003e', '/': '\\u002f', '\u2028': '\\u2028', '\u2029': '\\u2029', }; function safeJSONStringify(obj) { return JSON.stringify(obj).replace(/[<>/\u2028\u2029]/g, chr => JS_ESCAPE_LOOKUP[chr]); } /** * When people webpack us up, e.g. for lambda, if they don't want GraphiQL then * they can seriously reduce bundle size by omitting the assets. */ const shouldOmitAssets = process.env.POSTGRAPHILE_OMIT_ASSETS === '1'; // Used by `createPostGraphileHttpRequestHandler` let lastString; let lastHash; const calculateQueryHash = (queryString) => { if (queryString !== lastString) { lastString = queryString; lastHash = createHash('sha1').update(queryString).digest('base64'); } return lastHash; }; // Fast way of checking if an object is empty, // faster than `Object.keys(value).length === 0`. // NOTE: we don't need a `hasOwnProperty` call here because isEmpty is called // with an `Object.create(null)` object, so it has no no-own properties. /* tslint:disable forin */ function isEmpty(value) { for (const _key in value) { return false; } return true; } exports.isEmpty = isEmpty; /* tslint:enable forin */ const isPostGraphileDevelopmentMode = process.env.POSTGRAPHILE_ENV === 'development'; const debugGraphql = Debugger('postgraphile:graphql'); const debugRequest = Debugger('postgraphile:request'); /** * We need to be able to share the withPostGraphileContext logic between HTTP * and websockets */ function withPostGraphileContextFromReqResGenerator(options) { const { pgSettings: pgSettingsGenerator, allowExplain: allowExplainGenerator, jwtSecret, jwtPublicKey, additionalGraphQLContextFromRequest, } = options; return async (req, res, moreOptions, fn) => { const jwtVerificationSecret = jwtPublicKey || jwtSecret; const jwtToken = jwtVerificationSecret ? getJwtToken(req) : null; const additionalContext = typeof additionalGraphQLContextFromRequest === 'function' ? await additionalGraphQLContextFromRequest(req, res) : null; const pgSettings = typeof pgSettingsGenerator === 'function' ? await pgSettingsGenerator(req) : pgSettingsGenerator; const allowExplain = typeof allowExplainGenerator === 'function' ? await allowExplainGenerator(req) : allowExplainGenerator; return withPostGraphileContext_1.default(Object.assign(Object.assign(Object.assign({}, options), { jwtToken, pgSettings, explain: allowExplain && req.headers['x-postgraphile-explain'] === 'on' }), moreOptions), context => { const graphqlContext = additionalContext ? Object.assign(Object.assign({}, additionalContext), context) : context; return fn(graphqlContext); }); }; } /** * Creates a GraphQL request handler that can support many different `http` frameworks, including: * * - Native Node.js `http`. * - `connect`. * - `express`. * - `koa` (2.0). */ function createPostGraphileHttpRequestHandler(options) { const MEGABYTE = 1024 * 1024; const subscriptions = !!options.subscriptions; const { getGqlSchema, pgPool, pgSettings, pgDefaultRole, shutdownActions, queryCacheMaxSize = 50 * MEGABYTE, extendedErrors, showErrorStack, watchPg, disableQueryLog, enableQueryBatching, websockets = options.subscriptions || options.live ? ['v0', 'v1'] : [], } = options; const live = !!options.live; const enhanceGraphiql = options.enhanceGraphiql === false ? false : !!options.enhanceGraphiql || subscriptions || live; const enableCors = !!options.enableCors || isPostGraphileDevelopmentMode; const graphiql = options.graphiql === true; if (options['absoluteRoutes']) { throw new Error('Sorry - the `absoluteRoutes` setting has been replaced with `externalUrlBase` which solves the issue in a cleaner way. Please update your settings. Thank you for testing a PostGraphile pre-release 🙏'); } // Using let because we might override it on the first request. let externalUrlBase = options.externalUrlBase; if (externalUrlBase && externalUrlBase.endsWith('/')) { throw new Error('externalUrlBase must not end with a slash (`/`)'); } // Validate websockets argument if ( // must be array !Array.isArray(websockets) || // empty array = 'none' (websockets.length && // array can only hold the versions websockets.some(ver => !['v0', 'v1'].includes(ver)))) { throw new Error(`Invalid value for \`websockets\` option: '${websockets}'`); } const pluginHook = pluginHook_1.pluginHookFromOptions(options); const origGraphiqlHtml = pluginHook('postgraphile:graphiql:html', graphiql_html_1.default, { options }); 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(s => s.toLowerCase()) .includes('role')) { throw new Error('pgDefaultRole cannot be combined with pgSettings.role - please use one or the other.'); } if (graphiql && shouldOmitAssets) { throw new Error('Cannot enable GraphiQL when POSTGRAPHILE_OMIT_ASSETS is set'); } // Gets the route names for our GraphQL endpoint, and our GraphiQL endpoint. const graphqlRoute = options.graphqlRoute || '/graphql'; const graphiqlRoute = options.graphiqlRoute || '/graphiql'; // Set the request credential behavior in graphiql. const graphiqlCredentials = options.graphiqlCredentials || 'same-origin'; const eventStreamRoute = options.eventStreamRoute || `${graphqlRoute.replace(/\/+$/, '')}/stream`; const externalGraphqlRoute = options.externalGraphqlRoute; const externalEventStreamRoute = options.externalEventStreamRoute || (externalGraphqlRoute && !options.eventStreamRoute ? `${externalGraphqlRoute.replace(/\/+$/, '')}/stream` : undefined); // 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. const formatError = (error) => { // Get the appropriate formatted error object, including any extended error // fields if the user wants them. const formattedError = extendedErrors && extendedErrors.length ? extendedFormatError_1.extendedFormatError(error, extendedErrors) : graphql_1.formatError(error); // If the user wants to see the error’s stack, let’s add it to the // formatted error. if (showErrorStack) formattedError['stack'] = error.stack != null && showErrorStack === 'json' ? error.stack.split('\n') : error.stack; return formattedError; }; const DEFAULT_HANDLE_ERRORS = (errors) => errors.map(formatError); const handleErrors = options.handleErrors || DEFAULT_HANDLE_ERRORS; // 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. const bodyParserMiddlewares = [ // Parse JSON bodies. bodyParser.json({ limit: options.bodySizeLimit }), // Parse URL encoded bodies (forms). bodyParser.urlencoded({ extended: false, limit: options.bodySizeLimit }), // Parse `application/graphql` content type bodies as text. bodyParser.text({ type: 'application/graphql', limit: options.bodySizeLimit }), ]; // We'll turn this into one function now so it can be better JIT optimised const bodyParserMiddlewaresComposed = bodyParserMiddlewares.reduce((parent, fn) => { return (req, res, next) => { parent(req, res, error => { if (error) { return next(error); } fn(req, res, next); }); }; }, (_req, _res, next) => next()); // And we really want that function to be await-able const parseBody = (req, res) => new Promise((resolve, reject) => { bodyParserMiddlewaresComposed(req, // Note: middleware here doesn't actually use the response, but we pass // the underlying value so types match up. res.getNodeServerResponse(), (error) => { if (error) { reject(error); } else { resolve(); } }); }); // We only need to calculate the graphiql HTML once; but we need to receive the first request to do so. let graphiqlHtml; const withPostGraphileContextFromReqRes = withPostGraphileContextFromReqResGenerator(options); const staticValidationRules = pluginHook('postgraphile:validationRules:static', graphql_1.specifiedRules, { options, }); const cacheSize = Math.ceil(queryCacheMaxSize / CACHE_MULTIPLIER); // Do not create an LRU for cache size < 2 because @graphile/lru will baulk. const cacheEnabled = cacheSize >= 2; const queryCache = cacheEnabled ? new lru_1.default({ maxLength: cacheSize }) : null; let lastGqlSchema; const parseQuery = (gqlSchema, queryString) => { if (gqlSchema !== lastGqlSchema) { if (queryCache) { queryCache.reset(); } lastGqlSchema = gqlSchema; } // Only cache queries that are less than 100kB, we don't want DOS attacks // attempting to exhaust our memory. const canCache = cacheEnabled && queryString.length < 100000; const hash = canCache ? calculateQueryHash(queryString) : null; const result = canCache ? queryCache.get(hash) : null; if (result) { return result; } else { const source = new graphql_1.Source(queryString, 'GraphQL Http Request'); let queryDocumentAst; // 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) { error.statusCode = 400; throw error; } if (debugRequest.enabled) debugRequest('GraphQL query is parsed.'); // Validate our GraphQL query using given rules. const validationErrors = graphql_1.validate(gqlSchema, queryDocumentAst, staticValidationRules); const cacheResult = { queryDocumentAst, validationErrors, length: queryString.length, }; if (canCache) { queryCache.set(hash, cacheResult); } return cacheResult; } }; let firstRequestHandler = req => { // Never be called again firstRequestHandler = null; let graphqlRouteForWs = graphqlRoute; const parsed = parseUrl(req) || { pathname: '' }; const pathname = parsed && typeof parsed.pathname === 'string' ? parsed.pathname : ''; const parsed2 = parseUrl.original(req) || { pathname: '' }; const originalPathname = parsed2 && typeof parsed2.pathname === 'string' ? parsed2.pathname : ''; if (originalPathname !== pathname && originalPathname.endsWith(pathname)) { const base = originalPathname.slice(0, originalPathname.length - pathname.length); // Our websocket GraphQL route must be at a different place graphqlRouteForWs = base + graphqlRouteForWs; if (externalUrlBase == null) { // User hasn't specified externalUrlBase; let's try and guess it // We were mounted on a subpath (e.g. `app.use('/path/to', postgraphile(...))`). // Figure out our externalUrlBase for ourselves. externalUrlBase = base; } } // Make sure we have a string, at least externalUrlBase = externalUrlBase || ''; // Takes the original GraphiQL HTML file and replaces the default config object. graphiqlHtml = origGraphiqlHtml ? origGraphiqlHtml.replace(/<\/head>/, ` <script>window.POSTGRAPHILE_CONFIG=${safeJSONStringify({ graphqlUrl: externalGraphqlRoute || `${externalUrlBase}${graphqlRoute}`, streamUrl: watchPg ? externalEventStreamRoute || `${externalUrlBase}${eventStreamRoute}` : null, enhanceGraphiql, // if 'v1' websockets are included, use the v1 client always websockets: !websockets.length ? 'none' : websockets.includes('v1') ? 'v1' : 'v0', allowExplain: typeof options.allowExplain === 'function' ? ALLOW_EXPLAIN_PLACEHOLDER : !!options.allowExplain, credentials: graphiqlCredentials, })};</script>\n </head>`) : null; if (websockets.length) { const server = req && req.connection && req.connection['server']; if (!server) { // tslint:disable-next-line no-console console.warn("Failed to find server to add websocket listener to, you'll need to call `enhanceHttpServerWithWebSockets` manually"); } else { // Relying on this means that a normal request must come in before an // upgrade attempt. It's better to call it manually. subscriptions_1.enhanceHttpServerWithWebSockets(server, middleware, { graphqlRoute: graphqlRouteForWs, }); } } }; /* * If we're not in watch mode, then avoid the cost of `await`ing the schema * on every tick by having it available once it was generated. */ let theOneAndOnlyGraphQLSchema = null; if (!watchPg) { getGqlSchema() .then(schema => { theOneAndOnlyGraphQLSchema = schema; }) .catch(noop); } function neverReject(middlewareName, middleware) { return async (res) => { try { await middleware(res); } catch (e) { console.error(`An unexpected error occurred whilst processing '${middlewareName}'; this indicates a bug. The connection will be terminated.`); console.error(e); try { // At least terminate the connection res.statusCode = 500; res.end(); } catch (e) { /*NOOP*/ } } }; } /** * 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. If the function * throws an error, it's up to the wrapping middleware (imaginatively named * `middleware`, below) to handle the error. Frameworks like Koa have * middlewares reject a promise on error, whereas Express requires you pass * the error to the `next(err)` function. */ const requestHandler = async (responseHandler, next) => { const res = responseHandler; const incomingReq = res.getNodeServerRequest(); const nodeRes = res.getNodeServerResponse(); // You can use this hook either to modify the incoming request or to tell // PostGraphile not to handle the request further (return null). NOTE: if // you return `null` from this hook then you are also responsible for // calling `next()` (should that be required). const req = pluginHook('postgraphile:http:handler', incomingReq, { options, res: nodeRes, next, }); if (req == null) { return; } const { pathname = '' } = parseUrl(req) || {}; // Certain things depend on externalUrlBase, which we guess if the user // doesn't supply it, so we calculate them on the first request. After // first request, this function becomes a NOOP if (firstRequestHandler) firstRequestHandler(req); // ====================================================================== // GraphQL Watch Stream // ====================================================================== if (watchPg) { // Setup an event stream so we can broadcast events to graphiql, etc. if (pathname === eventStreamRoute || pathname === '/_postgraphile/stream') { return eventStreamRouteHandler(res); } } const isGraphqlRoute = pathname === graphqlRoute; // ======================================================================== // Serve GraphiQL and Related Assets // ======================================================================== if (!shouldOmitAssets && graphiql && !isGraphqlRoute) { // ====================================================================== // Favicon // ====================================================================== // If this is the favicon path and it has not yet been handled, let us // serve our GraphQL favicon. if (pathname === '/favicon.ico') { return faviconRouteHandler(res); } // ====================================================================== // GraphiQL HTML // ====================================================================== // If this is the GraphiQL route, show GraphiQL and stop execution. if (pathname === graphiqlRoute) { // If we are developing PostGraphile, instead just redirect. if (isPostGraphileDevelopmentMode) { res.statusCode = 302; res.setHeader('Location', 'http://localhost:5783'); res.end(); return; } return graphiqlRouteHandler(res); } } if (isGraphqlRoute) { return graphqlRouteHandler(res); } else { // This request wasn't for us. return next(); } }; const eventStreamRouteHandler = neverReject('eventStreamRouteHandler', async function eventStreamRouteHandler(res) { try { // You can use this hook either to modify the incoming request or to tell // PostGraphile not to handle the request further (return null). NOTE: if // you return `null` from this hook then you are also responsible for // terminating the request however your framework handles that (e.g. // `res.send(...)` or `next()`). const req = pluginHook('postgraphile:http:eventStreamRouteHandler', res.getNodeServerRequest(), { options, response: res }); if (req == null) { return; } // Add our CORS headers to be good web citizens (there are perf // implications though so be careful!) // // Always enable CORS when developing PostGraphile because GraphiQL will be // on port 5783. if (enableCors) addCORSHeaders(res); if (req.headers.accept !== 'text/event-stream') { res.statusCode = 405; res.end(); return; } setupServerSentEvents_1.default(res, options); } catch (e) { console.error('Unexpected error occurred in eventStreamRouteHandler'); console.error(e); res.statusCode = 500; res.end(); } }); const faviconRouteHandler = neverReject('faviconRouteHandler', async function faviconRouteHandler(res) { // You can use this hook either to modify the incoming request or to tell // PostGraphile not to handle the request further (return null). NOTE: if // you return `null` from this hook then you are also responsible for // terminating the request however your framework handles that (e.g. // `res.send(...)` or `next()`). const req = pluginHook('postgraphile:http:faviconRouteHandler', res.getNodeServerRequest(), { options, response: res, }); if (req == null) { return; } // 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; } // 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; } res.end(favicon_ico_1.default); }); const graphiqlRouteHandler = neverReject('graphiqlRouteHandler', async function graphiqlRouteHandler(res) { // You can use this hook either to modify the incoming request or to tell // PostGraphile not to handle the request further (return null). NOTE: if // you return `null` from this hook then you are also responsible for // terminating the request however your framework handles that (e.g. // `res.send(...)` or `next()`). const req = pluginHook('postgraphile:http:graphiqlRouteHandler', res.getNodeServerRequest(), { options, response: res, }); if (req == null) { return; } if (firstRequestHandler) firstRequestHandler(req); // 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; } res.statusCode = 200; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('Content-Security-Policy', "frame-ancestors 'self'"); // End early if the method is `HEAD`. if (req.method === 'HEAD') { res.end(); return; } // Actually renders GraphiQL. if (graphiqlHtml && typeof options.allowExplain === 'function') { res.end(graphiqlHtml.replace(`"${ALLOW_EXPLAIN_PLACEHOLDER}"`, // Because JSON escaped JSON.stringify(!!(await options.allowExplain(req))))); } else { res.end(graphiqlHtml); } }); const graphqlRouteHandler = neverReject('graphqlRouteHandler', async function graphqlRouteHandler(res) { // You can use this hook either to modify the incoming request or to tell // PostGraphile not to handle the request further (return null). NOTE: if // you return `null` from this hook then you are also responsible for // terminating the request however your framework handles that (e.g. // `res.send(...)` or `next()`). const req = pluginHook('postgraphile:http:graphqlRouteHandler', res.getNodeServerRequest(), { options, response: res, }); if (req == null) { return; } if (firstRequestHandler) firstRequestHandler(req); // Add our CORS headers to be good web citizens (there are perf // implications though so be careful!) // // Always enable CORS when developing PostGraphile because GraphiQL will be // on port 5783. if (enableCors) addCORSHeaders(res); // ======================================================================== // Execute GraphQL Queries // ======================================================================== // If we didn’t call `next` above, all requests will return 200 by default! res.statusCode = 200; if (watchPg) { // Inform GraphiQL and other clients that they can subscribe to events // (such as the schema being updated) at the following URL res.setHeader('X-GraphQL-Event-Stream', externalEventStreamRoute || `${externalUrlBase}${eventStreamRoute}`); } // Don’t execute our GraphQL stuffs for `OPTIONS` requests. if (req.method === 'OPTIONS') { res.statusCode = 200; res.end(); return; } // The `result` will be used at the very end in our `finally` block. // Statements inside the `try` will assign to `result` when they get // a result. We also keep track of `params`. let paramsList; let results = []; const queryTimeStart = !disableQueryLog && process.hrtime(); let pgRole; if (debugRequest.enabled) debugRequest('GraphQL query request has begun.'); let returnArray = false; // This big `try`/`catch`/`finally` block represents the execution of our // GraphQL query. All errors thrown in this block will be returned to the // client as GraphQL errors. try { // First thing we need to do is get the GraphQL schema for this request. // It should never really change unless we are in watch mode. const gqlSchema = theOneAndOnlyGraphQLSchema || (await getGqlSchema()); // Note that we 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. await parseBody(req, res); // 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. const body = req.body; paramsList = typeof body === 'string' ? { query: body } : body; // Validate our paramsList object a bit. if (paramsList == null) throw httpError(400, 'Must provide an object parameters, not nullish value.'); if (typeof paramsList !== 'object') throw httpError(400, `Expected parameter object, not value of type '${typeof paramsList}'.`); if (Array.isArray(paramsList)) { if (!enableQueryBatching) { throw httpError(501, 'Batching queries as an array is currently unsupported. Please provide a single query object.'); } else { returnArray = true; } } else { paramsList = [paramsList]; } paramsList = pluginHook('postgraphile:httpParamsList', paramsList, { options, req, res, returnArray, httpError, }); results = await Promise.all(paramsList.map(async (params) => { let queryDocumentAst = null; let result; const meta = Object.create(null); try { if (!params) throw httpError(400, 'Invalid query structure.'); const { query, operationName } = params; let { variables } = params; if (!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 variables === 'string') { // If variables is just an empty string, we should set it to null and // ignore it. if (variables === '') { variables = null; } else { // Otherwise, let us try to parse it as JSON. try { variables = JSON.parse(variables); } catch (error) { error.statusCode = 400; throw error; } } } // Throw an error if `variables` is not an object. if (variables != null && typeof variables !== 'object') throw httpError(400, `Variables must be an object, not '${typeof variables}'.`); // Throw an error if `operationName` is not a string. if (operationName != null && typeof operationName !== 'string') throw httpError(400, `Operation name must be a string, not '${typeof operationName}'.`); let validationErrors; ({ queryDocumentAst, validationErrors } = parseQuery(gqlSchema, query)); if (validationErrors.length === 0) { // You are strongly encouraged to use // `postgraphile:validationRules:static` if possible - you should // only use this one if you need access to variables. const moreValidationRules = pluginHook('postgraphile:validationRules', [], { options, req, res, variables, operationName, meta, }); if (moreValidationRules.length) { validationErrors = graphql_1.validate(gqlSchema, queryDocumentAst, moreValidationRules); } } // 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) { result = { errors: validationErrors, statusCode: 400 }; } else if (!queryDocumentAst) { throw new Error('Could not process query'); } else { if (debugRequest.enabled) debugRequest('GraphQL query is validated.'); // Lazily log the query. If this debugger isn’t enabled, don’t run it. if (debugGraphql.enabled) debugGraphql('%s', graphql_1.print(queryDocumentAst).replace(/\s+/g, ' ').trim()); result = await withPostGraphileContextFromReqRes(req, // For backwards compatibilty we must pass the actual node request object. res.getNodeServerResponse(), { singleStatement: false, queryDocumentAst, variables, operationName, }, (graphqlContext) => { pgRole = graphqlContext.pgRole; const graphqlResult = graphql_1.execute(gqlSchema, queryDocumentAst, null, graphqlContext, variables, operationName); if (typeof graphqlContext.getExplainResults === 'function') { return Promise.resolve(graphqlResult).then(async (obj) => (Object.assign(Object.assign({}, obj), { // Add our explain data explain: await graphqlContext.getExplainResults() }))); } else { return graphqlResult; } }); } } catch (error) { result = { errors: [error], statusCode: error.status || error.statusCode || 500, }; // If the status code is 500, let’s log our error. if (result.statusCode === 500) // tslint:disable-next-line no-console console.error(error.stack); } finally { // Format our errors so the client doesn’t get the full thing. if (result && result.errors) { result.errors = handleErrors(result.errors, req, res); } if (!isEmpty(meta)) { result.meta = meta; } result = pluginHook('postgraphile:http:result', result, { options, returnArray, queryDocumentAst, req, pgRole, }); // Log the query. If this debugger isn’t enabled, don’t run it. if (!disableQueryLog && queryDocumentAst) { // To appease TypeScript const definitelyQueryDocumentAst = queryDocumentAst; // We must reference this before it's deleted! const resultStatusCode = result.statusCode; const timeDiff = queryTimeStart && process.hrtime(queryTimeStart); setImmediate(() => { const prettyQuery = graphql_1.print(definitelyQueryDocumentAst) .replace(/\s+/g, ' ') .trim(); const errorCount = (result.errors || []).length; const ms = timeDiff[0] * 1e3 + timeDiff[1] * 1e-6; let message; if (resultStatusCode === 401) { // Users requested that JWT errors were raised differently: // // https://github.com/graphile/postgraphile/issues/560 message = chalk_1.default.red(`401 authentication error`); } else if (resultStatusCode === 403) { message = chalk_1.default.red(`403 forbidden error`); } else { message = chalk_1.default[errorCount === 0 ? 'green' : 'red'](`${errorCount} error(s)`); } // tslint:disable-next-line no-console console.log(`${message} ${pgRole != null ? `as ${chalk_1.default.magenta(pgRole)} ` : ''}in ${chalk_1.default.grey(`${ms.toFixed(2)}ms`)} :: ${prettyQuery}`); }); } if (debugRequest.enabled) debugRequest('GraphQL query has been executed.'); } return result; })); } catch (error) { // Set our status code and send the client our results! if (res.statusCode === 200) res.statusCode = error.status || error.statusCode || 500; // Overwrite entire response returnArray = false; results = [{ errors: handleErrors([error], req, res) }]; // If the status code is 500, let’s log our error. if (res.statusCode === 500) { // tslint:disable-next-line no-console console.error(error.stack); } } finally { // Finally, we send the client the results. if (!returnArray) { if (res.statusCode === 200 && results[0].statusCode) { res.statusCode = results[0].statusCode; } results[0].statusCode = undefined; } res.setHeader('Content-Type', 'application/json; charset=utf-8'); const { statusCode, result } = pluginHook('postgraphile:http:end', { statusCode: res.statusCode, result: returnArray ? results : results[0], }, { options, returnArray, req, // For backwards compatibility, the underlying response object. res: res.getNodeServerResponse(), }); if (statusCode) { res.statusCode = statusCode; } res.end(JSON.stringify(result)); if (debugRequest.enabled) { debugRequest('GraphQL ' + (returnArray ? 'queries' : 'query') + ' request finished.'); } } }); /** * 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). */ const middleware = (a, b, c) => { // If are arguments look like the arguments to koa middleware, this is // `koa` middleware. if (isKoaApp(a, b)) { // Set the correct `koa` variable names… const ctx = a; const next = b; const responseHandler = new frameworks_1.PostGraphileResponseKoa(ctx, next); // 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(responseHandler, 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. const req = a; const res = b; const next = c || finalHandler(req, res); const responseHandler = new frameworks_1.PostGraphileResponseNode(req, res, next); // Execute our request handler. If the request errored out, call `next` with the error. requestHandler(responseHandler, next).catch(next); // No return value. } }; middleware.getGraphQLSchema = getGqlSchema; middleware.formatError = formatError; middleware.pgPool = pgPool; middleware.withPostGraphileContextFromReqRes = withPostGraphileContextFromReqRes; middleware.handleErrors = handleErrors; middleware.options = options; middleware.graphqlRoute = graphqlRoute; middleware.graphqlRouteHandler = graphqlRouteHandler; middleware.graphiqlRoute = graphiqlRoute; middleware.graphiqlRouteHandler = graphiql ? graphiqlRouteHandler : null; middleware.faviconRouteHandler = graphiql ? faviconRouteHandler : null; middleware.eventStreamRoute = eventStreamRoute; middleware.eventStreamRouteHandler = watchPg ? eventStreamRouteHandler : null; middleware.shutdownActions = shutdownActions; // Experimental middleware.release = () => shutdownActions.invokeAll(); const hookedMiddleware = pluginHook('postgraphile:middleware', middleware, { options, // Experimental shutdownActions, }); // Sanity check: if (!hookedMiddleware.getGraphQLSchema) { throw new Error("Hook for 'postgraphile:middleware' has not copied over the helpers; e.g. missing `Object.assign(newMiddleware, oldMiddleware)`"); } return hookedMiddleware; } exports.default = createPostGraphileHttpRequestHandler; /** * 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-Allow-Methods', '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 PostGraphile for auth purposes. 'Authorization', // Used by GraphQL Playground and other Apollo-enabled servers 'X-Apollo-Tracing', // The `Content-*` headers are used when making requests with a body, // like in a POST request. 'Content-Type', 'Content-Length', // For our 'Explain' feature 'X-PostGraphile-Explain', ].join(', ')); res.setHeader('Access-Control-Expose-Headers', ['X-GraphQL-Event-Stream'].join(', ')); } function createBadAuthorizationHeaderError() { return httpError(400, 'Authorization header is not of the correct bearer scheme format.'); } /** * 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 */ const 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) { const { authorization } = request.headers; if (Array.isArray(authorization)) throw createBadAuthorizationHeaderError(); // If there was no authorization header, just return null. if (authorization == null) return null; const match = authorizationBearerRex.exec(authorization); // If we did not match the authorization header with our expected format, // throw a 400 error. if (!match) throw createBadAuthorizationHeaderError(); // Return the token from our match. return match[1]; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlUG9zdEdyYXBoaWxlSHR0cFJlcXVlc3RIYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL3Bvc3RncmFwaGlsZS9odHRwL2NyZWF0ZVBvc3RHcmFwaGlsZUh0dHBSZXF1ZXN0SGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw4RUFBOEU7QUFDOUUscUNBV2lCO0FBQ2pCLGdFQUE2RDtBQUU3RCw4Q0FBc0Q7QUFFdEQsbUVBQTREO0FBQzVELHdFQUFpRTtBQUNqRSx1Q0FBZ0M7QUFFaEMsaUNBQTBCO0FBQzFCLGtDQUFtQyxDQUFDLG9DQUFvQztBQUN4RSx5Q0FBMEM7QUFDMUMscUNBQXNDO0FBQ3RDLDZDQUE4QztBQUM5QywwQ0FBMkM7QUFDM0MsaUNBQWtDO0FBRWxDLE1BQU0sUUFBUSxHQUFHLENBQUMsQ0FBTSxFQUFFLENBQU0sRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDLENBQUMsR0FBRyxJQUFJLE9BQU8sQ0FBQyxLQUFLLFVBQVUsQ0FBQztBQUUvRSxNQUFNLGdCQUFnQixHQUFHLE1BQU0sQ0FBQztBQUVoQyxNQUFNLHlCQUF5QixHQUFHLDBCQUEwQixDQUFDO0FBQzdELE1BQU0sSUFBSSxHQUFHLEdBQUcsRUFBRTtJQUNoQixVQUFVO0FBQ1osQ0FBQyxDQUFDO0FBRUYsTUFBTSxFQUFFLFVBQVUsRUFBRSxHQUFHLE1BQU0sQ0FBQztBQUU5Qjs7Ozs7R0FLRztBQUNILDBEQUErQztBQUUvQzs7O0dBR0c7QUFDSCw4REFBMEQ7QUFDMUQsbURBQWtFO0FBQ2xFLDZDQU1zQjtBQUV0Qjs7O0dBR0c7QUFDSCxNQUFNLGdCQUFnQixHQUFHO0lBQ3ZCLEdBQUcsRUFBRSxTQUFTO0lBQ2QsR0FBRyxFQUFFLFNBQVM7SUFDZCxHQUFHLEVBQUUsU0FBUztJQUNkLFFBQVEsRUFBRSxTQUFTO0lBQ25CLFFBQVEsRUFBRSxTQUFTO0NBQ3BCLENBQUM7QUFDRixT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