events.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  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 {ColorConsole} = require('./utils/color');
  10. const npm = {
  11. main: require('./'),
  12. utils: require('./utils')
  13. };
  14. /////////////////////////////////
  15. // Client notification helpers;
  16. class Events {
  17. /**
  18. * @event connect
  19. * @description
  20. * Global notification of acquiring a new database connection from the connection pool, i.e. a virtual connection.
  21. *
  22. * However, for direct calls to method {@link Database#connect Database.connect} with parameter `{direct: true}`,
  23. * this event represents a physical connection.
  24. *
  25. * The library will suppress any error thrown by the handler and write it into the console.
  26. *
  27. * @param {{}} e Event Properties
  28. *
  29. * @param {external:Client} e.client
  30. * $[pg.Client] object that represents the connection.
  31. *
  32. * @param {*} e.dc
  33. * Database Context that was used when creating the database object (see {@link Database}).
  34. *
  35. * @param {number} e.useCount
  36. * Number of times the connection has been previously used, starting with 0, for a freshly
  37. * allocated physical connection.
  38. *
  39. * This parameter is always 0 for direct connections (created by calling {@link Database#connect Database.connect}
  40. * with parameter `{direct: true}`).
  41. *
  42. * @example
  43. *
  44. * const initOptions = {
  45. *
  46. * // pg-promise initialization options...
  47. *
  48. * connect(e) {
  49. * const cp = e.client.connectionParameters;
  50. * console.log('Connected to database:', cp.database);
  51. * }
  52. *
  53. * };
  54. */
  55. static connect(ctx, client, useCount) {
  56. if (typeof ctx.options.connect === 'function') {
  57. try {
  58. ctx.options.connect({client, dc: ctx.dc, useCount});
  59. } catch (e) {
  60. // have to silence errors here;
  61. // cannot allow unhandled errors while connecting to the database,
  62. // as it will break the connection logic;
  63. Events.unexpected('connect', e);
  64. }
  65. }
  66. }
  67. /**
  68. * @event disconnect
  69. * @description
  70. * Global notification of releasing a database connection back to the connection pool, i.e. releasing the virtual connection.
  71. *
  72. * However, when releasing a direct connection (created by calling {@link Database#connect Database.connect} with parameter
  73. * `{direct: true}`), this event represents a physical disconnection.
  74. *
  75. * The library will suppress any error thrown by the handler and write it into the console.
  76. *
  77. * @param {{}} e Event Properties
  78. *
  79. * @param {external:Client} e.client - $[pg.Client] object that represents connection with the database.
  80. *
  81. * @param {*} e.dc - Database Context that was used when creating the database object (see {@link Database}).
  82. *
  83. * @example
  84. *
  85. * const initOptions = {
  86. *
  87. * // pg-promise initialization options...
  88. *
  89. * disconnect(e) {
  90. * const cp = e.client.connectionParameters;
  91. * console.log('Disconnecting from database:', cp.database);
  92. * }
  93. *
  94. * };
  95. */
  96. static disconnect(ctx, client) {
  97. if (typeof ctx.options.disconnect === 'function') {
  98. try {
  99. ctx.options.disconnect({client, dc: ctx.dc});
  100. } catch (e) {
  101. // have to silence errors here;
  102. // cannot allow unhandled errors while disconnecting from the database,
  103. // as it will break the disconnection logic;
  104. Events.unexpected('disconnect', e);
  105. }
  106. }
  107. }
  108. /**
  109. * @event query
  110. * @description
  111. *
  112. * Global notification of a query that's about to execute.
  113. *
  114. * Notification happens just before the query execution. And if the handler throws an error, the query execution
  115. * will be rejected with that error.
  116. *
  117. * @param {EventContext} e
  118. * Event Context Object.
  119. *
  120. * @example
  121. *
  122. * const initOptions = {
  123. *
  124. * // pg-promise initialization options...
  125. *
  126. * query(e) {
  127. * console.log('QUERY:', e.query);
  128. * }
  129. * };
  130. */
  131. static query(options, context) {
  132. if (typeof options.query === 'function') {
  133. try {
  134. options.query(context);
  135. } catch (e) {
  136. // throwing an error during event 'query'
  137. // will result in a reject for the request.
  138. return e instanceof Error ? e : new npm.utils.InternalError(e);
  139. }
  140. }
  141. }
  142. /**
  143. * @event receive
  144. * @description
  145. * Global notification of any data received from the database, coming from a regular query or from a stream.
  146. *
  147. * The event is fired before the data reaches the client, and it serves two purposes:
  148. * - Providing selective data logging for debugging;
  149. * - Pre-processing data before it reaches the client.
  150. *
  151. * **NOTES:**
  152. * - If you alter the size of `data` directly or through the `result` object, it may affect `QueryResultMask`
  153. * validation for regular queries, which is executed right after.
  154. * - Any data pre-processing needs to be fast here, to avoid performance penalties.
  155. * - If the event handler throws an error, the original request will be rejected with that error.
  156. *
  157. * For methods {@link Database#multi Database.multi} and {@link Database#multiResult Database.multiResult},
  158. * this event is called for every result that's returned. And for method {@link Database#stream Database.stream},
  159. * the event occurs for every record.
  160. *
  161. * @param {{}} e Event Properties
  162. *
  163. * @param {Array<Object>} e.data
  164. * Array of received objects/rows.
  165. *
  166. * If any of those objects are modified during notification, the client will receive the modified data.
  167. *
  168. * @param {external:Result} e.result
  169. * - Original $[Result] object, if the data is from a non-stream query, in which case `data = result.rows`.
  170. * For single-query requests, $[Result] object is extended with property `duration` - number of milliseconds
  171. * it took to send the query, execute it and get the result back.
  172. * - It is `undefined` when the data comes from a stream (method {@link Database#stream Database.stream}).
  173. *
  174. * @param {EventContext} e.ctx
  175. * Event Context Object.
  176. *
  177. * @example
  178. *
  179. * // Example below shows the fastest way to camelize all column names.
  180. * // NOTE: The example does not do processing for nested JSON objects.
  181. *
  182. * const initOptions = {
  183. *
  184. * // pg-promise initialization options...
  185. *
  186. * receive(e) {
  187. * camelizeColumns(e.data);
  188. * }
  189. * };
  190. *
  191. * function camelizeColumns(data) {
  192. * const tmp = data[0];
  193. * for (const prop in tmp) {
  194. * const camel = pgp.utils.camelize(prop);
  195. * if (!(camel in tmp)) {
  196. * for (let i = 0; i < data.length; i++) {
  197. * const d = data[i];
  198. * d[camel] = d[prop];
  199. * delete d[prop];
  200. * }
  201. * }
  202. * }
  203. * }
  204. */
  205. static receive(options, data, result, ctx) {
  206. if (typeof options.receive === 'function') {
  207. try {
  208. options.receive({data, result, ctx});
  209. } catch (e) {
  210. // throwing an error during event 'receive'
  211. // will result in a reject for the request.
  212. return e instanceof Error ? e : new npm.utils.InternalError(e);
  213. }
  214. }
  215. }
  216. /**
  217. * @event task
  218. * @description
  219. * Global notification of a task start / finish events, as executed via
  220. * {@link Database#task Database.task} or {@link Database#taskIf Database.taskIf}.
  221. *
  222. * The library will suppress any error thrown by the handler and write it into the console.
  223. *
  224. * @param {EventContext} e
  225. * Event Context Object.
  226. *
  227. * @example
  228. *
  229. * const initOptions = {
  230. *
  231. * // pg-promise initialization options...
  232. *
  233. * task(e) {
  234. * if (e.ctx.finish) {
  235. * // this is a task->finish event;
  236. * console.log('Duration:', e.ctx.duration);
  237. * if (e.ctx.success) {
  238. * // e.ctx.result = resolved data;
  239. * } else {
  240. * // e.ctx.result = error/rejection reason;
  241. * }
  242. * } else {
  243. * // this is a task->start event;
  244. * console.log('Start Time:', e.ctx.start);
  245. * }
  246. * }
  247. * };
  248. *
  249. */
  250. static task(options, context) {
  251. if (typeof options.task === 'function') {
  252. try {
  253. options.task(context);
  254. } catch (e) {
  255. // silencing the error, to avoid breaking the task;
  256. Events.unexpected('task', e);
  257. }
  258. }
  259. }
  260. /**
  261. * @event transact
  262. * @description
  263. * Global notification of a transaction start / finish events, as executed via {@link Database#tx Database.tx}
  264. * or {@link Database#txIf Database.txIf}.
  265. *
  266. * The library will suppress any error thrown by the handler and write it into the console.
  267. *
  268. * @param {EventContext} e
  269. * Event Context Object.
  270. *
  271. * @example
  272. *
  273. * const initOptions = {
  274. *
  275. * // pg-promise initialization options...
  276. *
  277. * transact(e) {
  278. * if (e.ctx.finish) {
  279. * // this is a transaction->finish event;
  280. * console.log('Duration:', e.ctx.duration);
  281. * if (e.ctx.success) {
  282. * // e.ctx.result = resolved data;
  283. * } else {
  284. * // e.ctx.result = error/rejection reason;
  285. * }
  286. * } else {
  287. * // this is a transaction->start event;
  288. * console.log('Start Time:', e.ctx.start);
  289. * }
  290. * }
  291. * };
  292. *
  293. */
  294. static transact(options, context) {
  295. if (typeof options.transact === 'function') {
  296. try {
  297. options.transact(context);
  298. } catch (e) {
  299. // silencing the error, to avoid breaking the transaction;
  300. Events.unexpected('transact', e);
  301. }
  302. }
  303. }
  304. /**
  305. * @event error
  306. * @description
  307. * Global notification of every error encountered by this library.
  308. *
  309. * The library will suppress any error thrown by the handler and write it into the console.
  310. *
  311. * @param {*} err
  312. * The error encountered, of the same value and type as it was reported.
  313. *
  314. * @param {EventContext} e
  315. * Event Context Object.
  316. *
  317. * @example
  318. * const initOptions = {
  319. *
  320. * // pg-promise initialization options...
  321. *
  322. * error(err, e) {
  323. *
  324. * if (e.cn) {
  325. * // this is a connection-related error
  326. * // cn = safe connection details passed into the library:
  327. * // if password is present, it is masked by #
  328. * }
  329. *
  330. * if (e.query) {
  331. * // query string is available
  332. * if (e.params) {
  333. * // query parameters are available
  334. * }
  335. * }
  336. *
  337. * if (e.ctx) {
  338. * // occurred inside a task or transaction
  339. * }
  340. * }
  341. * };
  342. *
  343. */
  344. static error(options, err, context) {
  345. if (typeof options.error === 'function') {
  346. try {
  347. options.error(err, context);
  348. } catch (e) {
  349. // have to silence errors here;
  350. // throwing unhandled errors while handling an error
  351. // notification is simply not acceptable.
  352. Events.unexpected('error', e);
  353. }
  354. }
  355. }
  356. /**
  357. * @event extend
  358. * @description
  359. * Extends {@link Database} protocol with custom methods and properties.
  360. *
  361. * Override this event to extend the existing access layer with your own functions and
  362. * properties best suited for your application.
  363. *
  364. * The extension thus becomes available across all access layers:
  365. *
  366. * - Within the root/default database protocol;
  367. * - Inside transactions, including nested ones;
  368. * - Inside tasks, including nested ones.
  369. *
  370. * All pre-defined methods and properties are read-only, so you will get an error,
  371. * if you try overriding them.
  372. *
  373. * The library will suppress any error thrown by the handler and write it into the console.
  374. *
  375. * @param {object} obj - Protocol object to be extended.
  376. *
  377. * @param {*} dc - Database Context that was used when creating the {@link Database} object.
  378. *
  379. * @see $[pg-promise-demo]
  380. *
  381. * @example
  382. *
  383. * // In the example below we extend the protocol with function `addImage`
  384. * // that will insert one binary image and resolve with the new record id.
  385. *
  386. * const initOptions = {
  387. *
  388. * // pg-promise initialization options...
  389. *
  390. * extend(obj, dc) {
  391. * // dc = database context;
  392. * obj.addImage = data => {
  393. * // adds a new image and resolves with its record id:
  394. * return obj.one('INSERT INTO images(data) VALUES($1) RETURNING id', data, a => a.id);
  395. * }
  396. * }
  397. * };
  398. *
  399. * @example
  400. *
  401. * // It is best to extend the protocol by adding whole entity repositories to it as shown in the following example.
  402. * // For a comprehensive example see https://github.com/vitaly-t/pg-promise-demo
  403. *
  404. * class UsersRepository {
  405. * constructor(rep, pgp) {
  406. * this.rep = rep;
  407. * this.pgp = pgp;
  408. * }
  409. *
  410. * add(name) {
  411. * return this.rep.one('INSERT INTO users(name) VALUES($1) RETURNING id', name, a => a.id);
  412. * }
  413. *
  414. * remove(id) {
  415. * return this.rep.none('DELETE FROM users WHERE id = $1', id);
  416. * }
  417. * }
  418. *
  419. * // Overriding 'extend' event;
  420. * const initOptions = {
  421. *
  422. * // pg-promise initialization options...
  423. *
  424. * extend(obj, dc) {
  425. * // dc = database context;
  426. * obj.users = new UsersRepository(obj, pgp);
  427. * // You can set different repositories based on `dc`
  428. * }
  429. * };
  430. *
  431. * // Usage example:
  432. * db.users.add('John', true)
  433. * .then(id => {
  434. * // user added successfully, id = new user's id
  435. * })
  436. * .catch(error => {
  437. * // failed to add the user;
  438. * });
  439. *
  440. */
  441. static extend(options, obj, dc) {
  442. if (typeof options.extend === 'function') {
  443. try {
  444. options.extend.call(obj, obj, dc);
  445. } catch (e) {
  446. // have to silence errors here;
  447. // the result of throwing unhandled errors while
  448. // extending the protocol would be unpredictable.
  449. Events.unexpected('extend', e);
  450. }
  451. }
  452. }
  453. /**
  454. * @event unexpected
  455. * @param {string} event - unhandled event name.
  456. * @param {string|Error} e - unhandled error.
  457. * @private
  458. */
  459. static unexpected(event, e) {
  460. // If you should ever get here, your app is definitely broken, and you need to fix
  461. // your event handler to prevent unhandled errors during event notifications.
  462. //
  463. // Console output is suppressed when running tests, to avoid polluting test output
  464. // with error messages that are intentional and of no value to the test.
  465. /* istanbul ignore if */
  466. if (!npm.main.suppressErrors) {
  467. const stack = e instanceof Error ? e.stack : new Error().stack;
  468. ColorConsole.error(`Unexpected error in '${event}' event handler.\n${stack}\n`);
  469. }
  470. }
  471. }
  472. module.exports = {Events};
  473. /**
  474. * @typedef EventContext
  475. * @description
  476. * This common type is used for the following events: {@link event:query query}, {@link event:receive receive},
  477. * {@link event:error error}, {@link event:task task} and {@link event:transact transact}.
  478. *
  479. * @property {string|object} cn
  480. *
  481. * Set only for event {@link event:error error}, and only when the error is connection-related.
  482. *
  483. * It is a safe copy of the connection string/object that was used when initializing `db` - the database instance.
  484. *
  485. * If the original connection contains a password, the safe copy contains it masked with symbol `#`, so the connection
  486. * can be logged safely, without exposing the password.
  487. *
  488. * @property {*} dc
  489. * Database Context that was used when creating the database object (see {@link Database}). It is set for all events.
  490. *
  491. * @property {string|object} query
  492. *
  493. * Query string/object that was passed into the query method. This property is only set during events {@link event:query query},
  494. * {@link event:receive receive} and {@link event:error error} (only when the error is query-related).
  495. *
  496. * @property {external:Client} client
  497. *
  498. * $[pg.Client] object that represents the connection. It is set for all events, except for event {@link event:error error}
  499. * when it is connection-related. Note that sometimes the value may be unset when the connection is lost.
  500. *
  501. * @property {*} params - Formatting parameters for the query.
  502. *
  503. * It is set only for events {@link event:query query}, {@link event:receive receive} and {@link event:error error}, and only
  504. * when it is needed for logging. This library takes an extra step in figuring out when formatting parameters are of any value
  505. * to the event logging:
  506. * - when an error occurs related to the query formatting, event {@link event:error error} is sent with the property set.
  507. * - when initialization parameter `pgFormat` is used, and all query formatting is done within the $[PG] library, events
  508. * {@link event:query query} and {@link event:receive receive} will have this property set also, since this library no longer
  509. * handles the query formatting.
  510. *
  511. * When this parameter is not set, it means one of the two things:
  512. * - there were no parameters passed into the query method;
  513. * - property `query` of this object already contains all the formatting values in it, so logging only the query is sufficient.
  514. *
  515. * @property {TaskContext} ctx
  516. * _Task/Transaction Context_ object.
  517. *
  518. * This property is always set for events {@link event:task task} and {@link event:transact transact}, while for events
  519. * {@link event:query query}, {@link event:receive receive} and {@link event:error error} it is only set when they occur
  520. * inside a task or transaction.
  521. */