machinepack-postgresql
Version:
Structured Node.js bindings for connecting and running queries against a PostgreSQL database.
292 lines (240 loc) • 12 kB
JavaScript
// Dependencies
var util = require('util');
var url = require('url');
var _ = require('@sailshq/lodash');
var pg = require('pg');
module.exports = {
friendlyName: 'Create manager',
description: 'Build and initialize a connection manager instance for this database.',
extendedDescription:
'The `manager` instance returned by this method contains any configuration that is necessary ' +
'for communicating with the database and establishing connections (e.g. host, user, password) ' +
'as well as any other relevant metadata. The manager will often also contain a reference ' +
'to some kind of native container (e.g. a connection pool).\n' +
'\n' +
'Note that a manager instance does not necessarily need to correspond with a pool though--' +
'it might simply be a container for storing config, or it might refer to multiple pools.',
sync: true,
inputs: {
connectionString: {
description: 'A connection string to use to connect to a Postgresql database.',
extendedDescription: 'Be sure to include credentials. You can also optionally provide the name of an existing database on your Postgresql server.',
example: 'postgres://mikermcneil:p4ssw02D@localhost:5432/some_db',
required: true
},
onUnexpectedFailure: {
description: 'A function to call any time an unexpected error event is received from this manager or any of its connections.',
extendedDescription:
'This can be used for anything you like, whether that\'s sending an email to devops, ' +
'or something as simple as logging a warning to the console.\n' +
'\n' +
'For example:\n' +
'```\n' +
'onUnexpectedFailure: function (err) {\n' +
' console.warn(\'Unexpected failure in database manager:\',err);\n' +
'}\n' +
'```',
example: '->'
},
meta: {
friendlyName: 'Meta (custom)',
description: 'Additional PostgreSQL-specific options to use when connecting.',
extendedDescription: 'If specified, should be a dictionary. If there is a conflict between something provided in the connection string, and something in `meta`, the connection string takes priority.',
moreInfoUrl: 'https://github.com/coopernurse/node-pool#documentation',
example: '==='
}
},
exits: {
success: {
description: 'The manager was successfully created.',
extendedDescription:
'The new manager should be passed in to `getConnection()`.' +
'Note that _no matter what_, this manager must be capable of ' +
'spawning an infinite number of connections (i.e. via `getConnection()`). ' +
'The implementation of how exactly it does this varies on a driver-by-driver ' +
'basis; and it may also vary based on the configuration passed into the `meta` input.',
outputVariableName: 'report',
outputDescription: 'The `manager` property is a manager instance that will be passed into `getConnection()`. The `meta` property is reserved for custom driver-specific extensions.',
outputExample: '==='
// example: {
// manager: '===',
// meta: '==='
// }
},
malformed: {
description: 'The provided connection string is not valid for Postgresql.',
outputVariableName: 'report',
outputDescription: 'The `error` property is a JavaScript Error instance explaining that (and preferably "why") the provided connection string is invalid. The `meta` property is reserved for custom driver-specific extensions.',
outputExample: '==='
// example: {
// error: '===',
// meta: '==='
// }
},
failed: {
description: 'Could not create a connection manager for this database using the specified connection string.',
extendedDescription:
'If this exit is called, it might mean any of the following:\n' +
' + the credentials encoded in the connection string are incorrect\n' +
' + there is no database server running at the provided host (i.e. even if it is just that the database process needs to be started)\n' +
' + there is no software "database" with the specified name running on the server\n' +
' + the provided connection string does not have necessary access rights for the specified software "database"\n' +
' + this Node.js process could not connect to the database, perhaps because of firewall/proxy settings\n' +
' + any other miscellaneous connection error\n' +
'\n' +
'Note that even if the database is unreachable, bad credentials are being used, etc, ' +
'this exit will not necessarily be called-- that depends on the implementation of the driver ' +
'and any special configuration passed to the `meta` input. e.g. if a pool is being used that spins up ' +
'multiple connections immediately when the manager is created, then this exit will be called if any of ' +
'those initial attempts fail. On the other hand, if the manager is designed to produce adhoc connections, ' +
'any errors related to bad credentials, connectivity, etc. will not be caught until `getConnection()` is called.',
outputVariableName: 'report',
outputDescription: 'The `error` property is a JavaScript Error instance with more information and a stack trace. The `meta` property is reserved for custom driver-specific extensions.',
outputExample: '==='
// example: {
// error: '===',
// meta: '==='
// }
}
},
fn: function createManager(inputs, exits) {
// Note:
// Support for different types of managers is database-specific, and is not
// built into the Waterline driver spec-- however this type of configurability
// can be instrumented using `meta`.
//
// In particular, support for ad-hoc connections (i.e. no pool) and clusters/multiple
// pools (see "pg-pool": https://github.com/brianc/node-pg-pool)
// could be implemented here, using properties on `meta` to determine whether or not
// to have this manager produce connections ad-hoc, from a pool, or from a cluster of pools.
//
// Feel free to fork this driver and customize as you see fit. Also note that
// contributions to the core driver in this area are welcome and greatly appreciated!
// Build a local variable (`_clientConfig`) to house a dictionary
// of additional Postgres options that will be passed into `.createPool()`
// (Note that these could also be used with `.connect()` or `.createPoolCluster()`)
//
// This is pulled from the `connectionString` and `meta` inputs, and used for
// configuring stuff like `host` and `password`.
//
// For a complete list of available options, see:
// • https://github.com/brianc/node-postgres/wiki/Client#parameters
// • https://github.com/coopernurse/node-pool#documentation
//
// However, note that supported options are explicitly whitelisted below.
var _clientConfig = {};
// Validate and parse `meta` (if specified).
if (!_.isUndefined(inputs.meta)) {
if (!_.isObject(inputs.meta)) {
return exits.error('If provided, `meta` must be a dictionary.');
}
// Use properties of `meta` directly as Postgres client config.
// (note that we're very careful to only stick a property on the client config
// if it was not undefined, just in case that matters)
var configOptions = [
// Postgres Client Options:
// ============================================
// Basic:
'host', 'port', 'database', 'user', 'password', 'ssl',
// Advanced Client Config:
'application_name', 'fallback_application_name',
// General Pool Config:
'max', 'min', 'refreshIdle', 'idleTimeoutMillis',
// Advanced Pool Config:
// These should only be used if you know what you are doing.
// https://github.com/coopernurse/node-pool#documentation
'name', 'create', 'destroy', 'reapIntervalMillis', 'returnToHead',
'priorityRange', 'validate', 'validateAsync', 'log'
];
_.each(configOptions, function addConfigValue(clientConfKeyName) {
if (!_.isUndefined(inputs.meta[clientConfKeyName])) {
_clientConfig[clientConfKeyName] = inputs.meta[clientConfKeyName];
}
});
// In the future, other special properties of `meta` could be used
// as options for the manager-- e.g. whether or not to use pooling,
// or the connection strings of replicas, etc.
}
// Validate & parse connection string, pulling out Postgres client config
// (call `malformed` if invalid).
//
// Remember: connection string takes priority over `meta` in the event of a conflict.
try {
var urlToParse = inputs.connectionString;
// We don't actually care about the protocol, but `url.parse()` returns funky results
// if the argument doesn't have one. So we'll add one if necessary.
// See https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax
if (!urlToParse.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//)) {
urlToParse = 'postgresql://' + urlToParse;
}
var parsedConnectionStr = url.parse(urlToParse);
// Parse port & host
var DEFAULT_HOST = 'localhost';
var DEFAULT_PORT = 5432;
if (parsedConnectionStr.port) {
_clientConfig.port = +parsedConnectionStr.port;
} else {
_clientConfig.port = DEFAULT_PORT;
}
if (parsedConnectionStr.hostname) {
_clientConfig.host = parsedConnectionStr.hostname;
} else {
_clientConfig.host = DEFAULT_HOST;
}
// Parse user & password
if (parsedConnectionStr.auth && _.isString(parsedConnectionStr.auth)) {
var authPieces = parsedConnectionStr.auth.split(/:/);
if (authPieces[0]) {
_clientConfig.user = authPieces[0];
}
if (authPieces[1]) {
_clientConfig.password = authPieces[1];
}
}
// Parse database name
if (_.isString(parsedConnectionStr.pathname)) {
var _databaseName = parsedConnectionStr.pathname;
// Trim leading and trailing slashes
_databaseName = _databaseName.replace(/^\/+/, '');
_databaseName = _databaseName.replace(/\/+$/, '');
// If anything is left, use it as the database name.
if (_databaseName) {
_clientConfig.database = _databaseName;
}
}
} catch (_e) {
_e.message = util.format('Provided value (`%s`) is not a valid Postgres connection string.', inputs.connectionString) + ' Error details: ' + _e.message;
return exits.malformed({
error: _e,
meta: inputs.meta
});
}
// Create a connection pool.
//
// More about using pools with node-pg:
// • https://github.com/brianc/node-pg-pool
var pool = new pg.Pool(_clientConfig);
// Bind an "error" handler in order to handle errors from connections in the pool,
// or from the pool itself. Otherwise, without any further protection, if any Postgres
// connections in the pool die, then the process would crash with an error.
//
// For more background, see:
// • https://github.com/brianc/node-pg-pool#error
pool.on('error', function error(err) {
// When/if something goes wrong in this pool, call the `onUnexpectedFailure` notifier
// (if one was provided)
if (!_.isUndefined(inputs.onUnexpectedFailure)) {
inputs.onUnexpectedFailure(err || new Error('One or more pooled connections to Postgres database were lost. Did the database server go offline?'));
}
});
// Finally, build and return the manager.
var mgr = {
pool: pool,
connectionString: inputs.connectionString
};
return exits.success({
manager: mgr,
meta: inputs.meta
});
}
};