123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- /*
- * 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 {TableName} = require('./table-name');
- const {Column} = require('./column');
- const npm = {
- os: require('os'),
- utils: require('../utils'),
- formatting: require('../formatting')
- };
- /**
- * @class helpers.ColumnSet
- * @description
- * Performance-optimized, read-only structure with query-formatting columns.
- *
- * In order to avail from performance optimization provided by this class, it should be created
- * only once, statically, and then reused.
- *
- * @param {object|helpers.Column|array} columns
- * Columns information object, depending on the type:
- *
- * - When it is a simple object, its properties are enumerated to represent both column names and property names
- * within the source objects. See also option `inherit` that's applicable in this case.
- *
- * - When it is a single {@link helpers.Column Column} object, property {@link helpers.ColumnSet#columns columns} is initialized with
- * just a single column. It is not a unique situation when only a single column is required for an update operation.
- *
- * - When it is an array, each element is assumed to represent details for a column. If the element is already of type {@link helpers.Column Column},
- * it is used directly; otherwise the element is passed into {@link helpers.Column Column} constructor for initialization.
- * On any duplicate column name (case-sensitive) it will throw {@link external:Error Error} = `Duplicate column name "name".`
- *
- * - When it is none of the above, it will throw {@link external:TypeError TypeError} = `Invalid parameter 'columns' specified.`
- *
- * @param {object} [options]
- *
- * @param {helpers.TableName|string|{table,schema}} [options.table]
- * Table details.
- *
- * When it is a non-null value, and not a {@link helpers.TableName TableName} object, a new {@link helpers.TableName TableName} is constructed from the value.
- *
- * It can be used as the default for methods {@link helpers.insert insert} and {@link helpers.update update} when their parameter
- * `table` is omitted, and for logging purposes.
- *
- * @param {boolean} [options.inherit = false]
- * Use inherited properties in addition to the object's own properties.
- *
- * By default, only the object's own properties are enumerated for column names.
- *
- * @returns {helpers.ColumnSet}
- *
- * @see
- *
- * {@link helpers.ColumnSet#columns columns},
- * {@link helpers.ColumnSet#names names},
- * {@link helpers.ColumnSet#table table},
- * {@link helpers.ColumnSet#variables variables} |
- * {@link helpers.ColumnSet#assign assign},
- * {@link helpers.ColumnSet#assignColumns assignColumns},
- * {@link helpers.ColumnSet#extend extend},
- * {@link helpers.ColumnSet#merge merge},
- * {@link helpers.ColumnSet#prepare prepare}
- *
- * @example
- *
- * // A complex insert/update object scenario for table 'purchases' in schema 'fiscal'.
- * // For a good performance, you should declare such objects once and then reuse them.
- * //
- * // Column Requirements:
- * //
- * // 1. Property 'id' is only to be used for a WHERE condition in updates
- * // 2. Property 'list' needs to be formatted as a csv
- * // 3. Property 'code' is to be used as raw text, and to be defaulted to 0 when the
- * // property is missing in the source object
- * // 4. Property 'log' is a JSON object with 'log-entry' for the column name
- * // 5. Property 'data' requires SQL type casting '::int[]'
- * // 6. Property 'amount' needs to be set to 100, if it is 0
- * // 7. Property 'total' must be skipped during updates, if 'amount' was 0, plus its
- * // column name is 'total-val'
- *
- * const cs = new pgp.helpers.ColumnSet([
- * '?id', // ColumnConfig equivalent: {name: 'id', cnd: true}
- * 'list:csv', // ColumnConfig equivalent: {name: 'list', mod: ':csv'}
- * {
- * name: 'code',
- * mod: '^', // format as raw text
- * def: 0 // default to 0 when the property doesn't exist
- * },
- * {
- * name: 'log-entry',
- * prop: 'log',
- * mod: ':json' // format as JSON
- * },
- * {
- * name: 'data',
- * cast: 'int[]' // use SQL type casting '::int[]'
- * },
- * {
- * name: 'amount',
- * init(col) {
- * // set to 100, if the value is 0:
- * return col.value === 0 ? 100 : col.value;
- * }
- * },
- * {
- * name: 'total-val',
- * prop: 'total',
- * skip(col) {
- * // skip from updates, if 'amount' is 0:
- * return col.source.amount === 0;
- * }
- * }
- * ], {table: {table: 'purchases', schema: 'fiscal'}});
- *
- * // Alternatively, you could take the table declaration out:
- * // const table = new pgp.helpers.TableName('purchases', 'fiscal');
- *
- * console.log(cs); // console output for the object:
- * //=>
- * // ColumnSet {
- * // table: "fiscal"."purchases"
- * // columns: [
- * // Column {
- * // name: "id"
- * // cnd: true
- * // }
- * // Column {
- * // name: "list"
- * // mod: ":csv"
- * // }
- * // Column {
- * // name: "code"
- * // mod: "^"
- * // def: 0
- * // }
- * // Column {
- * // name: "log-entry"
- * // prop: "log"
- * // mod: ":json"
- * // }
- * // Column {
- * // name: "data"
- * // cast: "int[]"
- * // }
- * // Column {
- * // name: "amount"
- * // init: [Function]
- * // }
- * // Column {
- * // name: "total-val"
- * // prop: "total"
- * // skip: [Function]
- * // }
- * // ]
- * // }
- */
- class ColumnSet extends InnerState {
- constructor(columns, opt) {
- super();
- if (!columns || typeof columns !== 'object') {
- throw new TypeError('Invalid parameter \'columns\' specified.');
- }
- opt = assert(opt, ['table', 'inherit']);
- if (!npm.utils.isNull(opt.table)) {
- this.table = (opt.table instanceof TableName) ? opt.table : new TableName(opt.table);
- }
- /**
- * @name helpers.ColumnSet#table
- * @type {helpers.TableName}
- * @readonly
- * @description
- * Destination table. It can be specified for two purposes:
- *
- * - **primary:** to be used as the default table when it is omitted during a call into methods {@link helpers.insert insert} and {@link helpers.update update}
- * - **secondary:** to be automatically written into the console (for logging purposes).
- */
- /**
- * @name helpers.ColumnSet#columns
- * @type helpers.Column[]
- * @readonly
- * @description
- * Array of {@link helpers.Column Column} objects.
- */
- if (Array.isArray(columns)) {
- const colNames = {};
- this.columns = columns.map(c => {
- const col = (c instanceof Column) ? c : new Column(c);
- if (col.name in colNames) {
- throw new Error(`Duplicate column name "${col.name}".`);
- }
- colNames[col.name] = true;
- return col;
- });
- } else {
- if (columns instanceof Column) {
- this.columns = [columns];
- } else {
- this.columns = [];
- for (const name in columns) {
- if (opt.inherit || Object.prototype.hasOwnProperty.call(columns, name)) {
- this.columns.push(new Column(name));
- }
- }
- }
- }
- Object.freeze(this.columns);
- Object.freeze(this);
- this.extendState({
- names: undefined,
- variables: undefined,
- updates: undefined,
- isSimple: true
- });
- for (let i = 0; i < this.columns.length; i++) {
- const c = this.columns[i];
- // ColumnSet is simple when the source objects require no preparation,
- // and should be used directly:
- if (c.prop || c.init || 'def' in c) {
- this._inner.isSimple = false;
- break;
- }
- }
- }
- /**
- * @name helpers.ColumnSet#names
- * @type string
- * @readonly
- * @description
- * Returns a string - comma-separated list of all column names, properly escaped.
- *
- * @example
- * const cs = new ColumnSet(['id^', {name: 'cells', cast: 'int[]'}, 'doc:json']);
- * console.log(cs.names);
- * //=> "id","cells","doc"
- */
- get names() {
- const _i = this._inner;
- if (!_i.names) {
- _i.names = this.columns.map(c => c.escapedName).join();
- }
- return _i.names;
- }
- /**
- * @name helpers.ColumnSet#variables
- * @type string
- * @readonly
- * @description
- * Returns a string - formatting template for all column values.
- *
- * @see {@link helpers.ColumnSet#assign assign}
- *
- * @example
- * const cs = new ColumnSet(['id^', {name: 'cells', cast: 'int[]'}, 'doc:json']);
- * console.log(cs.variables);
- * //=> ${id^},${cells}::int[],${doc:json}
- */
- get variables() {
- const _i = this._inner;
- if (!_i.variables) {
- _i.variables = this.columns.map(c => c.variable + c.castText).join();
- }
- return _i.variables;
- }
- }
- /**
- * @method helpers.ColumnSet#assign
- * @description
- * Returns a formatting template of SET assignments, either generic or for a single object.
- *
- * The method is optimized to cache the output string when there are no columns that can be skipped dynamically.
- *
- * This method is primarily for internal use, that's why it does not validate the input.
- *
- * @param {object} [options]
- * Assignment/formatting options.
- *
- * @param {object} [options.source]
- * Source - a single object that contains values for columns.
- *
- * The object is only necessary to correctly apply the logic of skipping columns dynamically, based on the source data
- * and the rules defined in the {@link helpers.ColumnSet ColumnSet}. If, however, you do not care about that, then you do not need to specify any object.
- *
- * Note that even if you do not specify the object, the columns marked as conditional (`cnd: true`) will always be skipped.
- *
- * @param {string} [options.prefix]
- * In cases where needed, an alias prefix to be added before each column.
- *
- * @returns {string}
- * Comma-separated list of variable-to-column assignments.
- *
- * @see {@link helpers.ColumnSet#variables variables}
- *
- * @example
- *
- * const cs = new pgp.helpers.ColumnSet([
- * '?first', // = {name: 'first', cnd: true}
- * 'second:json',
- * {name: 'third', mod: ':raw', cast: 'text'}
- * ]);
- *
- * cs.assign();
- * //=> "second"=${second:json},"third"=${third:raw}::text
- *
- * cs.assign({prefix: 'a b c'});
- * //=> "a b c"."second"=${second:json},"a b c"."third"=${third:raw}::text
- */
- ColumnSet.prototype.assign = function (options) {
- const _i = this._inner;
- const hasPrefix = options && options.prefix && typeof options.prefix === 'string';
- if (_i.updates && !hasPrefix) {
- return _i.updates;
- }
- let dynamic = hasPrefix;
- const hasSource = options && options.source && typeof options.source === 'object';
- let list = this.columns.filter(c => {
- if (c.cnd) {
- return false;
- }
- if (c.skip) {
- dynamic = true;
- if (hasSource) {
- const a = colDesc(c, options.source);
- if (c.skip.call(options.source, a)) {
- return false;
- }
- }
- }
- return true;
- });
- const prefix = hasPrefix ? npm.formatting.as.alias(options.prefix) + '.' : '';
- list = list.map(c => prefix + c.escapedName + '=' + c.variable + c.castText).join();
- if (!dynamic) {
- _i.updates = list;
- }
- return list;
- };
- /**
- * @method helpers.ColumnSet#assignColumns
- * @description
- * Generates assignments for all columns in the set, with support for aliases and column-skipping logic.
- * Aliases are set by using method {@link formatting.alias as.alias}.
- *
- * @param {{}} [options]
- * Optional Parameters.
- *
- * @param {string} [options.from]
- * Alias for the source columns.
- *
- * @param {string} [options.to]
- * Alias for the destination columns.
- *
- * @param {string | Array<string> | function} [options.skip]
- * Name(s) of the column(s) to be skipped (case-sensitive). It can be either a single string or an array of strings.
- *
- * It can also be a function - iterator, to be called for every column, passing in {@link helpers.Column Column} as
- * `this` context, and plus as a single parameter. The function would return a truthy value for every column that needs to be skipped.
- *
- * @returns {string}
- * A string of comma-separated column assignments.
- *
- * @example
- *
- * const cs = new pgp.helpers.ColumnSet(['id', 'city', 'street']);
- *
- * cs.assignColumns({from: 'EXCLUDED', skip: 'id'})
- * //=> "city"=EXCLUDED."city","street"=EXCLUDED."street"
- *
- * @example
- *
- * const cs = new pgp.helpers.ColumnSet(['?id', 'city', 'street']);
- *
- * cs.assignColumns({from: 'source', to: 'target', skip: c => c.cnd})
- * //=> target."city"=source."city",target."street"=source."street"
- *
- */
- ColumnSet.prototype.assignColumns = function (options) {
- options = assert(options, ['from', 'to', 'skip']);
- const skip = (typeof options.skip === 'string' && [options.skip]) || ((Array.isArray(options.skip) || typeof options.skip === 'function') && options.skip);
- const from = (typeof options.from === 'string' && options.from && (npm.formatting.as.alias(options.from) + '.')) || '';
- const to = (typeof options.to === 'string' && options.to && (npm.formatting.as.alias(options.to) + '.')) || '';
- const iterator = typeof skip === 'function' ? c => !skip.call(c, c) : c => skip.indexOf(c.name) === -1;
- const cols = skip ? this.columns.filter(iterator) : this.columns;
- return cols.map(c => to + c.escapedName + '=' + from + c.escapedName).join();
- };
- /**
- * @method helpers.ColumnSet#extend
- * @description
- * Creates a new {@link helpers.ColumnSet ColumnSet}, by joining the two sets of columns.
- *
- * If the two sets contain a column with the same `name` (case-sensitive), an error is thrown.
- *
- * @param {helpers.Column|helpers.ColumnSet|array} columns
- * Columns to be appended, of the same type as parameter `columns` during {@link helpers.ColumnSet ColumnSet} construction, except:
- * - it can also be of type {@link helpers.ColumnSet ColumnSet}
- * - it cannot be a simple object (properties enumeration is not supported here)
- *
- * @returns {helpers.ColumnSet}
- * New {@link helpers.ColumnSet ColumnSet} object with the extended/concatenated list of columns.
- *
- * @see
- * {@link helpers.Column Column},
- * {@link helpers.ColumnSet#merge merge}
- *
- * @example
- *
- * const pgp = require('pg-promise')();
- *
- * const cs = new pgp.helpers.ColumnSet(['one', 'two'], {table: 'my-table'});
- * console.log(cs);
- * //=>
- * // ColumnSet {
- * // table: "my-table"
- * // columns: [
- * // Column {
- * // name: "one"
- * // }
- * // Column {
- * // name: "two"
- * // }
- * // ]
- * // }
- * const csExtended = cs.extend(['three']);
- * console.log(csExtended);
- * //=>
- * // ColumnSet {
- * // table: "my-table"
- * // columns: [
- * // Column {
- * // name: "one"
- * // }
- * // Column {
- * // name: "two"
- * // }
- * // Column {
- * // name: "three"
- * // }
- * // ]
- * // }
- */
- ColumnSet.prototype.extend = function (columns) {
- let cs = columns;
- if (!(cs instanceof ColumnSet)) {
- cs = new ColumnSet(columns);
- }
- // Any duplicate column will throw Error = 'Duplicate column name "name".',
- return new ColumnSet(this.columns.concat(cs.columns), {table: this.table});
- };
- /**
- * @method helpers.ColumnSet#merge
- * @description
- * Creates a new {@link helpers.ColumnSet ColumnSet}, by joining the two sets of columns.
- *
- * Items in `columns` with the same `name` (case-sensitive) override the original columns.
- *
- * @param {helpers.Column|helpers.ColumnSet|array} columns
- * Columns to be appended, of the same type as parameter `columns` during {@link helpers.ColumnSet ColumnSet} construction, except:
- * - it can also be of type {@link helpers.ColumnSet ColumnSet}
- * - it cannot be a simple object (properties enumeration is not supported here)
- *
- * @see
- * {@link helpers.Column Column},
- * {@link helpers.ColumnSet#extend extend}
- *
- * @returns {helpers.ColumnSet}
- * New {@link helpers.ColumnSet ColumnSet} object with the merged list of columns.
- *
- * @example
- *
- * const pgp = require('pg-promise')();
- *
- * const cs = new pgp.helpers.ColumnSet(['?one', 'two:json'], {table: 'my-table'});
- * console.log(cs);
- * //=>
- * // ColumnSet {
- * // table: "my-table"
- * // columns: [
- * // Column {
- * // name: "one"
- * // cnd: true
- * // }
- * // Column {
- * // name: "two"
- * // mod: ":json"
- * // }
- * // ]
- * // }
- * const csMerged = cs.merge(['two', 'three^']);
- * console.log(csMerged);
- * //=>
- * // ColumnSet {
- * // table: "my-table"
- * // columns: [
- * // Column {
- * // name: "one"
- * // cnd: true
- * // }
- * // Column {
- * // name: "two"
- * // }
- * // Column {
- * // name: "three"
- * // mod: "^"
- * // }
- * // ]
- * // }
- *
- */
- ColumnSet.prototype.merge = function (columns) {
- let cs = columns;
- if (!(cs instanceof ColumnSet)) {
- cs = new ColumnSet(columns);
- }
- const colNames = {}, cols = [];
- this.columns.forEach((c, idx) => {
- cols.push(c);
- colNames[c.name] = idx;
- });
- cs.columns.forEach(c => {
- if (c.name in colNames) {
- cols[colNames[c.name]] = c;
- } else {
- cols.push(c);
- }
- });
- return new ColumnSet(cols, {table: this.table});
- };
- /**
- * @method helpers.ColumnSet#prepare
- * @description
- * Prepares a source object to be formatted, by cloning it and applying the rules as set by the
- * columns configuration.
- *
- * This method is primarily for internal use, that's why it does not validate the input parameters.
- *
- * @param {object} source
- * The source object to be prepared, if required.
- *
- * It must be a non-`null` object, which the method does not validate, as it is
- * intended primarily for internal use by the library.
- *
- * @returns {object}
- * When the object needs to be prepared, the method returns a clone of the source object,
- * with all properties and values set according to the columns configuration.
- *
- * When the object does not need to be prepared, the original object is returned.
- */
- ColumnSet.prototype.prepare = function (source) {
- if (this._inner.isSimple) {
- return source; // a simple ColumnSet requires no object preparation;
- }
- const target = {};
- this.columns.forEach(c => {
- const a = colDesc(c, source);
- if (c.init) {
- target[a.name] = c.init.call(source, a);
- } else {
- if (a.exists || 'def' in c) {
- target[a.name] = a.value;
- }
- }
- });
- return target;
- };
- function colDesc(column, source) {
- const a = {
- source,
- name: column.prop || column.name
- };
- a.exists = a.name in source;
- if (a.exists) {
- a.value = source[a.name];
- } else {
- a.value = 'def' in column ? column.def : undefined;
- }
- return a;
- }
- /**
- * @method helpers.ColumnSet#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}
- */
- ColumnSet.prototype.toString = function (level) {
- level = level > 0 ? parseInt(level) : 0;
- const gap0 = npm.utils.messageGap(level),
- gap1 = npm.utils.messageGap(level + 1),
- lines = [
- 'ColumnSet {'
- ];
- if (this.table) {
- lines.push(gap1 + 'table: ' + this.table);
- }
- if (this.columns.length) {
- lines.push(gap1 + 'columns: [');
- this.columns.forEach(c => {
- lines.push(c.toString(2));
- });
- lines.push(gap1 + ']');
- } else {
- lines.push(gap1 + 'columns: []');
- }
- lines.push(gap0 + '}');
- return lines.join(npm.os.EOL);
- };
- npm.utils.addInspection(ColumnSet, function () {
- return this.toString();
- });
- module.exports = {ColumnSet};
|