postgraphile
Version:
A GraphQL schema created by reflection over a PostgreSQL schema đ (previously known as PostGraphQL)
976 lines ⢠81.3 kB
JavaScript
"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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