column.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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 {InnerState} = require('../inner-state');
  10. const {assert} = require('../assert');
  11. const npm = {
  12. os: require('os'),
  13. utils: require('../utils'),
  14. formatting: require('../formatting'),
  15. patterns: require('../patterns')
  16. };
  17. /**
  18. *
  19. * @class helpers.Column
  20. * @description
  21. *
  22. * Read-only structure with details for a single column. Used primarily by {@link helpers.ColumnSet ColumnSet}.
  23. *
  24. * The class parses details into a template, to be used for query generation.
  25. *
  26. * @param {string|helpers.ColumnConfig} col
  27. * Column details, depending on the type.
  28. *
  29. * When it is a string, it is expected to contain a name for both the column and the source property, assuming that the two are the same.
  30. * The name must adhere to JavaScript syntax for variable names. The name can be appended with any format modifier as supported by
  31. * {@link formatting.format as.format} (`^`, `~`, `#`, `:csv`, `:list`, `:json`, `:alias`, `:name`, `:raw`, `:value`), which is then removed from the name and put
  32. * into property `mod`. If the name starts with `?`, it is removed, while setting flag `cnd` = `true`.
  33. *
  34. * If the string doesn't adhere to the above requirements, the method will throw {@link external:TypeError TypeError} = `Invalid column syntax`.
  35. *
  36. * When `col` is a simple {@link helpers.ColumnConfig ColumnConfig}-like object, it is used as an input configurator to set all the properties
  37. * of the class.
  38. *
  39. * @property {string} name
  40. * Destination column name + source property name (if `prop` is skipped). The name must adhere to JavaScript syntax for variables,
  41. * unless `prop` is specified, in which case `name` represents only the column name, and therefore can be any non-empty string.
  42. *
  43. * @property {string} [prop]
  44. * Source property name, if different from the column's name. It must adhere to JavaScript syntax for variables.
  45. *
  46. * It is ignored when it is the same as `name`.
  47. *
  48. * @property {string} [mod]
  49. * Formatting modifier, as supported by method {@link formatting.format as.format}: `^`, `~`, `#`, `:csv`, `:list`, `:json`, `:alias`, `:name`, `:raw`, `:value`.
  50. *
  51. * @property {string} [cast]
  52. * Server-side type casting, without `::` in front.
  53. *
  54. * @property {boolean} [cnd]
  55. * Conditional column flag.
  56. *
  57. * Used by methods {@link helpers.update update} and {@link helpers.sets sets}, ignored by methods {@link helpers.insert insert} and
  58. * {@link helpers.values values}. It indicates that the column is reserved for a `WHERE` condition, not to be set or updated.
  59. *
  60. * It can be set from a string initialization, by adding `?` in front of the name.
  61. *
  62. * @property {*} [def]
  63. * Default value for the property, to be used only when the source object doesn't have the property.
  64. * It is ignored when property `init` is set.
  65. *
  66. * @property {helpers.initCB} [init]
  67. * Override callback for the value.
  68. *
  69. * @property {helpers.skipCB} [skip]
  70. * An override for skipping columns dynamically.
  71. *
  72. * Used by methods {@link helpers.update update} (for a single object) and {@link helpers.sets sets}, ignored by methods
  73. * {@link helpers.insert insert} and {@link helpers.values values}.
  74. *
  75. * It is also ignored when conditional flag `cnd` is set.
  76. *
  77. * @returns {helpers.Column}
  78. *
  79. * @see
  80. * {@link helpers.ColumnConfig ColumnConfig},
  81. * {@link helpers.Column#castText castText},
  82. * {@link helpers.Column#escapedName escapedName},
  83. * {@link helpers.Column#variable variable}
  84. *
  85. * @example
  86. *
  87. * const pgp = require('pg-promise')({
  88. * capSQL: true // if you want all generated SQL capitalized
  89. * });
  90. *
  91. * const Column = pgp.helpers.Column;
  92. *
  93. * // creating a column from just a name:
  94. * const col1 = new Column('colName');
  95. * console.log(col1);
  96. * //=>
  97. * // Column {
  98. * // name: "colName"
  99. * // }
  100. *
  101. * // creating a column from a name + modifier:
  102. * const col2 = new Column('colName:csv');
  103. * console.log(col2);
  104. * //=>
  105. * // Column {
  106. * // name: "colName"
  107. * // mod: ":csv"
  108. * // }
  109. *
  110. * // creating a column from a configurator:
  111. * const col3 = new Column({
  112. * name: 'colName', // required
  113. * prop: 'propName', // optional
  114. * mod: '^', // optional
  115. * def: 123 // optional
  116. * });
  117. * console.log(col3);
  118. * //=>
  119. * // Column {
  120. * // name: "colName"
  121. * // prop: "propName"
  122. * // mod: "^"
  123. * // def: 123
  124. * // }
  125. *
  126. */
  127. class Column extends InnerState {
  128. constructor(col) {
  129. super();
  130. if (typeof col === 'string') {
  131. const info = parseColumn(col);
  132. this.name = info.name;
  133. if ('mod' in info) {
  134. this.mod = info.mod;
  135. }
  136. if ('cnd' in info) {
  137. this.cnd = info.cnd;
  138. }
  139. } else {
  140. col = assert(col, ['name', 'prop', 'mod', 'cast', 'cnd', 'def', 'init', 'skip']);
  141. if ('name' in col) {
  142. if (!npm.utils.isText(col.name)) {
  143. throw new TypeError(`Invalid 'name' value: ${npm.utils.toJson(col.name)}. A non-empty string was expected.`);
  144. }
  145. if (npm.utils.isNull(col.prop) && !isValidVariable(col.name)) {
  146. throw new TypeError(`Invalid 'name' syntax: ${npm.utils.toJson(col.name)}.`);
  147. }
  148. this.name = col.name; // column name + property name (if 'prop' isn't specified)
  149. if (!npm.utils.isNull(col.prop)) {
  150. if (!npm.utils.isText(col.prop)) {
  151. throw new TypeError(`Invalid 'prop' value: ${npm.utils.toJson(col.prop)}. A non-empty string was expected.`);
  152. }
  153. if (!isValidVariable(col.prop)) {
  154. throw new TypeError(`Invalid 'prop' syntax: ${npm.utils.toJson(col.prop)}.`);
  155. }
  156. if (col.prop !== col.name) {
  157. // optional property name, if different from the column's name;
  158. this.prop = col.prop;
  159. }
  160. }
  161. if (!npm.utils.isNull(col.mod)) {
  162. if (typeof col.mod !== 'string' || !isValidMod(col.mod)) {
  163. throw new TypeError(`Invalid 'mod' value: ${npm.utils.toJson(col.mod)}.`);
  164. }
  165. this.mod = col.mod; // optional format modifier;
  166. }
  167. if (!npm.utils.isNull(col.cast)) {
  168. this.cast = parseCast(col.cast); // optional SQL type casting
  169. }
  170. if ('cnd' in col) {
  171. this.cnd = !!col.cnd;
  172. }
  173. if ('def' in col) {
  174. this.def = col.def; // optional default
  175. }
  176. if (typeof col.init === 'function') {
  177. this.init = col.init; // optional value override (overrides 'def' also)
  178. }
  179. if (typeof col.skip === 'function') {
  180. this.skip = col.skip;
  181. }
  182. } else {
  183. throw new TypeError('Invalid column details.');
  184. }
  185. }
  186. const variable = '${' + (this.prop || this.name) + (this.mod || '') + '}';
  187. const castText = this.cast ? ('::' + this.cast) : '';
  188. const escapedName = npm.formatting.as.name(this.name);
  189. this.extendState({variable, castText, escapedName});
  190. Object.freeze(this);
  191. }
  192. /**
  193. * @name helpers.Column#variable
  194. * @type string
  195. * @readonly
  196. * @description
  197. * Full-syntax formatting variable, ready for direct use in query templates.
  198. *
  199. * @example
  200. *
  201. * const cs = new pgp.helpers.ColumnSet([
  202. * 'id',
  203. * 'coordinate:json',
  204. * {
  205. * name: 'places',
  206. * mod: ':csv',
  207. * cast: 'int[]'
  208. * }
  209. * ]);
  210. *
  211. * // cs.columns[0].variable = ${id}
  212. * // cs.columns[1].variable = ${coordinate:json}
  213. * // cs.columns[2].variable = ${places:csv}::int[]
  214. */
  215. get variable() {
  216. return this._inner.variable;
  217. }
  218. /**
  219. * @name helpers.Column#castText
  220. * @type string
  221. * @readonly
  222. * @description
  223. * Full-syntax sql type casting, if there is any, or else an empty string.
  224. */
  225. get castText() {
  226. return this._inner.castText;
  227. }
  228. /**
  229. * @name helpers.Column#escapedName
  230. * @type string
  231. * @readonly
  232. * @description
  233. * Escaped name of the column, ready to be injected into queries directly.
  234. *
  235. */
  236. get escapedName() {
  237. return this._inner.escapedName;
  238. }
  239. }
  240. function parseCast(name) {
  241. if (typeof name === 'string') {
  242. const s = name.replace(/^[:\s]*|\s*$/g, '');
  243. if (s) {
  244. return s;
  245. }
  246. }
  247. throw new TypeError(`Invalid 'cast' value: ${npm.utils.toJson(name)}.`);
  248. }
  249. function parseColumn(name) {
  250. const m = name.match(npm.patterns.validColumn);
  251. if (m && m[0] === name) {
  252. const res = {};
  253. if (name[0] === '?') {
  254. res.cnd = true;
  255. name = name.substr(1);
  256. }
  257. const mod = name.match(npm.patterns.hasValidModifier);
  258. if (mod) {
  259. res.name = name.substr(0, mod.index);
  260. res.mod = mod[0];
  261. } else {
  262. res.name = name;
  263. }
  264. return res;
  265. }
  266. throw new TypeError(`Invalid column syntax: ${npm.utils.toJson(name)}.`);
  267. }
  268. function isValidMod(mod) {
  269. return npm.patterns.validModifiers.indexOf(mod) !== -1;
  270. }
  271. function isValidVariable(name) {
  272. const m = name.match(npm.patterns.validVariable);
  273. return !!m && m[0] === name;
  274. }
  275. /**
  276. * @method helpers.Column#toString
  277. * @description
  278. * Creates a well-formatted multi-line string that represents the object.
  279. *
  280. * It is called automatically when writing the object into the console.
  281. *
  282. * @param {number} [level=0]
  283. * Nested output level, to provide visual offset.
  284. *
  285. * @returns {string}
  286. */
  287. Column.prototype.toString = function (level) {
  288. level = level > 0 ? parseInt(level) : 0;
  289. const gap0 = npm.utils.messageGap(level),
  290. gap1 = npm.utils.messageGap(level + 1),
  291. lines = [
  292. gap0 + 'Column {',
  293. gap1 + 'name: ' + npm.utils.toJson(this.name)
  294. ];
  295. if ('prop' in this) {
  296. lines.push(gap1 + 'prop: ' + npm.utils.toJson(this.prop));
  297. }
  298. if ('mod' in this) {
  299. lines.push(gap1 + 'mod: ' + npm.utils.toJson(this.mod));
  300. }
  301. if ('cast' in this) {
  302. lines.push(gap1 + 'cast: ' + npm.utils.toJson(this.cast));
  303. }
  304. if ('cnd' in this) {
  305. lines.push(gap1 + 'cnd: ' + npm.utils.toJson(this.cnd));
  306. }
  307. if ('def' in this) {
  308. lines.push(gap1 + 'def: ' + npm.utils.toJson(this.def));
  309. }
  310. if ('init' in this) {
  311. lines.push(gap1 + 'init: [Function]');
  312. }
  313. if ('skip' in this) {
  314. lines.push(gap1 + 'skip: [Function]');
  315. }
  316. lines.push(gap0 + '}');
  317. return lines.join(npm.os.EOL);
  318. };
  319. npm.utils.addInspection(Column, function () {
  320. return this.toString();
  321. });
  322. /**
  323. * @typedef helpers.ColumnConfig
  324. * @description
  325. * A simple structure with column details, to be passed into the {@link helpers.Column Column} constructor for initialization.
  326. *
  327. * @property {string} name
  328. * Destination column name + source property name (if `prop` is skipped). The name must adhere to JavaScript syntax for variables,
  329. * unless `prop` is specified, in which case `name` represents only the column name, and therefore can be any non-empty string.
  330. *
  331. * @property {string} [prop]
  332. * Source property name, if different from the column's name. It must adhere to JavaScript syntax for variables.
  333. *
  334. * It is ignored when it is the same as `name`.
  335. *
  336. * @property {string} [mod]
  337. * Formatting modifier, as supported by method {@link formatting.format as.format}: `^`, `~`, `#`, `:csv`, `:list`, `:json`, `:alias`, `:name`, `:raw`, `:value`.
  338. *
  339. * @property {string} [cast]
  340. * Server-side type casting. Leading `::` is allowed, but not needed (automatically removed when specified).
  341. *
  342. * @property {boolean} [cnd]
  343. * Conditional column flag.
  344. *
  345. * Used by methods {@link helpers.update update} and {@link helpers.sets sets}, ignored by methods {@link helpers.insert insert} and
  346. * {@link helpers.values values}. It indicates that the column is reserved for a `WHERE` condition, not to be set or updated.
  347. *
  348. * It can be set from a string initialization, by adding `?` in front of the name.
  349. *
  350. * @property {*} [def]
  351. * Default value for the property, to be used only when the source object doesn't have the property.
  352. * It is ignored when property `init` is set.
  353. *
  354. * @property {helpers.initCB} [init]
  355. * Override callback for the value.
  356. *
  357. * @property {helpers.skipCB} [skip]
  358. * An override for skipping columns dynamically.
  359. *
  360. * Used by methods {@link helpers.update update} (for a single object) and {@link helpers.sets sets}, ignored by methods
  361. * {@link helpers.insert insert} and {@link helpers.values values}.
  362. *
  363. * It is also ignored when conditional flag `cnd` is set.
  364. *
  365. */
  366. /**
  367. * @callback helpers.initCB
  368. * @description
  369. * A callback function type used by parameter `init` within {@link helpers.ColumnConfig ColumnConfig}.
  370. *
  371. * It works as an override for the corresponding property value in the `source` object.
  372. *
  373. * The function is called with `this` set to the `source` object.
  374. *
  375. * @param {*} col
  376. * Column-to-property descriptor.
  377. *
  378. * @param {object} col.source
  379. * The source object, equals to `this` that's passed into the function.
  380. *
  381. * @param {string} col.name
  382. * Resolved name of the property within the `source` object, i.e. the value of `name` when `prop` is not used
  383. * for the column, or the value of `prop` when it is specified.
  384. *
  385. * @param {*} col.value
  386. *
  387. * Property value, set to one of the following:
  388. *
  389. * - Value of the property within the `source` object (`value` = `source[name]`), if the property exists
  390. * - If the property doesn't exist and `def` is set in the column, then `value` is set to the value of `def`
  391. * - If the property doesn't exist and `def` is not set in the column, then `value` is set to `undefined`
  392. *
  393. * @param {boolean} col.exists
  394. * Indicates whether the property exists in the `source` object (`exists = name in source`).
  395. *
  396. * @returns {*}
  397. * The new value to be used for the corresponding column.
  398. */
  399. /**
  400. * @callback helpers.skipCB
  401. * @description
  402. * A callback function type used by parameter `skip` within {@link helpers.ColumnConfig ColumnConfig}.
  403. *
  404. * It is to dynamically determine when the property with specified `name` in the `source` object is to be skipped.
  405. *
  406. * The function is called with `this` set to the `source` object.
  407. *
  408. * @param {*} col
  409. * Column-to-property descriptor.
  410. *
  411. * @param {object} col.source
  412. * The source object, equals to `this` that's passed into the function.
  413. *
  414. * @param {string} col.name
  415. * Resolved name of the property within the `source` object, i.e. the value of `name` when `prop` is not used
  416. * for the column, or the value of `prop` when it is specified.
  417. *
  418. * @param {*} col.value
  419. *
  420. * Property value, set to one of the following:
  421. *
  422. * - Value of the property within the `source` object (`value` = `source[name]`), if the property exists
  423. * - If the property doesn't exist and `def` is set in the column, then `value` is set to the value of `def`
  424. * - If the property doesn't exist and `def` is not set in the column, then `value` is set to `undefined`
  425. *
  426. * @param {boolean} col.exists
  427. * Indicates whether the property exists in the `source` object (`exists = name in source`).
  428. *
  429. * @returns {boolean}
  430. * A truthy value that indicates whether the column is to be skipped.
  431. *
  432. */
  433. module.exports = {Column};