task.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. /*
  2. * Copyright (c) 2015-present, Vitaly Tomilov
  3. *
  4. * See the LICENSE file at the top-level directory of this distribution
  5. * for licensing information.
  6. *
  7. * Removal or modification of this copyright notice is prohibited.
  8. */
  9. const {Events} = require('./events');
  10. const npm = {
  11. spex: require('spex'),
  12. utils: require('./utils'),
  13. mode: require('./tx-mode'),
  14. query: require('./query'),
  15. text: require('./text')
  16. };
  17. /**
  18. * @interface Task
  19. * @description
  20. * Extends {@link Database} for an automatic connection session, with methods for executing multiple database queries.
  21. *
  22. * The type isn't available directly, it can only be created via methods {@link Database#task Database.task}, {@link Database#tx Database.tx},
  23. * or their derivations.
  24. *
  25. * When executing more than one request at a time, one should allocate and release the connection only once,
  26. * while executing all the required queries within the same connection session. More importantly, a transaction
  27. * can only work within a single connection.
  28. *
  29. * This is an interface for tasks/transactions to implement a connection session, during which you can
  30. * execute multiple queries against the same connection that's released automatically when the task/transaction is finished.
  31. *
  32. * Each task/transaction manages the connection automatically. When executed on the root {@link Database} object, the connection
  33. * is allocated from the pool, and once the method's callback has finished, the connection is released back to the pool.
  34. * However, when invoked inside another task or transaction, the method reuses the parent connection.
  35. *
  36. * @see
  37. * {@link Task#ctx ctx},
  38. * {@link Task#batch batch},
  39. * {@link Task#sequence sequence},
  40. * {@link Task#page page}
  41. *
  42. * @example
  43. * db.task(t => {
  44. * // t = task protocol context;
  45. * // t.ctx = Task Context;
  46. * return t.one('select * from users where id=$1', 123)
  47. * .then(user => {
  48. * return t.any('select * from events where login=$1', user.name);
  49. * });
  50. * })
  51. * .then(events => {
  52. * // success;
  53. * })
  54. * .catch(error => {
  55. * // error;
  56. * });
  57. *
  58. */
  59. function Task(ctx, tag, isTX, config) {
  60. const $p = config.promise;
  61. /**
  62. * @member {TaskContext} Task#ctx
  63. * @readonly
  64. * @description
  65. * Task/Transaction Context object - contains individual properties for each task/transaction.
  66. *
  67. * @see event {@link event:query query}
  68. *
  69. * @example
  70. *
  71. * db.task(t => {
  72. * return t.ctx; // task context object
  73. * })
  74. * .then(ctx => {
  75. * console.log('Task Duration:', ctx.duration);
  76. * });
  77. *
  78. * @example
  79. *
  80. * db.tx(t => {
  81. * return t.ctx; // transaction context object
  82. * })
  83. * .then(ctx => {
  84. * console.log('Transaction Duration:', ctx.duration);
  85. * });
  86. */
  87. this.ctx = ctx.ctx = {}; // task context object;
  88. npm.utils.addReadProp(this.ctx, 'isTX', isTX);
  89. if ('context' in ctx) {
  90. npm.utils.addReadProp(this.ctx, 'context', ctx.context);
  91. }
  92. npm.utils.addReadProp(this.ctx, 'connected', !ctx.db);
  93. npm.utils.addReadProp(this.ctx, 'tag', tag);
  94. npm.utils.addReadProp(this.ctx, 'dc', ctx.dc);
  95. npm.utils.addReadProp(this.ctx, 'level', ctx.level);
  96. npm.utils.addReadProp(this.ctx, 'inTransaction', ctx.inTransaction);
  97. if (isTX) {
  98. npm.utils.addReadProp(this.ctx, 'txLevel', ctx.txLevel);
  99. }
  100. npm.utils.addReadProp(this.ctx, 'parent', ctx.parentCtx);
  101. // generic query method;
  102. this.query = function (query, values, qrm) {
  103. if (!ctx.db) {
  104. return $p.reject(new Error(npm.text.looseQuery));
  105. }
  106. return config.$npm.query.call(this, ctx, query, values, qrm);
  107. };
  108. /**
  109. * @deprecated
  110. * Consider using <b>async/await</b> syntax instead, or if you must have
  111. * pre-generated promises, then $[Promise.allSettled].
  112. *
  113. * @method Task#batch
  114. * @description
  115. * Settles a predefined array of mixed values by redirecting to method $[spex.batch].
  116. *
  117. * For complete method documentation see $[spex.batch].
  118. *
  119. * @param {array} values
  120. * @param {Object} [options]
  121. * Optional Parameters.
  122. * @param {function} [options.cb]
  123. *
  124. * @returns {external:Promise}
  125. */
  126. this.batch = function (values, options) {
  127. return config.$npm.spex.batch.call(this, values, options);
  128. };
  129. /**
  130. * @method Task#page
  131. * @description
  132. * Resolves a dynamic sequence of arrays/pages with mixed values, by redirecting to method $[spex.page].
  133. *
  134. * For complete method documentation see $[spex.page].
  135. *
  136. * @param {function} source
  137. * @param {Object} [options]
  138. * Optional Parameters.
  139. * @param {function} [options.dest]
  140. * @param {number} [options.limit=0]
  141. *
  142. * @returns {external:Promise}
  143. */
  144. this.page = function (source, options) {
  145. return config.$npm.spex.page.call(this, source, options);
  146. };
  147. /**
  148. * @method Task#sequence
  149. * @description
  150. * Resolves a dynamic sequence of mixed values by redirecting to method $[spex.sequence].
  151. *
  152. * For complete method documentation see $[spex.sequence].
  153. *
  154. * @param {function} source
  155. * @param {Object} [options]
  156. * Optional Parameters.
  157. * @param {function} [options.dest]
  158. * @param {number} [options.limit=0]
  159. * @param {boolean} [options.track=false]
  160. *
  161. * @returns {external:Promise}
  162. */
  163. this.sequence = function (source, options) {
  164. return config.$npm.spex.sequence.call(this, source, options);
  165. };
  166. }
  167. /**
  168. * @private
  169. * @method Task.callback
  170. * Callback invocation helper.
  171. *
  172. * @param ctx
  173. * @param obj
  174. * @param cb
  175. * @param config
  176. * @returns {Promise.<TResult>}
  177. */
  178. const callback = (ctx, obj, cb, config) => {
  179. const $p = config.promise;
  180. let result;
  181. try {
  182. if (cb.constructor.name === 'GeneratorFunction') {
  183. // v9.0 dropped all support for ES6 generator functions;
  184. // Clients should use the new ES7 async/await syntax.
  185. throw new TypeError('ES6 generator functions are no longer supported!');
  186. }
  187. result = cb.call(obj, obj); // invoking the callback function;
  188. } catch (err) {
  189. Events.error(ctx.options, err, {
  190. client: ctx.db && ctx.db.client, // the error can be due to loss of connectivity
  191. dc: ctx.dc,
  192. ctx: ctx.ctx
  193. });
  194. return $p.reject(err); // reject with the error;
  195. }
  196. if (result && typeof result.then === 'function') {
  197. return result; // result is a valid promise object;
  198. }
  199. return $p.resolve(result);
  200. };
  201. /**
  202. * @private
  203. * @method Task.execute
  204. * Executes a task.
  205. *
  206. * @param ctx
  207. * @param obj
  208. * @param isTX
  209. * @param config
  210. * @returns {Promise.<TResult>}
  211. */
  212. const execute = (ctx, obj, isTX, config) => {
  213. const $p = config.promise;
  214. // updates the task context and notifies the client;
  215. function update(start, success, result) {
  216. const c = ctx.ctx;
  217. if (start) {
  218. npm.utils.addReadProp(c, 'start', new Date());
  219. } else {
  220. c.finish = new Date();
  221. c.success = success;
  222. c.result = result;
  223. c.duration = c.finish - c.start;
  224. }
  225. (isTX ? Events.transact : Events.task)(ctx.options, {
  226. client: ctx.db && ctx.db.client, // loss of connectivity is possible at this point
  227. dc: ctx.dc,
  228. ctx: c
  229. });
  230. }
  231. let cbData, cbReason, success,
  232. spName; // Save-Point Name;
  233. const capSQL = ctx.options.capSQL; // capitalize sql;
  234. update(true);
  235. if (isTX) {
  236. // executing a transaction;
  237. spName = `sp_${ctx.txLevel}_${ctx.nextTxCount}`;
  238. return begin()
  239. .then(() => callback(ctx, obj, ctx.cb, config)
  240. .then(data => {
  241. cbData = data; // save callback data;
  242. success = true;
  243. return commit();
  244. }, err => {
  245. cbReason = err; // save callback failure reason;
  246. return rollback();
  247. })
  248. .then(() => {
  249. if (success) {
  250. update(false, true, cbData);
  251. return cbData;
  252. }
  253. update(false, false, cbReason);
  254. return $p.reject(cbReason);
  255. },
  256. err => {
  257. // either COMMIT or ROLLBACK has failed, which is impossible
  258. // to replicate in a test environment, so skipping from the test;
  259. // istanbul ignore next:
  260. update(false, false, err);
  261. // istanbul ignore next:
  262. return $p.reject(err);
  263. }),
  264. err => {
  265. // BEGIN has failed, which is impossible to replicate in a test
  266. // environment, so skipping the whole block from the test;
  267. // istanbul ignore next:
  268. update(false, false, err);
  269. // istanbul ignore next:
  270. return $p.reject(err);
  271. });
  272. }
  273. function begin() {
  274. if (!ctx.txLevel && ctx.mode instanceof npm.mode.TransactionMode) {
  275. return exec(ctx.mode.begin(capSQL), 'savepoint');
  276. }
  277. return exec('begin', 'savepoint');
  278. }
  279. function commit() {
  280. return exec('commit', 'release savepoint');
  281. }
  282. function rollback() {
  283. return exec('rollback', 'rollback to savepoint');
  284. }
  285. function exec(top, nested) {
  286. if (ctx.txLevel) {
  287. return obj.none((capSQL ? nested.toUpperCase() : nested) + ' ' + spName);
  288. }
  289. return obj.none(capSQL ? top.toUpperCase() : top);
  290. }
  291. // executing a task;
  292. return callback(ctx, obj, ctx.cb, config)
  293. .then(data => {
  294. update(false, true, data);
  295. return data;
  296. })
  297. .catch(error => {
  298. update(false, false, error);
  299. return $p.reject(error);
  300. });
  301. };
  302. module.exports = config => {
  303. const npmLocal = config.$npm;
  304. // istanbul ignore next:
  305. // we keep 'npm.query' initialization here, even though it is always
  306. // pre-initialized by the 'database' module, for integrity purpose.
  307. npmLocal.query = npmLocal.query || npm.query(config);
  308. npmLocal.spex = npmLocal.spex || npm.spex(config.promiseLib);
  309. return {
  310. Task, execute, callback
  311. };
  312. };
  313. /**
  314. * @typedef TaskContext
  315. * @description
  316. * Task/Transaction Context used via property {@link Task#ctx ctx} inside tasks (methods {@link Database#task Database.task} and {@link Database#taskIf Database.taskIf})
  317. * and transactions (methods {@link Database#tx Database.tx} and {@link Database#txIf Database.txIf}).
  318. *
  319. * Properties `context`, `connected`, `parent`, `level`, `dc`, `isTX`, `tag`, `start`, `useCount` and `serverVersion` are set just before the operation has started,
  320. * while properties `finish`, `duration`, `success` and `result` are set immediately after the operation has finished.
  321. *
  322. * @property {*} context
  323. * If the operation was invoked with a calling context - `task.call(context,...)` or `tx.call(context,...)`,
  324. * this property is set with the context that was passed in. Otherwise, the property doesn't exist.
  325. *
  326. * @property {*} dc
  327. * _Database Context_ that was passed into the {@link Database} object during construction.
  328. *
  329. * @property {boolean} isTX
  330. * Indicates whether this operation is a transaction (as opposed to a regular task).
  331. *
  332. * @property {number} duration
  333. * Number of milliseconds consumed by the operation.
  334. *
  335. * Set after the operation has finished, it is simply a shortcut for `finish - start`.
  336. *
  337. * @property {number} level
  338. * Task nesting level, starting from 0, counting both regular tasks and transactions.
  339. *
  340. * @property {number} txLevel
  341. * Transaction nesting level, starting from 0. Transactions on level 0 use `BEGIN/COMMIT/ROLLBACK`,
  342. * while transactions on nested levels use the corresponding `SAVEPOINT` commands.
  343. *
  344. * This property exists only within the context of a transaction (`isTX = true`).
  345. *
  346. * @property {boolean} inTransaction
  347. * Available in both tasks and transactions, it simplifies checking when there is a transaction
  348. * going on either on this level or above.
  349. *
  350. * For example, when you want to check for a containing transaction while inside a task, and
  351. * only start a transaction when there is none yet.
  352. *
  353. * @property {TaskContext} parent
  354. * Parent task/transaction context, or `null` when it is top-level.
  355. *
  356. * @property {boolean} connected
  357. * Indicates when the task/transaction acquired the connection on its own (`connected = true`), and will release it once
  358. * the operation has finished. When the value is `false`, the operation is reusing an existing connection.
  359. *
  360. * @property {*} tag
  361. * Tag value as it was passed into the task. See methods {@link Database#task task} and {@link Database#tx tx}.
  362. *
  363. * @property {Date} start
  364. * Date/Time of when this operation started the execution.
  365. *
  366. * @property {number} useCount
  367. * Number of times the connection has been previously used, starting with 0 for a freshly
  368. * allocated physical connection.
  369. *
  370. * @property {string} serverVersion
  371. * Version of the PostgreSQL server to which we are connected.
  372. * Not available with $[Native Bindings].
  373. *
  374. * @property {Date} finish
  375. * Once the operation has finished, this property is set to the Data/Time of when it happened.
  376. *
  377. * @property {boolean} success
  378. * Once the operation has finished, this property indicates whether it was successful.
  379. *
  380. * @property {*} result
  381. * Once the operation has finished, this property contains the result, depending on property `success`:
  382. * - data resolved by the operation, if `success = true`
  383. * - error / rejection reason, if `success = false`
  384. *
  385. */