UNPKG

pg-promise

Version:
273 lines (245 loc) 9.52 kB
'use strict'; var $npm = { spex: require('spex'), utils: require('./utils'), mode: require('./txMode'), events: require('./events'), query: require('./query'), async: require('./async') }; /** * @constructor Task * @extends Database * @description * Extends {@link Database} for an automatic connection session, with methods for executing multiple database queries. * The type isn't available directly, it can only be created via methods {@link Database.task} and {@link Database.tx}. * * When executing more than one request at a time, one should allocate and release the connection only once, * while executing all the required queries within the same connection session. More importantly, a transaction * can only work within a single connection. * * This class provides an interface for tasks/transactions to implement a connection session, during which you can * execute multiple queries against the same connection that's released automatically when the task/transaction is finished. * * @example * db.task(function (t) { * // this = t = task protocol context; * // this.ctx = task config + state context; * return t.one("select * from users where id=$1", 123) * .then(function (user) { * return t.any("select * from events where login=$1", user.name); * }); * }) * .then(function (events) { * // success; * }) * .catch(function (error) { * // error; * }); * */ function Task(ctx, tag, isTX, config) { /** * @member {Object} Task.ctx * @summary Task Context Object * * @property {Object} context * If the operation was invoked with an object context - `task.call(obj,...)` or * `tx.call(obj,...)`, this property is set with the context object that was passed in. * * @property {} dc - Database Context that was used when creating the database object. * See {@link Database}. * * @property {Boolean} isTX * Indicates whether this task represents a transaction. * * @property {} tag * Tag value as it was passed into the task. See methods {@link Database.task task} and {@link Database.tx tx}. * * @see event {@link event:query query} */ this.ctx = ctx.ctx = {}; // task context object; $npm.utils.addReadProp(this.ctx, 'isTX', isTX); if ('context' in ctx) { $npm.utils.addReadProp(this.ctx, 'context', ctx.context); } $npm.utils.addReadProp(this.ctx, 'tag', tag); $npm.utils.addReadProp(this.ctx, 'dc', ctx.dc); // generic query method; this.query = function (query, values, qrm) { if (!ctx.db) { throw new Error("Unexpected call outside of " + (isTX ? "transaction." : "task.")); } return config.npm.query.call(this, ctx, query, values, qrm); }; /** * @method Task.batch * @description * **Alternative Syntax:** `batch(values, {cb})` ⇒ `Promise` * * Settles a predefined array of mixed values by redirecting to method $[spex.batch]. * * For complete method documentation see $[spex.batch]. * @param {Array} values * @param {Function} [cb] * @returns {external:Promise} */ this.batch = function (values, cb) { return config.npm.spex.batch.call(this, values, cb); }; /** * @method Task.page * @description * **Alternative Syntax:** `page(source, {dest, limit})` ⇒ `Promise` * * Resolves a dynamic sequence of arrays/pages with mixed values, by redirecting to method $[spex.page]. * * For complete method documentation see $[spex.page]. * @param {Function} source * @param {Function} [dest] * @param {Number} [limit=0] * @returns {external:Promise} */ this.page = function (source, dest, limit) { return config.npm.spex.page.call(this, source, dest, limit); }; /** * @method Task.sequence * @description * **Alternative Syntax:** `sequence(source, {dest, limit, track})` ⇒ `Promise` * * Resolves a dynamic sequence of mixed values by redirecting to method $[spex.sequence]. * * For complete method documentation see $[spex.sequence]. * @param {Function} source * @param {Function} [dest] * @param {Number} [limit=0] * @param {Boolean} [track=false] * @returns {external:Promise} */ this.sequence = function (source, dest, limit, track) { return config.npm.spex.sequence.call(this, source, dest, limit, track); }; } ////////////////////////// // Executes a task; Task.exec = function (ctx, obj, isTX, config) { var $p = config.promise; // callback invocation helper; function callback() { var result, cb = ctx.cb; if (cb.constructor.name === 'GeneratorFunction') { cb = config.npm.async(cb); } try { result = cb.call(obj, obj); // invoking the callback function; } catch (err) { $npm.events.error(ctx.options, err, { client: ctx.db.client, dc: ctx.dc, ctx: ctx.ctx }); return $p.reject(err); // reject with the error; } if (result && result.then instanceof Function) { return result; // result is a valid promise object; } return $p.resolve(result); } // updates the task context and notifies the client; function update(start, success, result) { var c = ctx.ctx; if (start) { $npm.utils.addReadProp(c, 'start', new Date()); } else { c.finish = new Date(); c.success = success; c.result = result; $npm.utils.lock(c, true); } (isTX ? $npm.events.transact : $npm.events.task)(ctx.options, { client: ctx.db.client, dc: ctx.dc, ctx: c }); } var cbData, cbReason, success, spName, // Save-Point Name; capSQL = ctx.options && ctx.options.capSQL; // capitalize sql; update(true); if (isTX) { // executing a transaction; spName = "level_" + ctx.txLevel; return begin() .then(function () { return callback() .then(function (data) { cbData = data; // save callback data; success = true; return commit(); }, function (reason) { cbReason = reason; // save callback failure reason; return rollback(); }) .then(function () { if (success) { update(false, true, cbData); return cbData; } else { update(false, false, cbReason); return $p.reject(cbReason); } }, // istanbul ignore next: either `commit` or `rollback` has failed, which is // impossible to replicate in a test environment, so skipping from the test; function (reason) { update(false, false, reason); return $p.reject(reason); }); }, // istanbul ignore next: `begin` has failed, which is impossible // to replicate in a test environment, so skipping from the test; function (reason) { update(false, false, reason); return $p.reject(reason); }); } function begin() { if (!ctx.txLevel && ctx.cb.txMode instanceof $npm.mode.TransactionMode) { return exec(ctx.cb.txMode.begin(capSQL), 'savepoint'); } return exec('begin', 'savepoint'); } function commit() { return exec('commit', 'release savepoint'); } function rollback() { return exec('rollback', 'rollback to savepoint'); } function exec(top, nested) { if (ctx.txLevel) { return obj.none((capSQL ? nested.toUpperCase() : nested) + ' ' + spName); } return obj.none(capSQL ? top.toUpperCase() : top); } // executing a task; return callback() .then(function (data) { update(false, true, data); return data; }) .catch(function (error) { update(false, false, error); return $p.reject(error); }); }; module.exports = function (config) { var npm = config.npm; // istanbul ignore next: // we keep 'npm.query' initialization here, even though it is always // pre-initialized by the 'database' module, for integrity purpose. npm.query = npm.query || $npm.query(config); npm.async = npm.async || $npm.async(config); npm.spex = npm.spex || $npm.spex(config.promiseLib); return Task; };