/* * 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 {InnerState} = require(`../inner-state`); const {assertOptions} = require(`assert-options`); const npm = { os: require(`os`), utils: require(`../utils`), formatting: require(`../formatting`), patterns: require(`../patterns`) }; /** * * @class helpers.Column * @description * * Read-only structure with details for a single column. Used primarily by {@link helpers.ColumnSet ColumnSet}. * * The class parses details into a template, to be used for query generation. * * @param {string|helpers.ColumnConfig} col * Column details, depending on the type. * * 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. * The name must adhere to JavaScript syntax for variable names. The name can be appended with any format modifier as supported by * {@link formatting.format as.format} (`^`, `~`, `#`, `:csv`, `:list`, `:json`, `:alias`, `:name`, `:raw`, `:value`), which is then removed from the name and put * into property `mod`. If the name starts with `?`, it is removed, while setting flag `cnd` = `true`. * * If the string doesn't adhere to the above requirements, the method will throw {@link external:TypeError TypeError} = `Invalid column syntax`. * * When `col` is a simple {@link helpers.ColumnConfig ColumnConfig}-like object, it is used as an input configurator to set all the properties * of the class. * * @property {string} name * Destination column name + source property name (if `prop` is skipped). The name must adhere to JavaScript syntax for variables, * unless `prop` is specified, in which case `name` represents only the column name, and therefore can be any non-empty string. * * @property {string} [prop] * Source property name, if different from the column's name. It must adhere to JavaScript syntax for variables. * * It is ignored when it is the same as `name`. * * @property {string} [mod] * Formatting modifier, as supported by method {@link formatting.format as.format}: `^`, `~`, `#`, `:csv`, `:list`, `:json`, `:alias`, `:name`, `:raw`, `:value`. * * @property {string} [cast] * Server-side type casting, without `::` in front. * * @property {boolean} [cnd] * Conditional column flag. * * Used by methods {@link helpers.update update} and {@link helpers.sets sets}, ignored by methods {@link helpers.insert insert} and * {@link helpers.values values}. It indicates that the column is reserved for a `WHERE` condition, not to be set or updated. * * It can be set from a string initialization, by adding `?` in front of the name. * * @property {*} [def] * Default value for the property, to be used only when the source object doesn't have the property. * It is ignored when property `init` is set. * * @property {helpers.initCB} [init] * Override callback for the value. * * @property {helpers.skipCB} [skip] * An override for skipping columns dynamically. * * Used by methods {@link helpers.update update} (for a single object) and {@link helpers.sets sets}, ignored by methods * {@link helpers.insert insert} and {@link helpers.values values}. * * It is also ignored when conditional flag `cnd` is set. * * @returns {helpers.Column} * * @see * {@link helpers.ColumnConfig ColumnConfig}, * {@link helpers.Column#castText castText}, * {@link helpers.Column#escapedName escapedName}, * {@link helpers.Column#variable variable} * * @example * * const pgp = require('pg-promise')({ * capSQL: true // if you want all generated SQL capitalized * }); * * const Column = pgp.helpers.Column; * * // creating a column from just a name: * const col1 = new Column('colName'); * console.log(col1); * //=> * // Column { * // name: "colName" * // } * * // creating a column from a name + modifier: * const col2 = new Column('colName:csv'); * console.log(col2); * //=> * // Column { * // name: "colName" * // mod: ":csv" * // } * * // creating a column from a configurator: * const col3 = new Column({ * name: 'colName', // required * prop: 'propName', // optional * mod: '^', // optional * def: 123 // optional * }); * console.log(col3); * //=> * // Column { * // name: "colName" * // prop: "propName" * // mod: "^" * // def: 123 * // } * */ class Column extends InnerState { constructor(col) { super(); if (typeof col === `string`) { const info = parseColumn(col); this.name = info.name; if (`mod` in info) { this.mod = info.mod; } if (`cnd` in info) { this.cnd = info.cnd; } } else { col = assertOptions(col, [`name`, `prop`, `mod`, `cast`, `cnd`, `def`, `init`, `skip`]); if (`name` in col) { if (!npm.utils.isText(col.name)) { throw new TypeError(`Invalid 'name' value: ${npm.utils.toJson(col.name)}. A non-empty string was expected.`); } if (npm.utils.isNull(col.prop) && !isValidVariable(col.name)) { throw new TypeError(`Invalid 'name' syntax: ${npm.utils.toJson(col.name)}.`); } this.name = col.name; // column name + property name (if 'prop' isn't specified) if (!npm.utils.isNull(col.prop)) { if (!npm.utils.isText(col.prop)) { throw new TypeError(`Invalid 'prop' value: ${npm.utils.toJson(col.prop)}. A non-empty string was expected.`); } if (!isValidVariable(col.prop)) { throw new TypeError(`Invalid 'prop' syntax: ${npm.utils.toJson(col.prop)}.`); } if (col.prop !== col.name) { // optional property name, if different from the column's name; this.prop = col.prop; } } if (!npm.utils.isNull(col.mod)) { if (typeof col.mod !== `string` || !isValidMod(col.mod)) { throw new TypeError(`Invalid 'mod' value: ${npm.utils.toJson(col.mod)}.`); } this.mod = col.mod; // optional format modifier; } if (!npm.utils.isNull(col.cast)) { this.cast = parseCast(col.cast); // optional SQL type casting } if (`cnd` in col) { this.cnd = !!col.cnd; } if (`def` in col) { this.def = col.def; // optional default } if (typeof col.init === `function`) { this.init = col.init; // optional value override (overrides 'def' also) } if (typeof col.skip === `function`) { this.skip = col.skip; } } else { throw new TypeError(`Invalid column details.`); } } const variable = `\${` + (this.prop || this.name) + (this.mod || ``) + `}`; const castText = this.cast ? (`::` + this.cast) : ``; const escapedName = npm.formatting.as.name(this.name); this.extendState({variable, castText, escapedName}); Object.freeze(this); } /** * @name helpers.Column#variable * @type string * @readonly * @description * Full-syntax formatting variable, ready for direct use in query templates. * * @example * * const cs = new pgp.helpers.ColumnSet([ * 'id', * 'coordinate:json', * { * name: 'places', * mod: ':csv', * cast: 'int[]' * } * ]); * * // cs.columns[0].variable = ${id} * // cs.columns[1].variable = ${coordinate:json} * // cs.columns[2].variable = ${places:csv}::int[] */ get variable() { return this._inner.variable; } /** * @name helpers.Column#castText * @type string * @readonly * @description * Full-syntax sql type casting, if there is any, or else an empty string. */ get castText() { return this._inner.castText; } /** * @name helpers.Column#escapedName * @type string * @readonly * @description * Escaped name of the column, ready to be injected into queries directly. * */ get escapedName() { return this._inner.escapedName; } } function parseCast(name) { if (typeof name === `string`) { const s = name.replace(/^[:\s]*|\s*$/g, ``); if (s) { return s; } } throw new TypeError(`Invalid 'cast' value: ${npm.utils.toJson(name)}.`); } function parseColumn(name) { const m = name.match(npm.patterns.validColumn); if (m && m[0] === name) { const res = {}; if (name[0] === `?`) { res.cnd = true; name = name.substr(1); } const mod = name.match(npm.patterns.hasValidModifier); if (mod) { res.name = name.substr(0, mod.index); res.mod = mod[0]; } else { res.name = name; } return res; } throw new TypeError(`Invalid column syntax: ${npm.utils.toJson(name)}.`); } function isValidMod(mod) { return npm.patterns.validModifiers.indexOf(mod) !== -1; } function isValidVariable(name) { const m = name.match(npm.patterns.validVariable); return !!m && m[0] === name; } /** * @method helpers.Column#toString * @description * Creates a well-formatted multi-line string that represents the object. * * It is called automatically when writing the object into the console. * * @param {number} [level=0] * Nested output level, to provide visual offset. * * @returns {string} */ Column.prototype.toString = function (level) { level = level > 0 ? parseInt(level) : 0; const gap0 = npm.utils.messageGap(level), gap1 = npm.utils.messageGap(level + 1), lines = [ gap0 + `Column {`, gap1 + `name: ` + npm.utils.toJson(this.name) ]; if (`prop` in this) { lines.push(gap1 + `prop: ` + npm.utils.toJson(this.prop)); } if (`mod` in this) { lines.push(gap1 + `mod: ` + npm.utils.toJson(this.mod)); } if (`cast` in this) { lines.push(gap1 + `cast: ` + npm.utils.toJson(this.cast)); } if (`cnd` in this) { lines.push(gap1 + `cnd: ` + npm.utils.toJson(this.cnd)); } if (`def` in this) { lines.push(gap1 + `def: ` + npm.utils.toJson(this.def)); } if (`init` in this) { lines.push(gap1 + `init: [Function]`); } if (`skip` in this) { lines.push(gap1 + `skip: [Function]`); } lines.push(gap0 + `}`); return lines.join(npm.os.EOL); }; npm.utils.addInspection(Column, function () { return this.toString(); }); /** * @typedef helpers.ColumnConfig * @description * A simple structure with column details, to be passed into the {@link helpers.Column Column} constructor for initialization. * * @property {string} name * Destination column name + source property name (if `prop` is skipped). The name must adhere to JavaScript syntax for variables, * unless `prop` is specified, in which case `name` represents only the column name, and therefore can be any non-empty string. * * @property {string} [prop] * Source property name, if different from the column's name. It must adhere to JavaScript syntax for variables. * * It is ignored when it is the same as `name`. * * @property {string} [mod] * Formatting modifier, as supported by method {@link formatting.format as.format}: `^`, `~`, `#`, `:csv`, `:list`, `:json`, `:alias`, `:name`, `:raw`, `:value`. * * @property {string} [cast] * Server-side type casting. Leading `::` is allowed, but not needed (automatically removed when specified). * * @property {boolean} [cnd] * Conditional column flag. * * Used by methods {@link helpers.update update} and {@link helpers.sets sets}, ignored by methods {@link helpers.insert insert} and * {@link helpers.values values}. It indicates that the column is reserved for a `WHERE` condition, not to be set or updated. * * It can be set from a string initialization, by adding `?` in front of the name. * * @property {*} [def] * Default value for the property, to be used only when the source object doesn't have the property. * It is ignored when property `init` is set. * * @property {helpers.initCB} [init] * Override callback for the value. * * @property {helpers.skipCB} [skip] * An override for skipping columns dynamically. * * Used by methods {@link helpers.update update} (for a single object) and {@link helpers.sets sets}, ignored by methods * {@link helpers.insert insert} and {@link helpers.values values}. * * It is also ignored when conditional flag `cnd` is set. * */ /** * @callback helpers.initCB * @description * A callback function type used by parameter `init` within {@link helpers.ColumnConfig ColumnConfig}. * * It works as an override for the corresponding property value in the `source` object. * * The function is called with `this` set to the `source` object. * * @param {*} col * Column-to-property descriptor. * * @param {object} col.source * The source object, equals to `this` that's passed into the function. * * @param {string} col.name * Resolved name of the property within the `source` object, i.e. the value of `name` when `prop` is not used * for the column, or the value of `prop` when it is specified. * * @param {*} col.value * * Property value, set to one of the following: * * - Value of the property within the `source` object (`value` = `source[name]`), if the property exists * - If the property doesn't exist and `def` is set in the column, then `value` is set to the value of `def` * - If the property doesn't exist and `def` is not set in the column, then `value` is set to `undefined` * * @param {boolean} col.exists * Indicates whether the property exists in the `source` object (`exists = name in source`). * * @returns {*} * The new value to be used for the corresponding column. */ /** * @callback helpers.skipCB * @description * A callback function type used by parameter `skip` within {@link helpers.ColumnConfig ColumnConfig}. * * It is to dynamically determine when the property with specified `name` in the `source` object is to be skipped. * * The function is called with `this` set to the `source` object. * * @param {*} col * Column-to-property descriptor. * * @param {object} col.source * The source object, equals to `this` that's passed into the function. * * @param {string} col.name * Resolved name of the property within the `source` object, i.e. the value of `name` when `prop` is not used * for the column, or the value of `prop` when it is specified. * * @param {*} col.value * * Property value, set to one of the following: * * - Value of the property within the `source` object (`value` = `source[name]`), if the property exists * - If the property doesn't exist and `def` is set in the column, then `value` is set to the value of `def` * - If the property doesn't exist and `def` is not set in the column, then `value` is set to `undefined` * * @param {boolean} col.exists * Indicates whether the property exists in the `source` object (`exists = name in source`). * * @returns {boolean} * A truthy value that indicates whether the column is to be skipped. * */ module.exports = {Column};