UNPKG

pg-promise

Version:
649 lines (610 loc) 25.2 kB
'use strict'; var $npm = { result: require('./result'), special: require('./special'), context: require('./cnContext'), events: require('./events'), utils: require('./utils'), connect: require('./connect'), query: require('./query'), task: require('./task') }; /** * @constructor Database * @description * Represents an extensible database protocol. This type is not available directly, * it can only be created via the library's base call. * * @param {String|Object} cn * Connection object or string. * * @param {} [dc] * Database Context. * * Any object or value to be propagated through the entire protocol, to allow implementations * and event handling that depend on the database context. * * This is mainly to facilitate the use of multiple databases which may need separate protocol * extensions, or different implementations within a single task / transaction callback, * depending on the database context. * * @see {@link event:extend extend} * * @example * // Loading and initializing the library: * var pgp = require('pg-promise')({ * // Initialization Options * }); * * var cn = "postgres://username:password@host:port/database"; * * // Creating a new database instance from the connection details: * var db = pgp(cn); * */ function Database(cn, dc, options, config) { var $p = config.promise; /** * @method Database.connect * @deprecated * Method {@link Database.task task} offers a safer (automatic) way of opening and * releasing the connection. * * @summary Retrieves a new or existing connection from the pool, based on the current * connection parameters. * * @description * This method initiates a shared connection for executing a chain of queries on the * same connection. The connection must be released in the end of the chain by calling * method `done()` on the connection object. * * This is a legacy, low-level approach to chaining queries on the same connection. * A newer and safer approach is via methods {@link Database.task task}, which allocates * and releases the shared connection automatically. * * @returns {external:Promise} * Connection result: * - resolves with the connection object, if successful. The object has method `done()` that must * be called in the end of the query chain, in order to release the connection back to the pool. * - rejects with the connection error when fails. * * @see {@link Database.task} */ this.connect = function () { var ctx = createContext(); var self = { // Generic query method; query: function (query, values, qrm) { if (!ctx.db) { throw new Error("Cannot execute a query on a disconnected client."); } return config.npm.query.call(this, ctx, query, values, qrm); }, // Connection release method; done: function () { if (!ctx.db) { throw new Error("Cannot invoke done() on a disconnected client."); } ctx.disconnect(); } }; extend(ctx, self); // extending the protocol; return config.npm.connect(ctx) .then(function (db) { ctx.connect(db); return self; }); }; /** * @method Database.query * * @description * Executes a generic query request that expects return data * according to parameter `qrm`. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @param {queryResult} [qrm=queryResult.any] * {@link queryResult Query Result Mask} * * @returns {external:Promise} * A promise object that represents the query result. * * When the query result is an array, it is extended with a hidden * property `duration` - query duration in milliseconds. */ this.query = function (query, values, qrm) { var self = this, ctx = createContext(); return config.npm.connect(ctx) .then(function (db) { ctx.connect(db); return config.npm.query.call(self, ctx, query, values, qrm); }) .then(function (data) { ctx.disconnect(); return data; }) .catch(function (error) { ctx.disconnect(); return $p.reject(error); }); }; extend(createContext(), this); // extending root protocol; function createContext() { return new $npm.context(cn, dc, options); } //////////////////////////////////////////////////// // Injects additional methods into an access object, // extending the protocol's base method 'query'. function extend(ctx, obj) { /** * @method Database.none * @description * Executes a query that expects no data to be returned. * If the query returns any kind of data, the method rejects. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @returns {external:Promise} Result of the query call: * - when no records are returned, it resolves with `undefined` * - when any data is returned, it rejects with {@link QueryResultError} * = `No return data was expected.` */ obj.none = function (query, values) { return obj.query.call(this, query, values, $npm.result.none); }; /** * @method Database.one * @description * Executes a query that expects exactly one row of data. * When 0 or more than 1 rows are returned, the method rejects. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @returns {external:Promise} * Result of the query call: * - when 1 row is returned, it resolves with that row as a single object; * - when no rows are returned, it rejects with {@link QueryResultError} * = `No data returned from the query.` * - when multiple rows are returned, it rejects with {@link QueryResultError} * = `Multiple rows were not expected.` */ obj.one = function (query, values) { return obj.query.call(this, query, values, $npm.result.one); }; /** * @method Database.many * @description * Executes a query that expects one or more rows. * When the query returns no data, the method rejects. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @returns {external:Promise} Result of the query call: * - when 1 or more rows are returned, it resolves with the array of rows * - when no rows are returned, it rejects with {@link QueryResultError} * = `No data returned from the query.` */ obj.many = function (query, values) { return obj.query.call(this, query, values, $npm.result.many); }; /** * @method Database.oneOrNone * @description * Executes a query that expects 0 or 1 rows. * When the query returns more than 1 row, the method rejects. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @returns {external:Promise} Result of the query call: * - when no rows are returned, it resolves with `null` * - when 1 row is returned, it resolves with that row as a single object * - when multiple rows are returned, it rejects with {@link QueryResultError} * = `Multiple rows were not expected.` */ obj.oneOrNone = function (query, values) { return obj.query.call(this, query, values, $npm.result.one | $npm.result.none); }; /** * @method Database.manyOrNone * @description * Executes a query that expects any number of rows. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @returns {external:Promise} Result of the query call: * - when no rows are returned, it resolves with an empty array * - when 1 or more rows are returned, it resolves with the array of rows. * @see {@link Database.any} */ obj.manyOrNone = function (query, values) { return obj.query.call(this, query, values, $npm.result.many | $npm.result.none); }; /** * @method Database.any * @description * Executes a query that expects any number of rows. * * This is simply a shorter alias for method {@link Database.manyOrNone manyOrNone}. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @returns {external:Promise} The same as method {@link Database.manyOrNone manyOrNone} * @see {@link Database.manyOrNone} */ obj.any = function (query, values) { return obj.query.call(this, query, values, $npm.result.any); }; /** * @method Database.result * @description * Executes a query without any expectation for the return data, * to resolve with the original $[Result] object when successful. * * @param {String|Object} query * - query string * - prepared statement: `{name, text, values}` * - {@link PreparedStatement} object * - {@link QueryFile} object * * @param {Array|value} [values] * Formatting parameters for the query string: * - a single value - for variable `$1` * - an array of values - for variables `$1`, `$2`, etc. * - an object - for $[Named Parameters] * * @returns {external:Promise} * Result of the query call: * - resolves with the original $[Result] object, extended with * property `duration` - query duration in milliseconds. */ obj.result = function (query, values) { return obj.query.call(this, query, values, $npm.special.cache.resultQuery); }; /** * @method Database.stream * @description * Custom data streaming, with the help of $[pg-query-stream]. * * This method doesn't work with the $[Native Bindings], and if option `pgNative` * is set, it will reject with `Streaming doesn't work with native bindings.` * * @param {QueryStream} qs * Stream object of type $[QueryStream]. * * @param {streamInitCB} initCB * Stream initialization callback. * * It is invoked with the same `this` context as the calling method. * * @returns {external:Promise} Result of the streaming operation. * * Once the streaming has finished successfully, the method resolves with * `{processed, duration}`: * - `processed` - total number of rows processed; * - `duration` - streaming duration, in milliseconds. * * Possible rejections messages: * - `Invalid or missing stream object.` * - `Invalid stream state.` * - `Invalid or missing stream initialization callback.` */ obj.stream = function (qs, init) { return obj.query.call(this, qs, init, $npm.special.cache.streamQuery); }; /** * @method Database.func * @description * Executes a query against a database function by its name: `SELECT * FROM funcName(values)` * * It is the same as calling method {@link Database.query query} in any of the following ways: * - `query('SELECT * FROM funcName($1^)', pgp.as.csv(values), qrm)` * - `query('SELECT * FROM funcName($1:csv)', [values], qrm)` * - `query('SELECT * FROM funcName(${params:csv})', {params:values}, qrm)` * - `query('SELECT * FROM funcName(${params^})', {params:pgp.as.csv(values)}, qrm)` * * @param {String} funcName * Name of the function to be executed. * * @param {Array|value} [values] * Parameters for the function - one value or an array of values. * * @param {queryResult} [qrm=queryResult.any] - {@link queryResult Query Result Mask}. * * @returns {external:Promise} * Result of the query call, according to parameter `qrm`. * * @see {@link Database.query} * @see {@link Database.proc} */ obj.func = function (funcName, values, qrm) { return obj.query.call(this, { funcName: funcName }, values, qrm); }; /** * @method Database.proc * @description * Executes a query against a stored procedure via its name: `select * from procName(values)`, * expecting back 0 or 1 rows. * * The method simply forwards into `func(funcName, values, queryResult.one|queryResult.none)`. * * @param {String} procName * Name of the stored procedure to be executed. * * @param {Array|value} [values] * Parameters for the procedure - one value or an array of values. * * @returns {external:Promise} * The same result as method {@link Database.oneOrNone oneOrNone}. * * @see {@link Database.oneOrNone} * @see {@link Database.func} */ obj.proc = function (procName, values) { return obj.func.call(this, procName, values, $npm.result.one | $npm.result.none); }; /** * @method Database.task * @description * Executes a callback function (or $[ES6 generator]) with an automatically managed connection. * * This method should be used whenever executing more than one query at once, so the allocated connection * is reused between all queries, and released only after the task has finished. * * The callback function is called with one parameter - database protocol (same as `this`), extended with methods * {@link Task.batch batch}, {@link Task.page page}, {@link Task.sequence sequence}, plus property {@link Task.ctx ctx} - * the task context object. See {@link Task} for more details. * * @param {} tag/cb * When the method takes only one parameter, it must be the callback function (or $[ES6 generator]) for the task. * However, when calling the method with 2 parameters, the first one is always the `tag` - traceable context for the * task (see $[tags]). * * @param {Function|generator} [cb] * Task callback function (or $[ES6 generator]), if it is not `undefined`, or else the callback is expected to * be passed in as the first parameter. * * @returns {external:Promise} * Result from the callback function. * * @see {@link Task} * * @example * * // creating a task with a string for the tag: * db.task('my-task-name', t=> { * // t.ctx = task context object * * // task body, usually returning something * }) * .then(data=> { * // success * // data = as returned from the task's callback * }) * .catch(error=> { * // error * }); * */ obj.task = function (p1, p2) { return taskProcessor.call(this, p1, p2, false); }; /** * @method Database.tx * @description * Executes a callback function (or $[ES6 generator]) as a transaction. * * The transaction will execute `ROLLBACK` in the end, if the callback function returns a rejected * promise or throws an error; and it will execute `COMMIT` in all other cases. * * The callback function is called with one parameter - database protocol (same as `this`), extended with methods * {@link Task.batch batch}, {@link Task.page page}, {@link Task.sequence sequence}, plus property {@link Task.ctx ctx} - * the transaction context object. See {@link Task} for more details. * * @param {} tag/cb * When the method takes only one parameter, it must be the callback function (or $[ES6 generator]) for the transaction. * However, when calling the method with 2 parameters, the first one is always the `tag` - traceable context for the * transaction (see $[tags]). * * @param {Function|generator} [cb] * Transaction callback function (or $[ES6 generator]), if it is not `undefined`, or else the callback is expected to be * passed in as the first parameter. * * @returns {external:Promise} * Result from the callback function. * * @see {@link Task} * * @example * * // creating a transaction with a string for the tag: * db.tx('my-transaction-name', t=> { * // t.ctx = transaction context object * * // transaction body, usually returning something * }) * .then(data=> { * // success * // data = as returned from the transaction's callback * }) * .catch(error=> { * // error * }); * */ obj.tx = function (p1, p2) { return taskProcessor.call(this, p1, p2, true); }; // Task method; // Resolves with result from the callback function; function taskProcessor(p1, p2, isTX) { var tag, // tag object/value; taskCtx = ctx.clone(); // task context object; if (isTX) { taskCtx.txLevel = taskCtx.txLevel >= 0 ? (taskCtx.txLevel + 1) : 0; } if (this !== obj) { taskCtx.context = this; // calling context object; } taskCtx.cb = p1; // callback function; // allow inserting a tag in front of the callback // function, for better code readability; if (p2 !== undefined) { tag = p1; // overriding any default tag; taskCtx.cb = p2; } var cb = taskCtx.cb; if (typeof cb !== 'function') { return $p.reject("Callback function is required for the " + (isTX ? "transaction." : "task.")); } if (tag === undefined) { if (cb.tag !== undefined) { // use the default tag associated with the task: tag = cb.tag; } else { if (cb.name) { tag = cb.name; // use the function name as tag; } } } var tsk = new config.npm.task(taskCtx, tag, isTX, config); extend(taskCtx, tsk); if (taskCtx.db) { // reuse existing connection; return config.npm.task.exec(taskCtx, tsk, isTX, config); } // connection required; return config.npm.connect(taskCtx) .then(function (db) { taskCtx.connect(db); return config.npm.task.exec(taskCtx, tsk, isTX, config); }) .then(function (data) { taskCtx.disconnect(); return data; }) .catch(function (error) { taskCtx.disconnect(); return $p.reject(error); }); } // lock all default properties to read-only, // to prevent override by the client. $npm.utils.lock(obj, false, ctx.options); // extend the protocol; $npm.events.extend(ctx.options, obj, ctx.dc); // freeze the protocol permanently; $npm.utils.lock(obj, true, ctx.options); } } module.exports = function (config) { var npm = config.npm; npm.connect = npm.connect || $npm.connect(config); npm.query = npm.query || $npm.query(config); npm.task = npm.task || $npm.task(config); return Database; }; /** * @callback streamInitCB * @description * Stream initialization callback, used by {@link Database.stream}. * * @param {external:Stream} stream * Stream object to initialize streaming. * * @example * var QueryStream = require('pg-query-stream'); * var JSONStream = require('JSONStream'); * * // you can also use pgp.as.format(query, values, options) * // to format queries properly, via pg-promise; * var qs = new QueryStream('select * from users'); * * db.stream(qs, function (stream) { * // initiate streaming into the console: * stream.pipe(JSONStream.stringify()).pipe(process.stdout); * }) * .then(function (data) { * console.log("Total rows processed:", data.processed, * "Duration in milliseconds:", data.duration); * }) * .catch(function (error) { * // error; * }); */ /** * @external Stream * @see https://nodejs.org/api/stream.html */