UNPKG

@softvisio/core

Version:
1,128 lines (890 loc) • 33.2 kB
import "#lib/result"; import crypto from "node:crypto"; import net from "node:net"; import tls from "node:tls"; import Queue from "#lib/data-structures/queue"; import mixins from "#lib/mixins"; import Numeric from "#lib/numeric"; import Sasl from "#lib/sasl"; import constants from "#lib/sql/constants"; import Dbh from "#lib/sql/dbh"; import { createOffsetLimit, sql } from "#lib/sql/query"; import { encodeBuffer, encodeDate } from "#lib/sql/types"; import { READ_ONLY_SQL_TRANSACTION } from "./error-codes.js"; import LargeObject from "./large-object.js"; import MessageBuffer from "./message-buffer.js"; import Transactions from "./transactions.js"; const PROTOCOL_VERSION = Buffer.from( [ 0x00, 0x03, 0x00, 0x00 ] ); // v3 const MAX_PARAMS = 65_535; // FRONTEND MESSAGES const FRONTEND = { "SASL_RESPONSE": "p", "BIND": "B", "CANCEL_REQUEST": "", "CLOSE": "C", "CLOSE_STATEMENT": "S", "CLOSE_PORTAL": "P", "DESCRIBE": "D", "DESCRIBE_STATEMENT": "S", "DESCRIBE_PORTAL": "P", "EXECUTE": "E", "FLUSH": "H", "FUNCTION_CALL": "F", "PARSE": "P", "PASSWORD_MESSAGE": "p", "QUERY": "Q", "SSL_REQUEST": "", "STARTUP_MESSAGE": "", "SYNC": "S", "TERMINATE": "X", }; // BACKEND MESSAGES const BACKEND = { // AUTH "AUTHENTICATION": "R", // AUTH TYPE "AUTHENTICATION_OK": 0, "AUTHENTICATION_KERBEROS_V5": 2, "AUTHENTICATION_CLEARTEXT_PASSWORD": 3, "AUTHENTICATION_MD5_PASSWORD": 5, "AUTHENTICATION_SCM_CREDENTIAL": 6, "AUTHENTICATION_GSS": 7, "AUTHENTICATION_GSS_CONTINUE": 8, "AUTHENTICATION_SSPI": 9, "AUTHENTICATION_SASL": 10, "AUTHENTICATION_SASL_CONTINUE": 11, "AUTHENTICATION_SASL_FINAL": 12, "BACKEND_KEY_DATA": "K", "BIND_COMPLETE": "2", "CLOSE_COMPLETE": "3", "COMMAND_COMPLETE": "C", "DATA_ROW": "D", "EMPTY_QUERY_RESPONSE": "I", "ERROR_RESPONSE": "E", "FUNCTION_CALL_RESPONSE": "V", "NO_DATA": "n", "NOTICE_RESPONSE": "N", "NOTIFICATION_RESPONSE": "A", "PARAMETER_DESCRIPTION": "t", "PARAMETER_STATUS": "S", "PARSE_COMPLETE": "1", "PORTAL_SUSPENDED": "s", "READY_FOR_QUERY": "Z", "ROW_DESCRIPTION": "T", }; const BACKEND_METHOD = Object.fromEntries( Object.keys( BACKEND ).map( key => [ BACKEND[ key ], "_ON_PG_" + key ] ) ); const TRANSACTION_STATE = { "I": false, // idle, not in transaction "T": true, // in transaction block "E": true, // in a failed transaction block (queries will be rejected until block is ended) }; // COPY // const COPY_DATA = "d"; // frontend, backend // const COPY_DONE = "c"; // frontend, backend // const COPY_FAIL = "f"; // frontend // const COPY_IN_RESPONSE = "G"; // backend // const COPY_OUT_RESPONSE = "H"; // backend // const COPY_BOTH_RESPONSE = "W"; // backend const ERROR_RESPONSE_TYPE = { "S": "severity", "C": "code", "M": "message", "D": "detail", "H": "hint", "P": "position", "p": "internal_position", "q": "internal_query", "W": "where", "F": "file", "L": "line", "R": "routine", "V": "text", }; const messageBuffer = new MessageBuffer(); export default class DbhPostgreSql extends mixins( Transactions, LargeObject, Dbh ) { #pool; #username; #database; #standbyAddress; #sessionParams = {}; #pid; #secret; // eslint-disable-line no-unused-private-class-members #socket; #rbuf; #wbuf = []; #sasl; #queue = new Queue(); #prepared = {}; #isDestroyed; #isConnected = false; #isPrimary; #transactionState = "I"; // in "idle" state by default #abortController = new AbortController(); #sth; constructor ( pool, options = {} ) { super(); this.#pool = pool; this.#username = ( options.username || this.#pool.username ) ?? ""; this.#database = ( options.database || this.#pool.database ) ?? ""; this.#isDestroyed = !!options.destroyed; this.#isPrimary = !options.standbyAddress; this.#standbyAddress = options.standbyAddress; if ( !this.#isDestroyed ) this.#connect(); } // properties get isPostgreSql () { return true; } get mainPid () { return this.#pool.mainPid; } get url () { return this.#pool.url; } get isPrimary () { return this.#isPrimary; } get schema () { return this.#pool.schema; } get standbyAddress () { return this.#standbyAddress; } get inTransaction () { return TRANSACTION_STATE[ this.#transactionState ]; } get isDestroyed () { return this.#isDestroyed; } get pid () { return this.#pid; } get main () { return this.#pool; } get abortSignal () { return this.#abortController.signal; } // public toString () { return this.url; } toJSON () { return this.url; } quote ( value ) { return this.#pool.quote( value ); } destroy ( res ) { if ( this.#isDestroyed ) return; this.#isDestroyed = true; this.#isConnected = false; // cleanup socket this.#socket.removeAllListeners(); this.#socket.destroy(); this.#socket = null; // create result if ( !res ) { res = result.exception( [ 500, "Database connection closed" ] ); } else { res = result.exception( res ); } // cleanup queue var queue = this.#queue; this.#queue = new Queue(); // finish current request this.#finishRequest( res ); // finish all pending requests for ( const sth of queue ) { sth.resolve( res ); } this.#abortController.abort(); this.emit( "destroy", this, res ); } #send ( buf ) { if ( !this.#isConnected ) { this.#wbuf.push( buf ); } else { this.#socket.write( buf ); } } #connect () { // primary connection if ( this.#isPrimary ) { this.#socket = this.#pool.socket ? net.connect( this.#pool.socket ) : net.connect( this.#pool.port, this.#pool.hostname ); } // standby connection else { this.#socket = net.connect( this.#pool.standbyPort, this.#standbyAddress ); } // upgrade socket if ( this.#pool.protocol === "postgresql+tls:" ) { this.#socket = tls.connect( { "socket": this.#socket, "servername": this.#isPrimary ? this.#pool.hostname : this.#pool.standbyHostname, "rejectUnauthorized": this.#pool.checkCertificate, } ); this.#socket.once( "secureConnect", this.#onConnect.bind( this ) ); } else { this.#socket.once( "connect", this.#onConnect.bind( this ) ); } this.#socket.setKeepAlive( true, 60_000 ); this.#socket.on( "data", this.#onData.bind( this ) ); this.#socket.once( "close", hadError => this.destroy( result( [ 500, "DBH is disconnected" ] ) ) ); this.#socket.once( "error", e => this.destroy( result( [ 500, e.message ] ) ) ); } #onConnect () { const params = { "user": this.#username, "database": this.#database, "options": "--client-min-messages=warning", "replication": "false", "application_name": this.#pool.appName, // session run-time params "client_encoding": "utf8", "bytea_output": "hex", "backslash_quote": "off", "standard_conforming_strings": "on", }; messageBuffer.beginMsg( FRONTEND.STARTUP_MESSAGE ).write( PROTOCOL_VERSION ); Object.keys( params ).forEach( param => messageBuffer.write( param + "\0" + params[ param ] + "\0" ) ); this.#socket.write( messageBuffer.writeNull().endMsg().done() ); } #finishRequest ( res ) { var sth = this.#sth; if ( !sth ) { this._checkIdle(); return; } this.#sth = this.#queue.shift(); // compose result if ( !res ) { if ( sth.error ) { res = this._onQueryError( sth.error, sth.query, { "meta": sth.error, "silent": sth.options?._readOnlyTest && sth.error?.code === READ_ONLY_SQL_TRANSACTION, } ); } else { let columns; if ( sth.id ) { columns = this.#prepared[ sth.id ].columns; // prepared query finished succesfully this.#prepared[ sth.id ].described = true; } else { columns = sth.columns; } // check correct dbh method usage if ( sth.options.ignoreData ) { // query returns if ( columns ) { res = this._onQueryError( `Invalid usage, you need to execute query, that returns data, using "select" method`, sth.query ); } } // query returns no data else if ( !columns ) { res = this._onQueryError( `Invalid usage, you need to execute query, that returns no data, using "do" method`, sth.query ); } if ( !res ) { res = result( 200, sth.data.length ? sth.data : null ); if ( typeof sth.rows !== "undefined" ) res.meta.rows = sth.rows; } } } this._checkIdle(); sth.resolve( res ); } #onData ( data ) { if ( this.#rbuf ) { this.#rbuf = Buffer.concat( [ this.#rbuf, data ] ); } else { this.#rbuf = data; } while ( this.#rbuf.length > 4 ) { var length = this.#rbuf.readUInt32BE( 1 ); if ( this.#rbuf.length >= length + 1 ) { const msgId = this.#rbuf.toString( "latin1", 0, 1 ), body = this.#rbuf.subarray( 5, length + 1 ); this.#rbuf = this.#rbuf.subarray( length + 1 ); const method = this[ BACKEND_METHOD[ msgId ] ]; if ( !method ) throw `Unhandled PostgreSQL message "${ msgId }"`; method.call( this, body ); } else { break; } } } // postgres message handlers async _ON_PG_AUTHENTICATION ( data ) { var type = data.readUInt32BE(); if ( type === BACKEND.AUTHENTICATION_OK ) { // auth is ok } else if ( type === BACKEND.AUTHENTICATION_CLEARTEXT_PASSWORD ) { this.#socket.write( messageBuffer .beginMsg( FRONTEND.PASSWORD_MESSAGE ) .write( this.#pool.password ?? "" ) .writeNull() .endMsg() .done() ); } else if ( type === BACKEND.AUTHENTICATION_MD5_PASSWORD ) { const pwdhash = crypto .createHash( "MD5" ) .update( ( this.#pool.password ?? "" ) + this.#username, "utf8" ) .digest( "hex" ); const hash = "md5" + crypto .createHash( "MD5" ) .update( pwdhash + data.subarray( 4 ).toString( "latin1" ), "latin1" ) .digest( "hex" ); this.#socket.write( messageBuffer.beginMsg( FRONTEND.PASSWORD_MESSAGE ).write( hash ).writeNull().endMsg().done() ); } else if ( type === BACKEND.AUTHENTICATION_SASL ) { const mechanisms = data.subarray( 4 ).toString().split( "\0" ); this.#sasl = await Sasl.new( mechanisms, this.#username, this.#pool.password ); if ( !this.#sasl ) return this.destroy( result( [ 500, "Unsupported authentication method" ] ) ); const response = this.#sasl.continue(); // SASLInitialResponse this.#socket.write( messageBuffer.beginMsg( FRONTEND.SASL_RESPONSE ).write( this.#sasl.type ).writeNull().writeUInt32BE( response.length ).write( response ).endMsg().done() ); } else if ( type === BACKEND.AUTHENTICATION_SASL_CONTINUE ) { const response = this.#sasl.continue( data.subarray( 4 ).toString() ); if ( !response ) return this.destroy( result( [ 500, "Unsupported authentication method" ] ) ); // SASLResponse this.#socket.write( messageBuffer.beginMsg( FRONTEND.SASL_RESPONSE ).write( response ).endMsg().done() ); } else if ( type === BACKEND.AUTHENTICATION_SASL_FINAL ) { const ok = this.#sasl.continue( data.subarray( 4 ).toString() ); this.#sasl = null; if ( !ok ) this.destroy( result( [ 500, "Unsupported authentication method" ] ) ); } else { this.destroy( result( [ 500, "Unsupported authentication method" ] ) ); } } _ON_PG_PARAMETER_STATUS ( data ) { var [ key, val ] = data.toString().split( "\0" ); this.#sessionParams[ key ] = val; } _ON_PG_BACKEND_KEY_DATA ( data ) { // Identifies the message as cancellation key data. // The frontend must save these values if it wishes to be able to issue CancelRequest messages later. this.#pid = data.readUInt32BE(); this.#secret = data.readUInt32BE( 4 ); this.emit( "connect", this ); } // ready for query _ON_PG_READY_FOR_QUERY ( data ) { this.#transactionState = data.toString(); // dbh is connected if ( !this.#isConnected ) { this.#isConnected = true; // send pending messages if ( this.#wbuf.length ) { if ( this.#wbuf.length === 1 ) { this.#socket.write( this.#wbuf[ 0 ] ); } else { this.#socket.write( Buffer.concat( this.#wbuf ) ); } this.#wbuf = []; } } // query is finished else { this.#finishRequest(); } } // error response _ON_PG_ERROR_RESPONSE ( data ) { var error = {}; // parse error message for ( const field of data.toString().split( "\0" ) ) { if ( !field ) continue; error[ ERROR_RESPONSE_TYPE[ field.charAt( 0 ) ] ] = field.slice( 1 ); } // error during connection if ( !this.#isConnected ) { this.destroy( result( [ 500, error.message ] ) ); } // query error else if ( this.#sth ) { this.#sth.error = error; } // unknown error else { this.destroy( result( [ 500, error.message ] ) ); } } _ON_PG_EMPTY_QUERY_RESPONSE ( data ) { // identifies the message as a response to an empty query string. (This substitutes for CommandComplete.) this._ON_PG_COMMAND_COMPLETE( data ); } _ON_PG_ROW_DESCRIPTION ( data ) { var types = this.#sth.types, decoder, numOfCols = data.readInt16BE(), pos = 2, columns = []; for ( let n = 0; n < numOfCols; n++ ) { let idx = data.indexOf( "\0", pos ); const column = [ // data.toString( "latin1", pos, idx ), // 0, column name data.readInt32BE( ( idx += 1 ) ), // 1, if the field can be identified as a column of a specific table, the object ID of the table; otherwise zero. data.readInt16BE( ( idx += 4 ) ), // 2, if the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero. data.readInt32BE( ( idx += 2 ) ), // 3, the object ID of the field's data type. data.readInt16BE( ( idx += 4 ) ), // 4, the data type size (see pg_type.typlen). Note that negative values denote variable-width types., -1 - indicates a "varlena" type (one that has a length word);, -2 - indicates a null-terminated C string; data.readInt32BE( ( idx += 2 ) ), // 5, the type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific. data.readInt16BE( ( idx += 4 ) ), // 6, the format code being used for the field. Currently will be 0 (text) or 1 (binary). In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero. ]; pos = idx + 2; // get decoder by column name if ( types?.[ column[ 0 ] ] ) { decoder = types[ column[ 0 ] ]; if ( typeof decoder === "string" ) { decoder = this.#pool.decode[ decoder ]; if ( !decoder ) this.#sth.error = new Error( `Invalid decode type for column "${ column[ 0 ] }"` ); } } // get decoder by type oid (or base type for domains) else { decoder = this.#pool.decode[ column[ 3 ] ]; } // store type decoder column.push( decoder ); // add column columns.push( column ); } this.#sth.columns = columns; // query is prepared, store columns descriptions if ( this.#sth.id ) this.#prepared[ this.#sth.id ].columns = columns; } _ON_PG_NO_DATA ( data ) {} _ON_PG_DATA_ROW ( data ) { // ignore data if ( this.#sth.options.ignoreData ) return; var numOfCols = data.readUInt16BE(), columns = this.#sth.id ? this.#prepared[ this.#sth.id ].columns : this.#sth.columns, pos = 2, row = {}; for ( let n = 0; n < numOfCols; n++ ) { const length = data.readInt32BE( pos ); let col; pos += 4; // col is "" if ( !length ) { col = ""; } // col is null else if ( length === -1 ) { col = null; } // col has data else { const decoder = columns[ n ][ 7 ]; // column has decoder if ( decoder ) { col = decoder( data.subarray( pos, ( pos += length ) ) ); } // decode to utf8 string by default else { col = data.toString( "utf8", pos, ( pos += length ) ); } } row[ columns[ n ][ 0 ] ] = col; } this.#sth.data.push( row ); } _ON_PG_COMMAND_COMPLETE ( data ) { const rowsTag = data.toString( "latin1", 0, data.length - 1 ); const rows = rowsTag.match( /\s(\d+)$/ ); if ( rows ) this.#sth.rows = +rows[ 1 ]; } _ON_PG_PARSE_COMPLETE ( data ) {} _ON_PG_BIND_COMPLETE ( data ) {} _ON_PG_CLOSE_COMPLETE ( data ) {} _ON_PG_PORTAL_SUSPENDED ( data ) {} _ON_PG_NOTICE_RESPONSE ( data ) { var error = {}; for ( const field of data.toString().split( "\0" ) ) { if ( !field ) continue; error[ ERROR_RESPONSE_TYPE[ field.charAt( 0 ) ] ] = field.slice( 1 ); } console.log( "PostgreSQL notice:", error.severity, error.message + ( error.hint ? ", " + error.hint : "" ) ); } _ON_PG_NOTIFICATION_RESPONSE ( data ) { // const pid = data.readInt32BE(); var [ name, payload ] = data.subarray( 4 ).toString().split( "\0" ); // reserved event if ( constants.reservedEvents.has( name ) ) { if ( this.#pool.schema.isLoaded ) throw new Error( `PostgreSQL event name "${ name }" is reserved` ); else return; } // unknown event if ( this.#pool.schema.isLoaded && !this.#pool.schema.isEventValid( name ) ) throw new Error( `PostgreSQL event name "${ name }" is unknown` ); // no listeners, ignore event if ( !this.#pool.hasListeners( name ) ) return; // no payload if ( !payload ) { this.#pool.emit( name ); } // has payload else { // decode payload try { payload = JSON.parse( payload ); } catch { // invalid payload console.log( `Unable to decode JSON notification payload for event: "${ name }":\n${ payload }` ); return; } this.#pool.emit( name, payload ); } } // query async exec ( query, params ) { return new Promise( resolve => { this.#do( resolve, query, params, { "exec": true, "ignoreData": true, } ); } ); } async do ( query, params ) { return new Promise( resolve => { this.#do( resolve, query, params, { "ignoreData": true, } ); } ); } async select ( query, params ) { return new Promise( resolve => { this.#do( resolve, query, params ); } ); } async selectRow ( query, params ) { return new Promise( resolve => { this.#do( res => { if ( res.data ) { res.data = res.data[ 0 ]; res.meta.rows = 1; } else { res.meta.rows = 0; } resolve( res ); }, query, params, { "maxRows": 1, } ); } ); } async read ( query, { summaryQuery, orderBy, offset, limit, defaultLimit, maxLimit, maxResults } = {} ) { var maxResultsLimit; ( { offset, limit, maxResultsLimit } = createOffsetLimit( offset, limit, { defaultLimit, maxLimit, maxResults } ) ); // nothing to fetch if ( limit === 0 ) return result( 200, null, { "next_page": false } ); var summary, totalRows; // has summary query if ( summaryQuery ) { // cache query const preparedQuery = summaryQuery.prepare().decode( { "total": "int53" } ); summary = await this.selectRow( preparedQuery, summaryQuery.params ); // summary query error if ( !summary.ok ) return summary; summary = summary.data; // has total count of rows if ( "total" in summary ) { totalRows = summary.total; // no results if ( !totalRows ) { return result( 200, null, { "total_rows": 0, "next_page": false, summary, } ); } // do not perform main query if offset >= total rows else if ( offset && offset >= totalRows ) { return result( 200, null, { "total_rows": summary.total, "next_page": false, summary, } ); } } } // add 1 row to the limit if: // we don't know total number of rows // we don't request all results var addLimit = false; if ( totalRows == null && limit && !maxResultsLimit ) addLimit = true; // execute main query const fullQuery = query .ORDER_BY( orderBy ) .OFFSET( offset ) .LIMIT( addLimit ? limit + 1 : limit ); // cache query const preparedQuery = fullQuery.prepare(); const res = await this.select( preparedQuery, fullQuery.params ); if ( res.ok ) { if ( summary ) res.meta.summary = summary; // we know total number of the rows if ( totalRows != null ) { res.meta.total_rows = totalRows; res.meta.next_page = offset + res.meta.rows < totalRows; } // we added 1 row to the limit else if ( addLimit ) { // number of the returned rows > number of the requested rows if ( res.meta.rows > limit ) { res.meta.next_page = true; // decrement results res.data.pop(); res.meta.rows--; } else { res.meta.next_page = false; } } // we requested max. allowed number of the rows else { res.meta.next_page = false; } } return res; } // protected _getConnection () { return this; } _checkIdle () { // idle if ( !this.#isDestroyed && !this.isLocked && !this.#sth && !this.#queue.length ) this.emit( "idle", this ); } // private #do ( resolve, query, params, options = {} ) { // dbh is already destroyed if ( this.#isDestroyed ) { resolve( result.exception( [ 500, "Database connection closed" ] ) ); return; } if ( params && !Array.isArray( params ) ) { options = { ...params, ...options }; params = options.params; } query = this.#prepareQuery( query, params, options.exec ); var sth = { resolve, "id": query.id ?? "", "query": query.query, "types": query.types, options, "error": null, "columns": null, "data": [], }; // use simple query if query has no params, query is not prepared, no maxRows defined const useSimpleQuery = !( query.params || sth.id || options.maxRows ); // for simple query multiple queries in single statement are allowed if ( useSimpleQuery ) { messageBuffer.beginMsg( FRONTEND.QUERY ).write( sth.query ).writeNull().endMsg(); } // extended query mode else { const portalId = ""; // uuid(), currently we use unnamed portals // query is prepared if ( sth.id ) { if ( !this.#prepared[ sth.id ] ) { this.#prepared[ sth.id ] = { "described": false, }; } else { sth.parsed = true; } } // parse query if ( !sth.parsed ) { messageBuffer .beginMsg( FRONTEND.PARSE ) .write( sth.id + "\0" + sth.query + "\0\0\0" ) .endMsg(); } // bind, create portal messageBuffer .beginMsg( FRONTEND.BIND ) .write( portalId + "\0" + sth.id + "\0" ) .writeUInt16BE( 0 ) // params format text .writeUInt16BE( query.params?.length ?? 0 ); // params length // prepare params if ( query.params ) { for ( let param of query.params ) { // null or undefined if ( param == null ) { messageBuffer.writeInt32BE( -1 ); } else { if ( typeof param !== "string" ) { // number if ( typeof param === "number" ) { param = param.toString(); } // boolean else if ( typeof param === "boolean" ) { param = param ? "t" : "f"; } // bigint else if ( typeof param === "bigint" ) { param = param.toString(); } // object else if ( typeof param === "object" ) { // buffer if ( Buffer.isBuffer( param ) ) { param = encodeBuffer( param ); } // date else if ( param instanceof Date ) { param = encodeDate( param ); } // numeric else if ( Numeric.isNumeric( param ) ) { param = param.toString(); } // object else { param = JSON.stringify( param ); } } // error else { throw TypeError( `Unsupported SQL parameter type "${ param }"` ); } } messageBuffer.writeInt32BE( Buffer.byteLength( param ) ).write( param ); } } } // finish bind msg messageBuffer.writeNull( 2 ).endMsg(); // request portal description if ( !sth.id || !this.#prepared[ sth.id ].described ) { messageBuffer .beginMsg( FRONTEND.DESCRIBE ) .write( FRONTEND.DESCRIBE_PORTAL + portalId ) .writeNull() .endMsg(); } // execute messageBuffer .beginMsg( FRONTEND.EXECUTE ) .write( portalId ) .writeNull() .writeUInt32BE( options.maxRows || 0 ) .endMsg(); // close portal if maxRows was used if ( options.maxRows ) { messageBuffer .beginMsg( FRONTEND.CLOSE ) .write( FRONTEND.CLOSE_PORTAL + portalId + "\0" ) .endMsg(); } // flush messageBuffer.beginMsg( FRONTEND.FLUSH ).endMsg(); // sync messageBuffer.beginMsg( FRONTEND.SYNC ).endMsg(); } if ( !this.#sth ) { this.#sth = sth; } else { this.#queue.push( sth ); } this.#send( messageBuffer.done() ); } #prepareQuery ( query, params, toString ) { var sth; if ( typeof query === "string" ) { query = sql( query ); } else { params ||= query.params; } if ( !params?.length ) params = null; if ( params && ( toString || params.length > MAX_PARAMS ) ) { sth = { "query": this.queryToString( query, params ), "types": query.types, }; } else { sth = { "id": query.id, "query": query.postgresqlQuery, params, "types": query.types, }; } return sth; } // notifications get isConnected () { return this.#pool.isConnected; } async waitConnect ( signal ) { return this.#pool.waitConnect( signal ); } on ( name, listener ) { return this.#on( name, listener, false ); } once ( name, listener ) { return this.#on( name, listener, true ); } #on ( name, listener, once ) { if ( name === "commit" || name === "rollback" ) { if ( !this.inTransaction ) throw new Error( `Unable to use "${ name }" event when transaction is not started` ); return super.once( name, listener ); } else if ( name === "unlock" ) { if ( !this.inTransaction ) throw new Error( `Unable to use "${ name }" event when dbh is not locked` ); return super.once( name, listener ); } else { if ( once ) { return super.once( name, listener ); } else { return super.on( name, listener ); } } } }