index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. const themes = require('./themes');
  2. let cct = themes.dimmed; // current/default color theme;
  3. // monitor state;
  4. const $state = {};
  5. // supported events;
  6. const $events = ['connect', 'disconnect', 'query', 'error', 'task', 'transact'];
  7. const hasOwnProperty = (obj, propName) => Object.prototype.hasOwnProperty.call(obj, propName);
  8. const monitor = {
  9. ///////////////////////////////////////////////
  10. // 'connect' event handler;
  11. connect(e, detailed) {
  12. const event = 'connect';
  13. const cp = e?.client?.connectionParameters;
  14. if (!cp) {
  15. throw new TypeError(errors.redirectParams(event));
  16. }
  17. const d = (detailed === undefined) ? monitor.detailed : !!detailed;
  18. if (d) {
  19. const countInfo = typeof e.useCount === 'number' ? cct.cn('; useCount: ') + cct.value(e.useCount) : '';
  20. print(null, event, cct.cn('connect(') + cct.value(cp.user + '@' + cp.database) + cct.cn(')') + countInfo);
  21. } else {
  22. print(null, event, cct.cn('connect'));
  23. }
  24. },
  25. ///////////////////////////////////////////////
  26. // 'connect' event handler;
  27. disconnect(e, detailed) {
  28. const event = 'disconnect';
  29. const cp = e?.client?.connectionParameters;
  30. if (!cp) {
  31. throw new TypeError(errors.redirectParams(event));
  32. }
  33. const d = (detailed === undefined) ? monitor.detailed : !!detailed;
  34. if (d) {
  35. // report user@database details;
  36. print(null, event, cct.cn('disconnect(') + cct.value(cp.user + '@' + cp.database) + cct.cn(')'));
  37. } else {
  38. print(null, event, cct.cn('disconnect'));
  39. }
  40. },
  41. ///////////////////////////////////////////////
  42. // 'query' event handler;
  43. // parameters:
  44. // - e - the only parameter for the event;
  45. // - detailed - optional, indicates that both task and transaction context are to be reported;
  46. query(e, detailed) {
  47. const event = 'query';
  48. if (!e || !('query' in e)) {
  49. throw new TypeError(errors.redirectParams(event));
  50. }
  51. let q = e.query;
  52. let special, prepared;
  53. if (typeof q === 'string') {
  54. const qSmall = q.toLowerCase();
  55. const verbs = ['begin', 'commit', 'rollback', 'savepoint', 'release'];
  56. for (let i = 0; i < verbs.length; i++) {
  57. if (qSmall.indexOf(verbs[i]) === 0) {
  58. special = true;
  59. break;
  60. }
  61. }
  62. } else {
  63. if (typeof q === 'object' && ('name' in q || 'text' in q)) {
  64. // Either a Prepared Statement or a Parameterized Query;
  65. prepared = true;
  66. const msg = [];
  67. if ('name' in q) {
  68. msg.push(cct.query('name=') + '"' + cct.value(q.name) + '"');
  69. }
  70. if ('text' in q) {
  71. msg.push(cct.query('text=') + '"' + cct.value(q.text) + '"');
  72. }
  73. if (Array.isArray(q.values) && q.values.length) {
  74. msg.push(cct.query('values=') + cct.value(toJson(q.values)));
  75. }
  76. q = msg.join(', ');
  77. }
  78. }
  79. let qText = q;
  80. if (!prepared) {
  81. qText = special ? cct.special(q) : cct.query(q);
  82. }
  83. const d = (detailed === undefined) ? monitor.detailed : !!detailed;
  84. if (d && e.ctx) {
  85. // task/transaction details are to be reported;
  86. const sTag = getTagName(e), prefix = e.ctx.isTX ? 'tx' : 'task';
  87. if (sTag) {
  88. qText = cct.tx(prefix + '(') + cct.value(sTag) + cct.tx('): ') + qText;
  89. } else {
  90. qText = cct.tx(prefix + ': ') + qText;
  91. }
  92. }
  93. print(e, event, qText);
  94. if (e.params) {
  95. let p = e.params;
  96. if (typeof p !== 'string') {
  97. p = toJson(p);
  98. }
  99. print(e, event, timeGap + cct.paramTitle('params: ') + cct.value(p), true);
  100. }
  101. },
  102. ///////////////////////////////////////////////
  103. // 'task' event handler;
  104. // parameters:
  105. // - e - the only parameter for the event;
  106. task(e) {
  107. const event = 'task';
  108. if (!e || !e.ctx) {
  109. throw new TypeError(errors.redirectParams(event));
  110. }
  111. let msg = cct.tx('task');
  112. const sTag = getTagName(e);
  113. if (sTag) {
  114. msg += cct.tx('(') + cct.value(sTag) + cct.tx(')');
  115. }
  116. if (e.ctx.finish) {
  117. msg += cct.tx('/end');
  118. } else {
  119. msg += cct.tx('/start');
  120. }
  121. if (e.ctx.finish) {
  122. const duration = formatDuration(e.ctx.finish - e.ctx.start);
  123. msg += cct.tx('; duration: ') + cct.value(duration) + cct.tx(', success: ') + cct.value(!!e.ctx.success);
  124. }
  125. print(e, event, msg);
  126. },
  127. ///////////////////////////////////////////////
  128. // 'transact' event handler;
  129. // parameters:
  130. // - e - the only parameter for the event;
  131. transact(e) {
  132. const event = 'transact';
  133. if (!e || !e.ctx) {
  134. throw new TypeError(errors.redirectParams(event));
  135. }
  136. let msg = cct.tx('tx');
  137. const sTag = getTagName(e);
  138. if (sTag) {
  139. msg += cct.tx('(') + cct.value(sTag) + cct.tx(')');
  140. }
  141. if (e.ctx.finish) {
  142. msg += cct.tx('/end');
  143. } else {
  144. msg += cct.tx('/start');
  145. }
  146. if (e.ctx.finish) {
  147. const duration = formatDuration(e.ctx.finish - e.ctx.start);
  148. msg += cct.tx('; duration: ') + cct.value(duration) + cct.tx(', success: ') + cct.value(!!e.ctx.success);
  149. }
  150. print(e, event, msg);
  151. },
  152. ///////////////////////////////////////////////
  153. // 'error' event handler;
  154. // parameters:
  155. // - err - error-text parameter for the original event;
  156. // - e - error context object for the original event;
  157. // - detailed - optional, indicates that transaction context is to be reported;
  158. error(err, e, detailed) {
  159. const event = 'error';
  160. const errMsg = err ? (err.message || err) : null;
  161. if (!e || typeof e !== 'object') {
  162. throw new TypeError(errors.redirectParams(event));
  163. }
  164. print(e, event, cct.errorTitle('error: ') + cct.error(errMsg));
  165. let q = e.query;
  166. if (q !== undefined && typeof q !== 'string') {
  167. if (typeof q === 'object' && ('name' in q || 'text' in q)) {
  168. const tmp = {};
  169. const names = ['name', 'text', 'values'];
  170. names.forEach(n => {
  171. if (n in q) {
  172. tmp[n] = q[n];
  173. }
  174. });
  175. q = tmp;
  176. }
  177. q = toJson(q);
  178. }
  179. if (e.cn) {
  180. // a connection issue;
  181. print(e, event, timeGap + cct.paramTitle('connection: ') + cct.value(toJson(e.cn)), true);
  182. } else {
  183. if (q !== undefined) {
  184. const d = (detailed === undefined) ? monitor.detailed : !!detailed;
  185. if (d && e.ctx) {
  186. // transaction details are to be reported;
  187. const sTag = getTagName(e), prefix = e.ctx.isTX ? 'tx' : 'task';
  188. if (sTag) {
  189. print(e, event, timeGap + cct.paramTitle(prefix + '(') + cct.value(sTag) + cct.paramTitle('): ') + cct.value(q), true);
  190. } else {
  191. print(e, event, timeGap + cct.paramTitle(prefix + ': ') + cct.value(q), true);
  192. }
  193. } else {
  194. print(e, event, timeGap + cct.paramTitle('query: ') + cct.value(q), true);
  195. }
  196. }
  197. }
  198. if (e.params) {
  199. print(e, event, timeGap + cct.paramTitle('params: ') + cct.value(toJson(e.params)), true);
  200. }
  201. },
  202. /////////////////////////////////////////////////////////
  203. // attaches to pg-promise initialization options object:
  204. // - options - the options object;
  205. // - events - optional, list of events to attach to;
  206. // - override - optional, overrides the existing event handlers;
  207. attach(options, events, override) {
  208. if (options && options.options && typeof options.options === 'object') {
  209. events = options.events;
  210. override = options.override;
  211. options = options.options;
  212. }
  213. if ($state.options) {
  214. throw new Error('Repeated attachments not supported, must call detach first.');
  215. }
  216. if (!options || typeof options !== 'object') {
  217. throw new TypeError('Initialization object \'options\' must be specified.');
  218. }
  219. const hasFilter = Array.isArray(events);
  220. if (!isNull(events) && !hasFilter) {
  221. throw new TypeError('Invalid parameter \'events\' passed.');
  222. }
  223. $state.options = options;
  224. const self = monitor;
  225. // attaching to 'connect' event:
  226. if (!hasFilter || events.indexOf('connect') !== -1) {
  227. $state.connect = {
  228. value: options.connect,
  229. exists: 'connect' in options
  230. };
  231. if (typeof options.connect === 'function' && !override) {
  232. options.connect = function (e) {
  233. $state.connect.value(e); // call the original handler;
  234. self.connect(e);
  235. };
  236. } else {
  237. options.connect = self.connect;
  238. }
  239. }
  240. // attaching to 'disconnect' event:
  241. if (!hasFilter || events.indexOf('disconnect') !== -1) {
  242. $state.disconnect = {
  243. value: options.disconnect,
  244. exists: 'disconnect' in options
  245. };
  246. if (typeof options.disconnect === 'function' && !override) {
  247. options.disconnect = function (e) {
  248. $state.disconnect.value(e); // call the original handler;
  249. self.disconnect(e);
  250. };
  251. } else {
  252. options.disconnect = self.disconnect;
  253. }
  254. }
  255. // attaching to 'query' event:
  256. if (!hasFilter || events.indexOf('query') !== -1) {
  257. $state.query = {
  258. value: options.query,
  259. exists: 'query' in options
  260. };
  261. if (typeof options.query === 'function' && !override) {
  262. options.query = function (e) {
  263. $state.query.value(e); // call the original handler;
  264. self.query(e);
  265. };
  266. } else {
  267. options.query = self.query;
  268. }
  269. }
  270. // attaching to 'task' event:
  271. if (!hasFilter || events.indexOf('task') !== -1) {
  272. $state.task = {
  273. value: options.task,
  274. exists: 'task' in options
  275. };
  276. if (typeof options.task === 'function' && !override) {
  277. options.task = function (e) {
  278. $state.task.value(e); // call the original handler;
  279. self.task(e);
  280. };
  281. } else {
  282. options.task = self.task;
  283. }
  284. }
  285. // attaching to 'transact' event:
  286. if (!hasFilter || events.indexOf('transact') !== -1) {
  287. $state.transact = {
  288. value: options.transact,
  289. exists: 'transact' in options
  290. };
  291. if (typeof options.transact === 'function' && !override) {
  292. options.transact = function (e) {
  293. $state.transact.value(e); // call the original handler;
  294. self.transact(e);
  295. };
  296. } else {
  297. options.transact = self.transact;
  298. }
  299. }
  300. // attaching to 'error' event:
  301. if (!hasFilter || events.indexOf('error') !== -1) {
  302. $state.error = {
  303. value: options.error,
  304. exists: 'error' in options
  305. };
  306. if (typeof options.error === 'function' && !override) {
  307. options.error = function (err, e) {
  308. $state.error.value(err, e); // call the original handler;
  309. self.error(err, e);
  310. };
  311. } else {
  312. options.error = self.error;
  313. }
  314. }
  315. },
  316. isAttached() {
  317. return !!$state.options;
  318. },
  319. /////////////////////////////////////////////////////////
  320. // detaches from all events to which was attached during
  321. // the last `attach` call.
  322. detach() {
  323. if (!$state.options) {
  324. throw new Error('Event monitor not attached.');
  325. }
  326. $events.forEach(e => {
  327. if (e in $state) {
  328. if ($state[e].exists) {
  329. $state.options[e] = $state[e].value;
  330. } else {
  331. delete $state.options[e];
  332. }
  333. delete $state[e];
  334. }
  335. });
  336. $state.options = null;
  337. },
  338. //////////////////////////////////////////////////////////////////
  339. // sets a new theme either by its name (from the predefined ones),
  340. // or as a new object with all colors specified.
  341. setTheme(t) {
  342. const err = 'Invalid theme parameter specified.';
  343. if (!t) {
  344. throw new TypeError(err);
  345. }
  346. if (typeof t === 'string') {
  347. if (t in themes) {
  348. cct = themes[t];
  349. } else {
  350. throw new TypeError('Theme \'' + t + '\' does not exist.');
  351. }
  352. } else {
  353. if (typeof t === 'object') {
  354. for (const p in themes.monochrome) {
  355. if (!hasOwnProperty(t, p)) {
  356. throw new TypeError('Invalid theme: property \'' + p + '\' is missing.');
  357. }
  358. if (typeof t[p] !== 'function') {
  359. throw new TypeError('Theme property \'' + p + '\' is invalid.');
  360. }
  361. }
  362. cct = t;
  363. } else {
  364. throw new Error(err);
  365. }
  366. }
  367. },
  368. //////////////////////////////////////////////////
  369. // global 'detailed' flag override, to report all
  370. // the optional details that are supported;
  371. detailed: true,
  372. //////////////////////////////////////////////////////////////////
  373. // sets a new value to the detailed var. This function is needed
  374. // to support the value attribution in Typescript.
  375. setDetailed(value) {
  376. this.detailed = !!value;
  377. },
  378. //////////////////////////////////////////////////////////////////
  379. // sets a custom log function to support the function attribution
  380. // in Typescript.
  381. setLog(log) {
  382. module.exports.log = typeof log === 'function' ? log : null;
  383. }
  384. };
  385. // prints the text on screen, optionally
  386. // notifying the client of the log events;
  387. function print(e, event, text, isExtraLine) {
  388. let t = null, s = text;
  389. if (!isExtraLine) {
  390. t = new Date();
  391. s = cct.time(formatTime(t)) + ' ' + text;
  392. }
  393. let display = true;
  394. const log = module.exports.log;
  395. if (typeof log === 'function') {
  396. // the client expects log notifications;
  397. const info = {
  398. event,
  399. time: t,
  400. colorText: text.trim(),
  401. text: removeColors(text).trim()
  402. };
  403. if (e && e.ctx) {
  404. info.ctx = e.ctx;
  405. }
  406. log(removeColors(s), info);
  407. display = info.display === undefined || !!info.display;
  408. }
  409. // istanbul ignore next: cannot test the next
  410. // block without writing things into the console;
  411. if (display) {
  412. if (!process.stdout.isTTY) {
  413. s = removeColors(s);
  414. }
  415. // eslint-disable-next-line
  416. console.log(s);
  417. }
  418. }
  419. // formats time as '00:00:00';
  420. function formatTime(t) {
  421. return padZeros(t.getHours(), 2) + ':' + padZeros(t.getMinutes(), 2) + ':' + padZeros(t.getSeconds(), 2);
  422. }
  423. // formats duration value (in milliseconds) as '00:00:00.000',
  424. // shortened to just the values that are applicable.
  425. function formatDuration(d) {
  426. const hours = Math.floor(d / 3600000);
  427. const minutes = Math.floor((d - hours * 3600000) / 60000);
  428. const seconds = Math.floor((d - hours * 3600000 - minutes * 60000) / 1000);
  429. const ms = d - hours * 3600000 - minutes * 60000 - seconds * 1000;
  430. let s = '.' + padZeros(ms, 3); // milliseconds are shown always;
  431. if (d >= 1000) {
  432. // seconds are to be shown;
  433. s = padZeros(seconds, 2) + s;
  434. if (d >= 60000) {
  435. // minutes are to be shown;
  436. s = padZeros(minutes, 2) + ':' + s;
  437. if (d >= 3600000) {
  438. // hours are to be shown;
  439. s = padZeros(hours, 2) + ':' + s;
  440. }
  441. }
  442. }
  443. return s;
  444. }
  445. // removes color elements from the text;
  446. function removeColors(text) {
  447. /*eslint no-control-regex: 0*/
  448. return text.replace(/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/g, '');
  449. }
  450. function padZeros(value, n) {
  451. let str = value.toString();
  452. while (str.length < n)
  453. str = '0' + str;
  454. return str;
  455. }
  456. // extracts tag name from a tag object/value;
  457. function getTagName(event) {
  458. let sTag;
  459. const tag = event.ctx.tag;
  460. if (tag) {
  461. switch (typeof tag) {
  462. case 'string':
  463. sTag = tag;
  464. break;
  465. case 'number':
  466. if (Number.isFinite(tag)) {
  467. sTag = tag.toString();
  468. }
  469. break;
  470. case 'object':
  471. // A tag-object must have its own method toString(), in order to be converted automatically;
  472. if (hasOwnProperty(tag, 'toString') && typeof tag.toString === 'function') {
  473. sTag = tag.toString();
  474. }
  475. break;
  476. default:
  477. break;
  478. }
  479. }
  480. return sTag;
  481. }
  482. ////////////////////////////////////////////
  483. // Simpler check for null/undefined;
  484. function isNull(value) {
  485. return value === null || value === undefined;
  486. }
  487. ///////////////////////////////////////////////////////////////
  488. // Adds support for BigInt, to be rendered like in JavaScript,
  489. // as an open value, with 'n' in the end.
  490. function toJson(data) {
  491. if (data !== undefined) {
  492. return JSON.stringify(data, (_, v) => typeof v === 'bigint' ? `${v}#bigint` : v)
  493. .replace(/"(-?\d+)#bigint"/g, (_, a) => a + 'n');
  494. }
  495. }
  496. // reusable error messages;
  497. const errors = {
  498. redirectParams(event) {
  499. return 'Invalid event \'' + event + '\' redirect parameters.';
  500. }
  501. };
  502. // 9 spaces for the time offset:
  503. const timeGap = ' '.repeat(9);
  504. module.exports = monitor;