'use strict'; const _ = require('lodash'); const AbstractConnectionManager = require('../abstract/connection-manager'); const { logger } = require('../../utils/logger'); const debug = logger.debugContext('connection:pg'); const sequelizeErrors = require('../../errors'); const semver = require('semver'); const dataTypes = require('../../data-types'); const momentTz = require('moment-timezone'); const { promisify } = require('util'); class ConnectionManager extends AbstractConnectionManager { constructor(dialect, sequelize) { sequelize.config.port = sequelize.config.port || 5432; super(dialect, sequelize); const pgLib = this._loadDialectModule('pg'); this.lib = this.sequelize.config.native ? pgLib.native : pgLib; this._clearDynamicOIDs(); this._clearTypeParser(); this.refreshTypeParser(dataTypes.postgres); } // Expose this as a method so that the parsing may be updated when the user has added additional, custom types _refreshTypeParser(dataType) { const arrayParserBuilder = parser => { return value => this.lib.types.arrayParser.create(value, parser).parse(); }; const rangeParserBuilder = parser => { return value => dataType.parse(value, { parser }); }; // Set range parsers if (dataType.key.toLowerCase() === 'range') { for (const name in this.nameOidMap) { const entry = this.nameOidMap[name]; if (! entry.rangeOid) continue; const rangeParser = rangeParserBuilder(this.getTypeParser(entry.oid)); const arrayRangeParser = arrayParserBuilder(rangeParser); this.oidParserMap.set(entry.rangeOid, rangeParser); if (! entry.arrayRangeOid) continue; this.oidParserMap.set(entry.arrayRangeOid, arrayRangeParser); } return; } // Create parsers for normal or enum data types const parser = value => dataType.parse(value); const arrayParser = arrayParserBuilder(parser); // Set enum parsers if (dataType.key.toLowerCase() === 'enum') { this.enumOids.oids.forEach(oid => { this.oidParserMap.set(oid, parser); }); this.enumOids.arrayOids.forEach(arrayOid => { this.oidParserMap.set(arrayOid, arrayParser); }); return; } // Set parsers for normal data types dataType.types.postgres.forEach(name => { if (! this.nameOidMap[name]) return; this.oidParserMap.set(this.nameOidMap[name].oid, parser); if (! this.nameOidMap[name].arrayOid) return; this.oidParserMap.set(this.nameOidMap[name].arrayOid, arrayParser); }); } _clearTypeParser() { this.oidParserMap = new Map(); } getTypeParser(oid, ...args) { if (this.oidParserMap.get(oid)) return this.oidParserMap.get(oid); return this.lib.types.getTypeParser(oid, ...args); } async connect(config) { config.user = config.username; const connectionConfig = _.pick(config, [ 'user', 'password', 'host', 'database', 'port' ]); connectionConfig.types = { getTypeParser: ConnectionManager.prototype.getTypeParser.bind(this) }; if (config.dialectOptions) { _.merge(connectionConfig, _.pick(config.dialectOptions, [ // see [http://www.postgresql.org/docs/9.3/static/runtime-config-logging.html#GUC-APPLICATION-NAME] 'application_name', // choose the SSL mode with the PGSSLMODE environment variable // object format: [https://github.com/brianc/node-postgres/blob/ee19e74ffa6309c9c5e8e01746261a8f651661f8/lib/connection.js#L79] // see also [http://www.postgresql.org/docs/9.3/static/libpq-ssl.html] 'ssl', // In addition to the values accepted by the corresponding server, // you can use "auto" to determine the right encoding from the // current locale in the client (LC_CTYPE environment variable on Unix systems) 'client_encoding', // !! DO NOT SET THIS TO TRUE !! // (unless you know what you're doing) // see [http://www.postgresql.org/message-id/flat/bc9549a50706040852u27633f41ib1e6b09f8339d845@mail.gmail.com#bc9549a50706040852u27633f41ib1e6b09f8339d845@mail.gmail.com] 'binary', // This should help with backends incorrectly considering idle clients to be dead and prematurely disconnecting them. // this feature has been added in pg module v6.0.0, check pg/CHANGELOG.md 'keepAlive', // Times out queries after a set time in milliseconds in the database end. Added in pg v7.3 'statement_timeout', // Times out queries after a set time in milliseconds in client end, query would be still running in database end. 'query_timeout', // Terminate any session with an open transaction that has been idle for longer than the specified duration in milliseconds. Added in pg v7.17.0 only supported in postgres >= 10 'idle_in_transaction_session_timeout', // Postgres allows additional session variables to be configured in the connection string in the `options` param. // see [https://www.postgresql.org/docs/14/libpq-connect.html#LIBPQ-CONNECT-OPTIONS] 'options' ])); } const connection = await new Promise((resolve, reject) => { let responded = false; const connection = new this.lib.Client(connectionConfig); const parameterHandler = message => { switch (message.parameterName) { case 'server_version': if (this.sequelize.options.databaseVersion === 0) { const version = semver.coerce(message.parameterValue).version; this.sequelize.options.databaseVersion = semver.valid(version) ? version : this.dialect.defaultVersion; } break; case 'standard_conforming_strings': connection['standard_conforming_strings'] = message.parameterValue; break; } }; const endHandler = () => { debug('connection timeout'); if (!responded) { reject(new sequelizeErrors.ConnectionTimedOutError(new Error('Connection timed out'))); } }; // If we didn't ever hear from the client.connect() callback the connection timeout // node-postgres does not treat this as an error since no active query was ever emitted connection.once('end', endHandler); if (!this.sequelize.config.native) { // Receive various server parameters for further configuration connection.connection.on('parameterStatus', parameterHandler); } connection.connect(err => { responded = true; if (!this.sequelize.config.native) { // remove parameter handler connection.connection.removeListener('parameterStatus', parameterHandler); } if (err) { if (err.code) { switch (err.code) { case 'ECONNREFUSED': reject(new sequelizeErrors.ConnectionRefusedError(err)); break; case 'ENOTFOUND': reject(new sequelizeErrors.HostNotFoundError(err)); break; case 'EHOSTUNREACH': reject(new sequelizeErrors.HostNotReachableError(err)); break; case 'EINVAL': reject(new sequelizeErrors.InvalidConnectionError(err)); break; default: reject(new sequelizeErrors.ConnectionError(err)); break; } } else { reject(new sequelizeErrors.ConnectionError(err)); } } else { debug('connection acquired'); connection.removeListener('end', endHandler); resolve(connection); } }); }); let query = ''; if (this.sequelize.options.standardConformingStrings !== false && connection['standard_conforming_strings'] !== 'on') { // Disable escape characters in strings // see https://github.com/sequelize/sequelize/issues/3545 (security issue) // see https://www.postgresql.org/docs/current/static/runtime-config-compatible.html#GUC-STANDARD-CONFORMING-STRINGS query += 'SET standard_conforming_strings=on;'; } if (this.sequelize.options.clientMinMessages !== undefined) { console.warn('Usage of "options.clientMinMessages" is deprecated and will be removed in v7.'); console.warn('Please use the sequelize option "dialectOptions.clientMinMessages" instead.'); } // Redshift dosen't support client_min_messages, use 'ignore' to skip this settings. // If no option, the default value in sequelize is 'warning' if ( !( config.dialectOptions && config.dialectOptions.clientMinMessages && config.dialectOptions.clientMinMessages.toLowerCase() === 'ignore' || this.sequelize.options.clientMinMessages === false ) ) { const clientMinMessages = config.dialectOptions && config.dialectOptions.clientMinMessages || this.sequelize.options.clientMinMessages || 'warning'; query += `SET client_min_messages TO ${clientMinMessages};`; } if (!this.sequelize.config.keepDefaultTimezone) { const isZone = !!momentTz.tz.zone(this.sequelize.options.timezone); if (isZone) { query += `SET TIME ZONE '${this.sequelize.options.timezone}';`; } else { query += `SET TIME ZONE INTERVAL '${this.sequelize.options.timezone}' HOUR TO MINUTE;`; } } if (query) { await connection.query(query); } if (Object.keys(this.nameOidMap).length === 0 && this.enumOids.oids.length === 0 && this.enumOids.arrayOids.length === 0) { await this._refreshDynamicOIDs(connection); } // Don't let a Postgres restart (or error) to take down the whole app connection.on('error', error => { connection._invalid = true; debug(`connection error ${error.code || error.message}`); this.pool.destroy(connection); }); return connection; } async disconnect(connection) { if (connection._ending) { debug('connection tried to disconnect but was already at ENDING state'); return; } return await promisify(callback => connection.end(callback))(); } validate(connection) { return !connection._invalid && !connection._ending; } async _refreshDynamicOIDs(connection) { const databaseVersion = this.sequelize.options.databaseVersion; const supportedVersion = '8.3.0'; // Check for supported version if ( (databaseVersion && semver.gte(databaseVersion, supportedVersion)) === false) { return; } const results = await (connection || this.sequelize).query( 'WITH ranges AS (' + ' SELECT pg_range.rngtypid, pg_type.typname AS rngtypname,' + ' pg_type.typarray AS rngtyparray, pg_range.rngsubtype' + ' FROM pg_range LEFT OUTER JOIN pg_type ON pg_type.oid = pg_range.rngtypid' + ')' + 'SELECT pg_type.typname, pg_type.typtype, pg_type.oid, pg_type.typarray,' + ' ranges.rngtypname, ranges.rngtypid, ranges.rngtyparray' + ' FROM pg_type LEFT OUTER JOIN ranges ON pg_type.oid = ranges.rngsubtype' + ' WHERE (pg_type.typtype IN(\'b\', \'e\'));' ); let result = Array.isArray(results) ? results.pop() : results; // When searchPath is prepended then two statements are executed and the result is // an array of those two statements. First one is the SET search_path and second is // the SELECT query result. if (Array.isArray(result)) { if (result[0].command === 'SET') { result = result.pop(); } } const newNameOidMap = {}; const newEnumOids = { oids: [], arrayOids: [] }; for (const row of result.rows) { // Mapping enums, handled separatedly if (row.typtype === 'e') { newEnumOids.oids.push(row.oid); if (row.typarray) newEnumOids.arrayOids.push(row.typarray); continue; } // Mapping base types and their arrays newNameOidMap[row.typname] = { oid: row.oid }; if (row.typarray) newNameOidMap[row.typname].arrayOid = row.typarray; // Mapping ranges(of base types) and their arrays if (row.rngtypid) { newNameOidMap[row.typname].rangeOid = row.rngtypid; if (row.rngtyparray) newNameOidMap[row.typname].arrayRangeOid = row.rngtyparray; } } // Replace all OID mappings. Avoids temporary empty OID mappings. this.nameOidMap = newNameOidMap; this.enumOids = newEnumOids; this.refreshTypeParser(dataTypes.postgres); } _clearDynamicOIDs() { this.nameOidMap = {}; this.enumOids = { oids: [], arrayOids: [] }; } } module.exports = ConnectionManager; module.exports.ConnectionManager = ConnectionManager; module.exports.default = ConnectionManager;