| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 | /* * 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 {assert} = require('../assert');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 = assert(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};
 |