123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- /*
- * Copyright (c) 2015-present, Vitaly Tomilov
- *
- * See the LICENSE file at the top-level directory of this distribution
- * for licensing information.
- *
- * Removal or modification of this copyright notice is prohibited.
- */
- const {Events} = require('./events');
- const npm = {
- spex: require('spex'),
- utils: require('./utils'),
- mode: require('./tx-mode'),
- query: require('./query'),
- text: require('./text')
- };
- /**
- * @interface Task
- * @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 Database.task}, {@link Database#tx Database.tx},
- * or their derivations.
- *
- * 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 is 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.
- *
- * Each task/transaction manages the connection automatically. When executed on the root {@link Database} object, the connection
- * is allocated from the pool, and once the method's callback has finished, the connection is released back to the pool.
- * However, when invoked inside another task or transaction, the method reuses the parent connection.
- *
- * @see
- * {@link Task#ctx ctx},
- * {@link Task#batch batch},
- * {@link Task#sequence sequence},
- * {@link Task#page page}
- *
- * @example
- * db.task(t => {
- * // t = task protocol context;
- * // t.ctx = Task Context;
- * return t.one('select * from users where id=$1', 123)
- * .then(user => {
- * return t.any('select * from events where login=$1', user.name);
- * });
- * })
- * .then(events => {
- * // success;
- * })
- * .catch(error => {
- * // error;
- * });
- *
- */
- function Task(ctx, tag, isTX, config) {
- const $p = config.promise;
- /**
- * @member {TaskContext} Task#ctx
- * @readonly
- * @description
- * Task/Transaction Context object - contains individual properties for each task/transaction.
- *
- * @see event {@link event:query query}
- *
- * @example
- *
- * db.task(t => {
- * return t.ctx; // task context object
- * })
- * .then(ctx => {
- * console.log('Task Duration:', ctx.duration);
- * });
- *
- * @example
- *
- * db.tx(t => {
- * return t.ctx; // transaction context object
- * })
- * .then(ctx => {
- * console.log('Transaction Duration:', ctx.duration);
- * });
- */
- 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, 'connected', !ctx.db);
- npm.utils.addReadProp(this.ctx, 'tag', tag);
- npm.utils.addReadProp(this.ctx, 'dc', ctx.dc);
- npm.utils.addReadProp(this.ctx, 'level', ctx.level);
- npm.utils.addReadProp(this.ctx, 'inTransaction', ctx.inTransaction);
- if (isTX) {
- npm.utils.addReadProp(this.ctx, 'txLevel', ctx.txLevel);
- }
- npm.utils.addReadProp(this.ctx, 'parent', ctx.parentCtx);
- // generic query method;
- this.query = function (query, values, qrm) {
- if (!ctx.db) {
- return $p.reject(new Error(npm.text.looseQuery));
- }
- return config.$npm.query.call(this, ctx, query, values, qrm);
- };
- /**
- * @deprecated
- * Consider using <b>async/await</b> syntax instead, or if you must have
- * pre-generated promises, then $[Promise.allSettled].
- *
- * @method Task#batch
- * @description
- * Settles a predefined array of mixed values by redirecting to method $[spex.batch].
- *
- * For complete method documentation see $[spex.batch].
- *
- * @param {array} values
- * @param {Object} [options]
- * Optional Parameters.
- * @param {function} [options.cb]
- *
- * @returns {external:Promise}
- */
- this.batch = function (values, options) {
- return config.$npm.spex.batch.call(this, values, options);
- };
- /**
- * @method Task#page
- * @description
- * 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 {Object} [options]
- * Optional Parameters.
- * @param {function} [options.dest]
- * @param {number} [options.limit=0]
- *
- * @returns {external:Promise}
- */
- this.page = function (source, options) {
- return config.$npm.spex.page.call(this, source, options);
- };
- /**
- * @method Task#sequence
- * @description
- * Resolves a dynamic sequence of mixed values by redirecting to method $[spex.sequence].
- *
- * For complete method documentation see $[spex.sequence].
- *
- * @param {function} source
- * @param {Object} [options]
- * Optional Parameters.
- * @param {function} [options.dest]
- * @param {number} [options.limit=0]
- * @param {boolean} [options.track=false]
- *
- * @returns {external:Promise}
- */
- this.sequence = function (source, options) {
- return config.$npm.spex.sequence.call(this, source, options);
- };
- }
- /**
- * @private
- * @method Task.callback
- * Callback invocation helper.
- *
- * @param ctx
- * @param obj
- * @param cb
- * @param config
- * @returns {Promise.<TResult>}
- */
- const callback = (ctx, obj, cb, config) => {
- const $p = config.promise;
- let result;
- try {
- if (cb.constructor.name === 'GeneratorFunction') {
- // v9.0 dropped all support for ES6 generator functions;
- // Clients should use the new ES7 async/await syntax.
- throw new TypeError('ES6 generator functions are no longer supported!');
- }
- result = cb.call(obj, obj); // invoking the callback function;
- } catch (err) {
- Events.error(ctx.options, err, {
- client: ctx.db && ctx.db.client, // the error can be due to loss of connectivity
- dc: ctx.dc,
- ctx: ctx.ctx
- });
- return $p.reject(err); // reject with the error;
- }
- if (result && typeof result.then === 'function') {
- return result; // result is a valid promise object;
- }
- return $p.resolve(result);
- };
- /**
- * @private
- * @method Task.execute
- * Executes a task.
- *
- * @param ctx
- * @param obj
- * @param isTX
- * @param config
- * @returns {Promise.<TResult>}
- */
- const execute = (ctx, obj, isTX, config) => {
- const $p = config.promise;
- // updates the task context and notifies the client;
- function update(start, success, result) {
- const c = ctx.ctx;
- if (start) {
- npm.utils.addReadProp(c, 'start', new Date());
- } else {
- c.finish = new Date();
- c.success = success;
- c.result = result;
- c.duration = c.finish - c.start;
- }
- (isTX ? Events.transact : Events.task)(ctx.options, {
- client: ctx.db && ctx.db.client, // loss of connectivity is possible at this point
- dc: ctx.dc,
- ctx: c
- });
- }
- let cbData, cbReason, success,
- spName; // Save-Point Name;
- const capSQL = ctx.options.capSQL; // capitalize sql;
- update(true);
- if (isTX) {
- // executing a transaction;
- spName = `sp_${ctx.txLevel}_${ctx.nextTxCount}`;
- return begin()
- .then(() => callback(ctx, obj, ctx.cb, config)
- .then(data => {
- cbData = data; // save callback data;
- success = true;
- return commit();
- }, err => {
- cbReason = err; // save callback failure reason;
- return rollback();
- })
- .then(() => {
- if (success) {
- update(false, true, cbData);
- return cbData;
- }
- update(false, false, cbReason);
- return $p.reject(cbReason);
- },
- err => {
- // either COMMIT or ROLLBACK has failed, which is impossible
- // to replicate in a test environment, so skipping from the test;
- // istanbul ignore next:
- update(false, false, err);
- // istanbul ignore next:
- return $p.reject(err);
- }),
- err => {
- // BEGIN has failed, which is impossible to replicate in a test
- // environment, so skipping the whole block from the test;
- // istanbul ignore next:
- update(false, false, err);
- // istanbul ignore next:
- return $p.reject(err);
- });
- }
- function begin() {
- if (!ctx.txLevel && ctx.mode instanceof npm.mode.TransactionMode) {
- return exec(ctx.mode.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(ctx, obj, ctx.cb, config)
- .then(data => {
- update(false, true, data);
- return data;
- })
- .catch(error => {
- update(false, false, error);
- return $p.reject(error);
- });
- };
- module.exports = config => {
- const npmLocal = 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.
- npmLocal.query = npmLocal.query || npm.query(config);
- npmLocal.spex = npmLocal.spex || npm.spex(config.promiseLib);
- return {
- Task, execute, callback
- };
- };
- /**
- * @typedef TaskContext
- * @description
- * Task/Transaction Context used via property {@link Task#ctx ctx} inside tasks (methods {@link Database#task Database.task} and {@link Database#taskIf Database.taskIf})
- * and transactions (methods {@link Database#tx Database.tx} and {@link Database#txIf Database.txIf}).
- *
- * Properties `context`, `connected`, `parent`, `level`, `dc`, `isTX`, `tag`, `start`, `useCount` and `serverVersion` are set just before the operation has started,
- * while properties `finish`, `duration`, `success` and `result` are set immediately after the operation has finished.
- *
- * @property {*} context
- * If the operation was invoked with a calling context - `task.call(context,...)` or `tx.call(context,...)`,
- * this property is set with the context that was passed in. Otherwise, the property doesn't exist.
- *
- * @property {*} dc
- * _Database Context_ that was passed into the {@link Database} object during construction.
- *
- * @property {boolean} isTX
- * Indicates whether this operation is a transaction (as opposed to a regular task).
- *
- * @property {number} duration
- * Number of milliseconds consumed by the operation.
- *
- * Set after the operation has finished, it is simply a shortcut for `finish - start`.
- *
- * @property {number} level
- * Task nesting level, starting from 0, counting both regular tasks and transactions.
- *
- * @property {number} txLevel
- * Transaction nesting level, starting from 0. Transactions on level 0 use `BEGIN/COMMIT/ROLLBACK`,
- * while transactions on nested levels use the corresponding `SAVEPOINT` commands.
- *
- * This property exists only within the context of a transaction (`isTX = true`).
- *
- * @property {boolean} inTransaction
- * Available in both tasks and transactions, it simplifies checking when there is a transaction
- * going on either on this level or above.
- *
- * For example, when you want to check for a containing transaction while inside a task, and
- * only start a transaction when there is none yet.
- *
- * @property {TaskContext} parent
- * Parent task/transaction context, or `null` when it is top-level.
- *
- * @property {boolean} connected
- * Indicates when the task/transaction acquired the connection on its own (`connected = true`), and will release it once
- * the operation has finished. When the value is `false`, the operation is reusing an existing connection.
- *
- * @property {*} tag
- * Tag value as it was passed into the task. See methods {@link Database#task task} and {@link Database#tx tx}.
- *
- * @property {Date} start
- * Date/Time of when this operation started the execution.
- *
- * @property {number} useCount
- * Number of times the connection has been previously used, starting with 0 for a freshly
- * allocated physical connection.
- *
- * @property {string} serverVersion
- * Version of the PostgreSQL server to which we are connected.
- * Not available with $[Native Bindings].
- *
- * @property {Date} finish
- * Once the operation has finished, this property is set to the Data/Time of when it happened.
- *
- * @property {boolean} success
- * Once the operation has finished, this property indicates whether it was successful.
- *
- * @property {*} result
- * Once the operation has finished, this property contains the result, depending on property `success`:
- * - data resolved by the operation, if `success = true`
- * - error / rejection reason, if `success = false`
- *
- */
|