import createDebugger = require ( 'debug' ) ; import jwt = require ( 'jsonwebtoken' ) ; import { Pool , PoolClient , QueryConfig , QueryResult } from 'pg' ; import { ExecutionResult , OperationDefinitionNode , Kind } from 'graphql' ; import * as sql from 'pg-sql2' ; import { $$pgClient } from '../postgres/inventory/pgClientFromContext' ; import { pluginHookFromOptions } from './pluginHook' ; import { mixed , WithPostGraphileContextOptions } from '../interfaces' ; import { formatSQLForDebugging } from 'postgraphile-core' ; const undefinedIfEmpty = ( o ? ) : undefined | Array < string | RegExp > | string | RegExp => o && ( ! Array . isArray ( o ) || o . length ) ? o : undefined ; interface PostGraphileContext { [ $$pgClient ] : PoolClient ; [ key ] : PoolClient | mixed ; } export type WithPostGraphileContextFn < TResult = ExecutionResult > = ( options : WithPostGraphileContextOptions , callback : ( context : PostGraphileContext ) => Promise < TResult > ) => Promise < TResult > ; const debugPg = createDebugger ( 'postgraphile:postgres' ) ; const debugPgError = createDebugger ( 'postgraphile:postgres:error' ) ; const debugPgNotice = createDebugger ( 'postgraphile:postgres:notice' ) ; /**
 * Formats an error/notice from `pg` and feeds it into a `debug` function.
 */ function debugPgErrorObject ( debugFn , object ) { debugFn ( '%s%s: %s%s%s' , object . severity || 'ERROR' , object . code ? `[${ object . code }]` : '' , object . message || object , object . where ? ` | WHERE: ${ object . where }` : '' , object . hint ? ` | HINT: ${ object . hint }` : '' ) ; } type WithAuthenticatedPgClientFunction = < T > ( cb : ( pgClient : PoolClient ) => Promise < T > ) => Promise < T > ; const simpleWithPgClientCache = new WeakMap < Pool , WithAuthenticatedPgClientFunction > ( ) ; function simpleWithPgClient ( pgPool ) { const cached = simpleWithPgClientCache . get ( pgPool ) ; if ( cached ) { return cached ; } const func = async ( cb ) => { const pgClient = await pgPool . connect ( ) ; try { return await cb ( pgClient ) ; } finally { pgClient . release ( ) ; } } ; simpleWithPgClientCache . set ( pgPool , func ) ; return func ; } const withDefaultPostGraphileContext = async ( options , callback ) : Promise < ExecutionResult > => { const { pgPool , jwtToken , jwtSecret , jwtPublicKey , jwtAudiences , jwtRole = [ 'role' ] , jwtVerifyOptions , pgDefaultRole , pgSettings , explain , queryDocumentAst , operationName , pgForceTransaction , singleStatement , } = options ; let operation ; if ( ! pgForceTransaction && queryDocumentAst ) { for ( let i = 0 , l = queryDocumentAst . definitions . length ; i < l ; i ++ ) { const definition = queryDocumentAst . definitions [ i ] ; if ( definition . kind === Kind . OPERATION_DEFINITION ) { if ( ! operationName && operation ) { throw new Error ( 'Multiple operations present in GraphQL query, you must specify an `operationName` so we know which one to execute.' ) ; } else if ( ! operationName || ( definition . name && definition . name . value === operationName ) ) { operation = definition ; } } } } const operationType = operation != null ? operation . operation : null ; const { role : pgRole , localSettings , jwtClaims } = await getSettingsForPgClientTransaction ( { jwtToken , jwtSecret , jwtPublicKey , jwtAudiences , jwtRole , jwtVerifyOptions , pgDefaultRole , pgSettings , } ) ; const sqlSettings = [ ] ; if ( localSettings . length > 0 ) { const seenKeys = [ ] ; for ( let i = localSettings . length - 1 ; i >= 0 ; i -- ) { const [ key , value ] = localSettings [ i ] ; if ( ! seenKeys . includes ( key ) ) { seenKeys . push ( key ) ; sqlSettings . unshift ( sql . fragment `set_config(${ sql . value ( key ) }, ${ sql . value ( value ) }, true)` ) ; } } } const sqlSettingsQuery = sqlSettings . length > 0 ? sql . compile ( sql . query `select ${ sql . join ( sqlSettings , ', ' ) }` ) : null ; const needTransaction = pgForceTransaction || ! ! sqlSettingsQuery || ( operationType !== 'query' && operationType !== 'subscription' ) ; const withAuthenticatedPgClient = ! needTransaction ? simpleWithPgClient ( pgPool ) : async ( cb ) => { const pgClient = await pgPool . connect ( ) ; await pgClient . query ( 'begin' ) ; try { if ( sqlSettingsQuery ) { await pgClient . query ( sqlSettingsQuery ) ; } return await cb ( pgClient ) ; } finally { try { await pgClient . query ( 'commit' ) ; } finally { pgClient . release ( ) ; } } } ; if ( singleStatement ) { const fakePgClient = { query ( textOrQueryOptions ? , values ? , cb ? ) { if ( ! textOrQueryOptions ) { throw new Error ( 'Incompatible call to singleStatement - no statement passed?' ) ; } else if ( typeof textOrQueryOptions === 'object' ) { if ( values || cb ) { throw new Error ( 'Incompatible call to singleStatement - expected no callback' ) ; } } else if ( typeof textOrQueryOptions !== 'string' ) { throw new Error ( 'Incompatible call to singleStatement - bad query' ) ; } else if ( values && ! Array . isArray ( values ) ) { throw new Error ( 'Incompatible call to singleStatement - bad values' ) ; } else if ( cb ) { throw new Error ( 'Incompatible call to singleStatement - expected to return promise' ) ; } return withAuthenticatedPgClient ( pgClient => pgClient . query ( textOrQueryOptions , values ) ) ; } , } as any ; return callback ( { [ $$pgClient ] : fakePgClient , pgRole , jwtClaims , } ) ; } else { return withAuthenticatedPgClient ( async ( pgClient ) => { let results = null ; if ( explain ) { pgClient . startExplain ( ) ; } try { return await callback ( { [ $$pgClient ] : pgClient , pgRole , jwtClaims , ... ( explain ? { getExplainResults : ( ) : Promise < Array < ExplainResult > > => { results = results || pgClient . stopExplain ( ) ; return results ; } , } : null ) , } ) ; } finally { if ( explain ) { results = results || pgClient . stopExplain ( ) ; } } } ) ; } } ; /**
 * Creates a PostGraphile context object which should be passed into a GraphQL
 * execution. This function will also connect a client from a Postgres pool and
 * setup a transaction in that client.
 *
 * This function is intended to wrap a call to GraphQL-js execution like so:
 *
 * ```js
 * const result = await withPostGraphileContext({
 *   pgPool,
 *   jwtToken,
 *   jwtSecret,
 *   pgDefaultRole,
 * }, async context => {
 *   return await graphql(
 *     schema,
 *     query,
 *     null,
 *     { ...context },
 *     variables,
 *     operationName,
 *   );
 * });
 * ```
 */ const withPostGraphileContext = async ( options , callback ) : Promise < ExecutionResult > => { const pluginHook = pluginHookFromOptions ( options ) ; const withContext = pluginHook ( 'withPostGraphileContext' , withDefaultPostGraphileContext , { options , } ) ; return withContext ( options , callback ) ; } ; export default withPostGraphileContext ; /**
 * Sets up the Postgres client transaction by decoding the JSON web token and
 * doing some other cool things.
 */ async function getSettingsForPgClientTransaction ( { jwtToken , jwtSecret , jwtPublicKey , jwtAudiences , jwtRole , jwtVerifyOptions , pgDefaultRole , pgSettings , } ) { let role = pgDefaultRole ; let jwtClaims = { } ; if ( jwtToken ) { try { const jwtVerificationSecret = jwtPublicKey || jwtSecret ; if ( ! Buffer . isBuffer ( jwtVerificationSecret ) && typeof jwtVerificationSecret !== 'string' && typeof jwtVerificationSecret !== 'function' ) { console . error ( `ERROR: '${ jwtPublicKey ? 'jwtPublicKey' : 'jwtSecret' }' was not set to a string or buffer - rejecting JWT-authenticated request.` ) ; throw new Error ( 'Not allowed to provide a JWT token.' ) ; } if ( jwtAudiences != null && jwtVerifyOptions && 'audience' in jwtVerifyOptions ) throw new Error ( `Provide either 'jwtAudiences' or 'jwtVerifyOptions.audience' but not both` ) ; const claims = await new Promise ( ( resolve , reject ) => { jwt . verify ( jwtToken , jwtVerificationSecret , { ... jwtVerifyOptions , audience : jwtAudiences || ( jwtVerifyOptions && 'audience' in ( jwtVerifyOptions as Record < string , any > ) ? undefinedIfEmpty ( jwtVerifyOptions . audience ) : [ 'postgraphile' ] ) , } , ( err , decoded ) => { if ( err ) reject ( err ) ; else resolve ( decoded ) ; } ) ; } ) ; if ( typeof claims === 'string' ) { throw new Error ( 'Invalid JWT payload' ) ; } jwtClaims = claims as typeof jwtClaims ; const roleClaim = getPath ( jwtClaims , jwtRole ) ; if ( typeof roleClaim !== 'undefined' ) { if ( typeof roleClaim !== 'string' ) throw new Error ( `JWT \`role\` claim must be a string. Instead found '${ typeof jwtClaims [ 'role' ] }'.` ) ; role = roleClaim ; } } catch ( error ) { error . statusCode = 'name' in error && error . name === 'TokenExpiredError' ? 401 : 403 ; throw error ; } } const localSettings = [ ] ; if ( pgSettings && typeof pgSettings === 'object' ) { for ( const key in pgSettings ) { if ( Object . prototype . hasOwnProperty . call ( pgSettings , key ) && isPgSettingValid ( pgSettings [ key ] ) ) { if ( key === 'role' ) { role = String ( pgSettings [ key ] ) ; } else { localSettings . push ( [ key , String ( pgSettings [ key ] ) ] ) ; } } } } if ( typeof role === 'string' ) { localSettings . push ( [ 'role' , role ] ) ; } for ( const key in jwtClaims ) { if ( Object . prototype . hasOwnProperty . call ( jwtClaims , key ) ) { const rawValue = jwtClaims [ key ] ; const value = rawValue != null && typeof rawValue === 'object' ? JSON . stringify ( rawValue ) : rawValue ; if ( isPgSettingValid ( value ) ) { localSettings . push ( [ `jwt.claims.${ key }` , String ( value ) ] ) ; } } } return { localSettings , role , jwtClaims : jwtToken ? jwtClaims : null , } ; } const $$pgClientOrigQuery = Symbol ( ) ; interface RawExplainResult { query : string ; result : any ; } type ExplainResult = Omit < RawExplainResult , 'result' > & { plan : string ; } ; declare module 'pg' { interface ClientBase { _explainResults : Array < RawExplainResult > | null ; startExplain : ( ) => void ; stopExplain : ( ) => Promise < Array < ExplainResult > > ; } } /**
 * Monkey-patches the `query` method of a pg Client to add debugging
 * functionality. Use with care.
 */ export function debugPgClient ( pgClient , allowExplain = false ) { if ( ! pgClient [ $$pgClientOrigQuery ] ) { pgClient [ $$pgClientOrigQuery ] = pgClient . query ; pgClient . startExplain = ( ) => { pgClient . _explainResults = [ ] ; } ; pgClient . stopExplain = async ( ) => { const results = pgClient . _explainResults ; pgClient . _explainResults = null ; if ( ! results ) { return Promise . resolve ( [ ] ) ; } return ( await Promise . all ( results . map ( async ( r ) => { const { result : resultPromise , ... rest } = r ; const result = await resultPromise ; const firstKey = result && result [ 0 ] && Object . keys ( result [ 0 ] ) [ 0 ] ; if ( ! firstKey ) { return null ; } const plan = result . map ( ( r ) => r [ firstKey ] ) . join ( '\n' ) ; return { ... rest , plan , } ; } ) ) ) . filter ( ( entry ) : entry is ExplainResult => ! ! entry ) ; } ; if ( debugPgNotice . enabled ) { pgClient . on ( 'notice' , ( msg ) => { debugPgErrorObject ( debugPgNotice , msg ) ; } ) ; } const logError = ( error ) => { if ( error . name && error [ 'severity' ] ) { debugPgErrorObject ( debugPgError , error as PgNotice ) ; } else { debugPgError ( '%O' , error ) ; } } ; if ( debugPg . enabled || debugPgNotice . enabled || allowExplain ) { pgClient . query = function ( ... args ) : any { const [ a , b , c ] = args ; if ( ( typeof a === 'string' && ! c && ( ! b || Array . isArray ( b ) ) ) || ( typeof a === 'object' && ! b && ! c ) ) { if ( debugPg . enabled ) { debugPg ( '%s' , formatSQLForDebugging ( a && a . text ? a . text : a ) ) ; } if ( pgClient . _explainResults ) { const query = a && a . text ? a . text : a ; const values = a && a . text ? a . values : b ; if ( query . match ( /^\s*(select|insert|update|delete|with)\s/i ) && ! query . includes ( ';' ) ) { const explain = `explain ${ query }` ; pgClient . _explainResults . push ( { query , result : pgClient [ $$pgClientOrigQuery ] . call ( this , explain , values ) . then ( ( data ) => data . rows ) . catch ( ( ) => null ) , } ) ; } } const promiseResult = pgClient [ $$pgClientOrigQuery ] . apply ( this , args ) ; if ( debugPgError . enabled ) { promiseResult . catch ( logError ) ; } return promiseResult ; } else { return pgClient [ $$pgClientOrigQuery ] . apply ( this , args ) ; } } ; } } return pgClient ; } private function getPath ( inObject , path ) { let object = inObject ; let index = 0 ; const length = path . length ; while ( object && index < length ) { object = object [ path [ index ++ ] ] ; } return index && index === length ? object : undefined ; } private function isPgSettingValid ( pgSetting ) { if ( pgSetting === undefined || pgSetting === null ) { return false ; } const typeOfPgSetting = typeof pgSetting ; if ( typeOfPgSetting === 'string' || typeOfPgSetting === 'number' || typeOfPgSetting === 'boolean' ) { return true ; } throw new Error ( `Error converting pgSetting: ${ typeof pgSetting } needs to be of type string, number or boolean.` ) ; } interface PgNotice { readonly name : 'notice' ; readonly message : string | undefined ; length : number ; severity : string | undefined ; code : string | undefined ; detail : string | undefined ; hint : string | undefined ; where : string | undefined ; schema : string | undefined ; table : string | undefined ; column : string | undefined ; constraint : string | undefined ; file : string | undefined ; line : string | undefined ; routine : string | undefined ; }