UNPKG

@kitmi/data

Version:

Jacaranda Framework Data Access Model

1,012 lines 50.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { buildDataSetQuery: function() { return buildDataSetQuery; }, default: function() { return _default; } }); const _numbertoletter = /*#__PURE__*/ _interop_require_default(require("number-to-letter")); const _utils = require("@kitmi/utils"); const _types = require("@kitmi/types"); const _Connector = /*#__PURE__*/ _interop_require_default(require("../../Connector")); const _helpers = require("../../helpers"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const buildDataSetQuery = (opOptions, dataSet, aliasMap)=>{ let Entity = opOptions.$entity; if (Entity == null) { throw new _types.ApplicationError('Entity instance should be prepared in the operation options for delayed DataSet.', { dataSet }); } if (Entity.meta.name !== dataSet.model) { Entity = Entity.db.entity(dataSet.model); } return Entity.findSql({ ...dataSet.query, $aliasMap: aliasMap }); }; class RelationalConnector extends _Connector.default { get specParamToken() { return `$?`; } /** * Run aggregate pipeline * @param {string} model * @param {array} pipeline * @param {object} [options] * @returns {*} */ async aggregate_(model, pipeline, options, connection) { if (!Array.isArray(pipeline) || pipeline.length === 0) { throw new _types.InvalidArgument('"pipeline" should be an unempty array.'); } const [startingQuery, ..._pipeline] = pipeline; let query = this.buildQuery(model, startingQuery); _pipeline.forEach((stage, i)=>{ let _params = query.params; query = this.buildQuery({ sql: query.sql, alias: `_STAGE_${i}` }, stage); query.params = _params.concat(query.params); }); return this._executeQuery_(query, options, connection); } /** * Build sql statement. * @param {*} model * @param {*} condition */ buildQuery(model, opOptions) { let { $assoc, $select, $where, $groupBy, $orderBy, $countBy, $offset, $limit, $hasSubQuery, $aliasMap, $skipOrm } = opOptions; const { fromTable, withTables, model: _model } = this._buildCTEHeader(model); model = _model; let startId = $aliasMap ? Object.keys($aliasMap).length : 0; const mainAlias = (0, _numbertoletter.default)(startId++); const aliasMap = { ...$aliasMap && (0, _utils.prefixKeys)($aliasMap, '_.'), [model]: mainAlias }; let joinings; let mainEntityForJoin; const joiningParams = []; let directJoinings; let needDistinctForLimit = false; // build alias map first // cache params if ($assoc) { mainEntityForJoin = model; let nextId; [joinings, directJoinings, nextId] = this._joinAssociations(opOptions, $assoc, mainEntityForJoin, aliasMap, startId, joiningParams); startId = nextId; // when limit or offset is set, distinct is required, otherwise nested records will be also be counted for limit needDistinctForLimit = $limit != null || $offset != null; } else if ($hasSubQuery) { mainEntityForJoin = model; } // Build select columns if (!$skipOrm) { if ((0, _helpers.isSelectAll)($select)) { const l = model.length + 1; $select || ($select = new Set([ '*' ])); _utils._.each(aliasMap, (v, k)=>{ if (k !== model) { $select.add(k.substring(l) + '.*'); } }); } } const selectParams = []; const selectColomns = $select ? $skipOrm ? this._buildColumns($select, selectParams, mainEntityForJoin, aliasMap) : this._buildOrmColumns($select, selectParams, mainEntityForJoin, aliasMap) : '*'; // Build from clause let fromClause = ' FROM ' + fromTable; let fromAndJoin = fromClause; if (mainEntityForJoin) { fromAndJoin += ` ${this.escapeId(mainAlias)} `; if ($assoc) { fromAndJoin = this._concatJoinClauses(fromAndJoin, directJoinings, joinings); } } // Build where clause let whereClause = ''; const whereParams = []; if ($where) { whereClause = this._joinCondition(opOptions, $where, whereParams, null, mainEntityForJoin, aliasMap); if (whereClause) { whereClause = ' WHERE ' + whereClause; } } // Build group by clause let groupByClause = ''; if ($groupBy) { groupByClause += ' ' + this._buildGroupBy($groupBy, mainEntityForJoin, aliasMap); } // Build order by clause let orderByClause = ''; if ($orderBy) { orderByClause += ' ' + this._buildOrderBy($orderBy, mainEntityForJoin, aliasMap); } // Build limit & offset clause const limitOffetParams = []; let limitOffset = this._buildLimitOffset($limit, $offset, limitOffetParams); const result = { hasJoining: $assoc != null, aliasMap, startId }; if (needDistinctForLimit) { const pKey = this._escapeIdWithAlias(opOptions.$key, mainEntityForJoin, aliasMap); const distinctFieldWithAlias = `DISTINCT(${pKey}) AS key_`; const keysSql = orderByClause ? `WITH records_ AS (SELECT ${distinctFieldWithAlias}, ROW_NUMBER() OVER(PARTITION BY ${pKey}${orderByClause}) AS row_${fromAndJoin}${whereClause}${groupByClause}) SELECT key_ FROM records_ ORDER BY row_${limitOffset}` : `WITH records_ AS (SELECT ${distinctFieldWithAlias}${fromAndJoin}${whereClause}${groupByClause}) SELECT key_ FROM records_ GROUP BY key_${limitOffset}`; let _nextId = Object.keys(aliasMap).length; const keySqlAnchor = (0, _numbertoletter.default)(_nextId++); const _joinings = []; const _directJoinings = []; const _joiningParams = []; this._joinAssociation(opOptions, { alias: keySqlAnchor, sql: keysSql, params: joiningParams.concat(whereParams, limitOffetParams), joinType: 'INNER JOIN', on: { [opOptions.$key]: (0, _helpers.xrCol)(`${keySqlAnchor}.key_`) } }, keySqlAnchor, _joinings, _directJoinings, model, aliasMap, _nextId, _joiningParams); if (_joinings.length) { fromAndJoin += ' ' + _joinings.join(' '); } result.sql = withTables + 'SELECT ' + selectColomns + fromAndJoin + whereClause + groupByClause + orderByClause; result.params = selectParams.concat(joiningParams, _joiningParams, whereParams); } else { result.sql = withTables + 'SELECT ' + selectColomns + fromAndJoin + whereClause + groupByClause + orderByClause + limitOffset; result.params = selectParams.concat(joiningParams, whereParams, limitOffetParams); } if ($countBy) { result.countSql = withTables + `SELECT COUNT(*) AS count FROM (SELECT DISTINCT ` + this._escapeIdWithAlias($countBy, mainEntityForJoin, aliasMap) + fromAndJoin + whereClause + groupByClause + ')'; result.countParams = joiningParams.concat(whereParams); } return result; } _concatJoinClauses(fromAndJoin, directJoinings, joinings) { directJoinings.forEach((dj)=>{ fromAndJoin += `, ${dj.entity ? this.escapeId(dj.entity) : dj.sql} ${this.escapeId(dj.alias)}`; }); if (joinings.length) { fromAndJoin += joinings.join(' '); } return fromAndJoin; } /** * Build CTE header and return the select from target and CTE header * @param {string} model * @returns {object} { fromTable, withTables, model } */ _buildCTEHeader(model) { let withTables = ''; let fromTable; // CTE, used by aggregation if (typeof model === 'object') { const { sql: subSql, alias } = model; model = alias; fromTable = alias; withTables = `WITH ${this.escapeId(alias)} AS (${subSql}) `; } fromTable = this.escapeId(model); return { fromTable, withTables, model }; } /** * Generate an alias * @param {*} index * @param {*} anchor * @returns {string} */ _generateAlias(index, anchor) { if (this.options.verboseAlias) { return `${(0, _utils.snakeCase)(anchor).toUpperCase()}${index}`; } return (0, _numbertoletter.default)(index); } /** * Extract associations into joining clauses. * { * entity: <remote entity> * joinType: 'LEFT JOIN|INNER JOIN|FULL OUTER JOIN' * anchor: 'local property to place the remote entity' * localField: <local field to join> * remoteField: <remote field to join> * subAssocs: { ... } * } * * @param {*} opOptions * @param {*} associations * @param {*} parentAliasKey * @param {*} aliasMap * @param {*} params * @param {*} startId * @param {*} params * @returns {object} */ _joinAssociations(opOptions, associations, parentAliasKey, aliasMap, startId, params) { let joinings = []; let directJoinings = []; _utils._.each(associations, (assocInfo, anchor)=>{ startId = this._joinAssociation(opOptions, assocInfo, anchor, joinings, directJoinings, parentAliasKey, aliasMap, startId, params); }); return [ joinings, directJoinings, startId ]; } _joinAssociation(opOptions, assocInfo, anchor, joinings, directJoinings, parentAliasKey, aliasMap, startId, params) { const alias = assocInfo.alias || this._generateAlias(startId++, anchor); let { joinType, on } = assocInfo; joinType || (joinType = 'LEFT JOIN'); if (assocInfo.sql) { aliasMap[parentAliasKey + '.' + alias] = assocInfo.list ? { list: true, alias } : alias; if (assocInfo.params) { params.push(...assocInfo.params); } if (on == null) { throw new _types.InvalidArgument('Joining custom SQL relation without "on" is not supported.', { mainEntity: parentAliasKey, sql: assocInfo.sql }); } joinings.push(`${joinType} (${assocInfo.sql}) ${this.escapeId(alias)} ON ${this._joinCondition(opOptions, on, params, null, parentAliasKey, aliasMap)}`); return startId; } const { entity, subAssocs, list } = assocInfo; const aliasKey = parentAliasKey + '.' + anchor; aliasMap[aliasKey] = list ? { list, alias } : alias; let subJoinings; const subJoiningParams = []; if (subAssocs) { let _directJoinings = []; let nextId; [subJoinings, _directJoinings, nextId] = this._joinAssociations(opOptions, subAssocs, aliasKey, aliasMap, startId, subJoiningParams); startId = nextId; if (_directJoinings.length) { throw new _types.InvalidArgument('Direct joining without "on" is not supported in sub-associations.', { entity: entity, mainEntity: parentAliasKey }); } } if (on == null) { directJoinings.push({ entity, alias }); } else { joinings.push(`${joinType} ${this.escapeId(entity)} ${this.escapeId(alias)} ON ${this._joinCondition(opOptions, on, params, null, parentAliasKey, aliasMap)}`); } if (subJoinings) { subJoinings.forEach((sj)=>joinings.push(sj)); params.push(...subJoiningParams); } return startId; } /** * SQL condition representation * Rules: * default: * array: OR * kv-pair: AND * $all: * array: AND * $any: * kv-pair: OR * $not: * array: not ( or ) * kv-pair: not ( and ) * @param {object} opOptions * @param {object|array|string} condition - The condition object * @param {array} params - The parameters array * @param {string} joinOperator - 'AND' or 'OR' * @param {string} mainEntity - The entity name that has joining * @param {object} aliasMap - The alias map, dot separated key -> alias * @returns {string} */ _joinCondition(opOptions, condition, params, joinOperator, mainEntity, aliasMap) { if (Array.isArray(condition)) { if (!joinOperator) { joinOperator = 'OR'; } return condition.map((c)=>'(' + this._joinCondition(opOptions, c, params, null, mainEntity, aliasMap) + ')').join(` ${joinOperator} `); } if ((0, _utils.isPlainObject)(condition)) { if (condition.$xr) { return this._packValue(condition, params, mainEntity, aliasMap); } if (!joinOperator) { joinOperator = 'AND'; } return _utils._.map(condition, (value, key)=>{ if (key === '$all' || key === '$and' || key.startsWith('$and_')) { // for avoiding duplicate, $and_1, $and_2 is valid if (!Array.isArray(value) && !(0, _utils.isPlainObject)(value)) { throw new _types.InvalidArgument('"$and" operator value should be an array or plain object.', { key, value }); } return '(' + this._joinCondition(opOptions, value, params, 'AND', mainEntity, aliasMap) + ')'; } if (key === '$any' || key === '$or' || key.startsWith('$or_')) { // for avoiding dupliate, $or_1, $or_2 is valid if (!Array.isArray(value) && !(0, _utils.isPlainObject)(value)) { throw new _types.InvalidArgument('"$or" operator value should be an array or plain object.', { key, value }); } return '(' + this._joinCondition(opOptions, value, params, 'OR', mainEntity, aliasMap) + ')'; } if (key === '$not' || key.startsWith('$not_')) { if (Array.isArray(value)) { if (value.length === 0) { throw new _types.InvalidArgument('"$not" operator value should be non-empty.', { key, value }); } return 'NOT (' + this._joinCondition(opOptions, value, params, null, mainEntity, aliasMap) + ')'; } if ((0, _utils.isPlainObject)(value)) { if ((0, _utils.isEmpty)(value)) { throw new _types.InvalidArgument('"$not" operator value should be non-empty.', { key, value }); } return 'NOT (' + this._joinCondition(opOptions, value, params, null, mainEntity, aliasMap) + ')'; } if (typeof value !== 'string') { throw new _types.InvalidArgument('Unsupported condition expression!', { key, value }); } return 'NOT (' + condition + ')'; } if ((key === '$expr' || key.startsWith('$expr_')) && typeof value === 'object') { if (value.$xr) { if (value.$xr === 'BinExpr') { const left = this._packValue(value.left, params, mainEntity, aliasMap); const right = this._packValue(value.right, params, mainEntity, aliasMap); return left + ` ${value.op} ` + right; } if (value.$xr === 'Raw') { if (value.params) { const numParamTokens = (0, _utils.countOfChar)(value.value, this.specParamToken); if (numParamTokens !== value.params.length) { throw new _types.InvalidArgument('Parameter placeholder count mismatch in raw SQL expression.', { expected: value.params.length, actual: numParamTokens }); } params.push(...value.params); } return value.value; } throw new _types.InvalidArgument('Unsupported $xr type in $expr.', { value }); } let dataSet; let exists = true; if (value.$exist || value.$exists) { dataSet = value.$exist || value.$exists; } else if (value.$notExist || value.$notExists) { dataSet = value.$notExist || value.$notExists; exists = false; } if (typeof dataSet === 'object') { if (dataSet.$xr === 'DataSet') { const sqlInfo = buildDataSetQuery(opOptions, dataSet, aliasMap); sqlInfo.params && params.push(...sqlInfo.params); return `(${exists ? 'EXISTS' : 'NOT EXISTS'} (${sqlInfo.sql}))`; } } throw new _types.InvalidArgument('Unsupported $expr value.', { value }); } return this._packCondition(opOptions, key, value, params, mainEntity, aliasMap); }).join(` ${joinOperator} `); } if (typeof condition !== 'string') { throw new _types.InvalidArgument('Unsupported condition!', { condition }); } return condition; } /** * Build limit and offset clause * @param {*} $limit * @param {*} $offset * @param {*} params * @returns {string} '' or ' LIMIT X OFFSET Y' */ _buildLimitOffset($limit, $offset, params) { let sql = ''; if ((0, _utils.isInteger)($limit) && $limit > 0) { if ((0, _utils.isInteger)($offset) && $offset > 0) { params.push($offset); sql = ` OFFSET ${this.specParamToken} LIMIT ${this.specParamToken}`; params.push($limit); } else { params.push($limit); sql = ` LIMIT ${this.specParamToken}`; } } else if ((0, _utils.isInteger)($offset) && $offset > 0) { params.push($offset); sql = ` OFFSET ${this.specParamToken}`; } return sql; } /** * Convert the dot separated field name to alias padded and escaped field name * @param {string} fieldName - The dot separate field name or starting with "::" for skipping alias padding * @param {string} mainEntity - Only called when mainEntity != null * @param {*} aliasMap * @returns {string} */ _escapeIdWithAlias(fieldName, mainEntity, aliasMap, dontEscape, disallowList) { if (fieldName.startsWith('::')) { // ::fieldName for skipping alias padding return this.escapeId(fieldName.substring(2)); } const rpos = fieldName.lastIndexOf('.'); if (rpos > 0) { const actualFieldName = fieldName.substring(rpos + 1); const basePath = fieldName.substring(0, rpos); let aliasKey; if (basePath.startsWith('_.')) { aliasKey = basePath; } else { if (!mainEntity) { throw new _types.InvalidArgument('Cascade alias is not allowed when the query has no associated entity populated.', { alias: fieldName }); } aliasKey = mainEntity + '.' + basePath; } return this._buildAliasedColumn(aliasMap, aliasMap[aliasKey], mainEntity, fieldName, actualFieldName, dontEscape, disallowList); } if (!mainEntity) { return fieldName === '*' ? '*' : dontEscape ? fieldName : this.escapeId(fieldName); } return this._buildAliasedColumn(aliasMap, aliasMap[mainEntity], mainEntity, fieldName, fieldName, dontEscape, disallowList); } _buildAliasedColumn(aliasMap, alias, mainEntity, fieldName, actualFieldName, dontEscape, disallowList) { if (!alias) { throw new _types.InvalidArgument(`Column reference "${fieldName}" not found in populated associations.`, { entity: mainEntity, aliasMap }); } if (typeof alias === 'object') { // note: Although order by a column in a list relation is non-sense, but it may be join with a unique condition and the result is not a list /* if (disallowList && alias.list) { throw new InvalidArgument(`Reference to a column "${fieldName}" in a list relation is not allowed.`, { entity: mainEntity, aliasMap, }) } */ alias = alias.alias; } return (this.reservedAlias.has(alias) ? alias : this.escapeId(alias)) + '.' + (actualFieldName === '*' ? '*' : dontEscape ? actualFieldName : this.escapeId(actualFieldName)); } _splitColumnsAsInput(opOptions, data, params, mainEntity, aliasMap) { return _utils._.reduce(data, (result, v, fieldName)=>{ const _packed = this._packSetValue(opOptions, fieldName, v, params, mainEntity, aliasMap); if (typeof _packed === 'string') { result.push(_packed); } else { result.push(..._packed); } return result; }, []); } /** * Pack an array of values into params and return the parameterized string with placeholders * @param {*} array * @param {*} params * @param {*} mainEntity * @param {*} aliasMap * @returns {string} */ _packArray(array, params, mainEntity, aliasMap) { return array.map((value)=>this._packValue(value, params, mainEntity, aliasMap)).join(','); } /** * Pack a value into params and return the parameter placeholder * @param {*} value * @param {*} params * @param {*} mainEntity * @param {*} aliasMap * @returns {string} */ _packValue(value, params, mainEntity, aliasMap) { if ((0, _utils.isPlainObject)(value)) { if (value.$xr) { switch(value.$xr){ case 'Column': return this._escapeIdWithAlias(value.name, mainEntity, aliasMap); case 'Function': return value.name + '(' + (value.args ? this._packArray(value.args, params, mainEntity, aliasMap) : '') + ')'; case 'Raw': if (value.params.length > 0) { params.push(...value.params); } return value.value; case 'BinExpr': { const left = this._packValue(value.left, params, mainEntity, aliasMap); const right = this._packValue(value.right, params, mainEntity, aliasMap); return '(' + left + ` ${value.op} ` + right + ')'; } default: throw new Error(`Unknown xeml runtime type: ${value.$xr}, value: ${JSON.stringify(value)}`); } } } params.push(value); return this.specParamToken; } /** * Pack a condition clause * * Value can be a literal or a plain condition object. * 1. fieldName, <literal> * 2. fieldName, { normal object } * * @param {object} opOptions * @param {string} fieldName - The field name in a condition object. * @param {*} value * @param {array} params - The parameters array * @param {string} mainEntity - The entity name that has joining * @param {object} aliasMap - The alias map, dot separated key -> alias * @returns {string} */ _packCondition(opOptions, fieldName, value, params, mainEntity, aliasMap) { if (value == null) { return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' IS NULL'; } if (Array.isArray(value)) { return this._packCondition(opOptions, fieldName, { $in: value }, params, mainEntity, aliasMap); } if (typeof value === 'object') { if (value.$xr) { return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' = ' + this._packValue(value, params, mainEntity, aliasMap); } const hasOperator = _utils._.find(Object.keys(value), (k)=>k && k[0] === '$'); if (hasOperator) { return _utils._.map(value, (v, k)=>{ if (k && k[0] === '$') { // operator switch(k){ case '$exist': case '$exists': return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + (v ? ' IS NOT NULL' : 'IS NULL'); case '$notExist': case '$notExists': return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + (v ? ' IS NULL' : 'IS NOT NULL'); case '$eq': case '$equal': return this._packCondition(opOptions, fieldName, v, params, mainEntity, aliasMap); case '$ne': case '$neq': case '$notEqual': if (v == null) { return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' IS NOT NULL'; } return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` <> ${this._packValue(v, params, mainEntity, aliasMap)}`; case '$>': case '$gt': case '$greaterThan': return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` > ${this._packValue(v, params, mainEntity, aliasMap)}`; case '$>=': case '$gte': case '$greaterThanOrEqual': return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` >= ${this._packValue(v, params, mainEntity, aliasMap)}`; case '$<': case '$lt': case '$lessThan': return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` < ${this._packValue(v, params, mainEntity, aliasMap)}`; case '$<=': case '$lte': case '$lessThanOrEqual': return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` <= ${this._packValue(v, params, mainEntity, aliasMap)}`; case '$between': if (!Array.isArray(v) || v.length !== 2) { throw new _types.InvalidArgument('The value should be an array with 2 elements when using "$between" operator.', { value: v }); } params.push(v[0]); params.push(v[1]); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` BETWEEN ${this.specParamToken} AND ${this.specParamToken}`; case '$notBetween': if (!Array.isArray(v) || v.length !== 2) { throw new _types.InvalidArgument('The value should be an array with 2 elements when using "$notBetween" operator.', { value: v }); } params.push(v[0]); params.push(v[1]); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` NOT BETWEEN ${this.specParamToken} AND ${this.specParamToken}`; case '$in': if (Array.isArray(v)) { params.push(v); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + this.specInClause; } if (typeof v === 'object') { if (v.$xr === 'Raw') { v.params && v.params.forEach((p)=>params.push(p)); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` IN (${v.value})`; } if (v.$xr === 'DataSet') { const sqlInfo = buildDataSetQuery(opOptions, v, aliasMap); sqlInfo.params && params.push(...sqlInfo.params); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` IN (${sqlInfo.sql})`; } } throw new _types.InvalidArgument('The value should be a dataset or an array when using "$in" operator.', { value: v }); case '$nin': case '$notIn': if (Array.isArray(v)) { params.push(v); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + this.specNotInClause; } if (typeof v === 'object') { if (v.$xr === 'Raw') { v.params && v.params.forEach((p)=>params.push(p)); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` NOT IN (${v.value})`; } if (v.$xr === 'DataSet') { const sqlInfo = buildDataSetQuery(opOptions, v, aliasMap); sqlInfo.params && params.push(...sqlInfo.params); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` NOT IN (${sqlInfo.sql})`; } } throw new _types.InvalidArgument('The value should be an array when using "$notIn" operator.', { value: v }); case '$startWith': case '$startsWith': if (typeof v !== 'string') { throw new _types.InvalidArgument('The value should be a string when using "$startWith" operator.', { value: v }); } params.push(`${v}%`); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' LIKE ' + this.specParamToken; case '$endWith': case '$endsWith': if (typeof v !== 'string') { throw new _types.InvalidArgument('The value should be a string when using "$endWith" operator.', { value: v }); } params.push(`%${v}`); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' LIKE ' + this.specParamToken; case '$like': case '$likes': if (typeof v !== 'string') { throw new _types.InvalidArgument('The value should be a string when using "$like" operator.', { value: v }); } params.push(`%${v}%`); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' LIKE ' + this.specParamToken; case '$filter': if (typeof v !== 'object') { throw new _types.InvalidArgument('The value should be an object when using "$filter" operator.', { value: v }); } params.push(v); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' @> ' + this.specParamToken; default: break; } throw new _types.InvalidArgument(`Unsupported condition operator: "${k}"!`, { key: fieldName, value: v }); } else { throw new _types.InvalidArgument('Operator should not be mixed with condition value.', { key: fieldName, value }); } }).join(' AND '); } } params.push(value); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ' = ' + this.specParamToken; } _packSetValue(opOptions, fieldName, value, params, mainEntity, aliasMap) { if (value == null) { return this._escapeIdWithAlias(fieldName) + ' = NULL'; } if ((0, _utils.isPlainObject)(value)) { if (value.$xr) { return this._escapeIdWithAlias(fieldName) + ' = ' + this._packValue(value, params, mainEntity, aliasMap); } const hasOperator = _utils._.find(Object.keys(value), (k)=>k && k[0] === '$'); if (hasOperator) { return _utils._.map(value, (v, k)=>{ if (k && k[0] === '$') { // operator switch(k){ case '$set': if (typeof v !== 'object') { throw new _types.InvalidArgument('The value should be an object when using "$set" operator.', { value: v }); } return _utils._.map(v, (_v, _k)=>{ if (typeof _v === 'string') { _v = JSON.stringify(_v); } params.push(_v); const accessor = _k.split('.').map((k)=>`[${this.escapeValue(k)}]`).join(''); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + accessor + ' = ' + this.specParamToken; }).join(', '); case '$setAt': if (typeof v !== 'object') { throw new _types.InvalidArgument('The value should be an object when using "$setAt" operator.', { value: v }); } params.push(v.value); const dontEscape = true; let index; if (typeof v.at === 'object') { if (v.at.$xr !== 'Column') { throw new _types.InvalidArgument('Invalid "$set" operator value. "$at" should be a column reference or one-based index literal.', { value: v }); } index = this._escapeIdWithAlias(v.at.name, mainEntity, aliasMap, dontEscape); } else { if (v.at <= 0) { throw new _types.InvalidArgument('Invalid "$set" operator value. "$at" should be a positive integer.', { value: v }); } index = v.at; } return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + `[${index}] = ` + this.specParamToken; case '$setSlice': if (typeof v !== 'object') { throw new _types.InvalidArgument('The value should be an object when using "$setSlice" operator.', { value: v }); } const begin = v.begin > 0 ? `${v.begin}` : ''; const end = v.end > 0 ? `:${v.end}` : ':'; params.push(v.value); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + `[${begin}${end}] = ` + this.specParamToken; case '$case': if (typeof v !== 'object') { throw new _types.InvalidArgument('The value should be an object when using "$case" operator.', { value: v }); } const { $when, $then, $else } = v; const whenClause = this._joinCondition(opOptions, $when, params, 'AND', mainEntity, aliasMap); // case result requires type cast const typeCastId = this.specGetTypeCast(opOptions, fieldName); const thenClause = this.specApplyTypeCast(typeCastId, this._packValue($then, params, mainEntity, aliasMap)); const elseClause = this.specApplyTypeCast(typeCastId, this._packValue($else, params, mainEntity, aliasMap)); return this._escapeIdWithAlias(fieldName, mainEntity, aliasMap) + ` = CASE WHEN ${whenClause} THEN ${thenClause} ELSE ${elseClause} END`; default: break; } throw new _types.InvalidArgument(`Unsupported set value operator: "${k}"!`, { key: fieldName, value: v }); } else { throw new _types.InvalidArgument('Operator should not be mixed with condition value.', { key: fieldName, value }); } }); } } params.push(value); return this._escapeIdWithAlias(fieldName) + ' = ' + this.specParamToken; } /** * Build column list * @param {array} columns * @param {array} params * @param {*} mainEntity * @param {*} aliasMap * @returns {string} */ _buildColumns(columns, params, mainEntity, aliasMap) { let colList = ''; columns.forEach((col)=>colList += this._buildColumn(col, params, mainEntity, aliasMap) + ', '); return colList.substring(0, colList.length - 2); } /** * * @param {string|number|object} col - The column object or string, quoted string will be treated as raw SQL * @property {string} [col.alias] - The alias of the column, can be a dot separated string * @property {string} [col.$xr] - The type of the column, "function" | "func", "expression" | "expr, "column" | "col" * @property {string} [col.name] - The column name * @property {string} [col.prefix] - The prefix of the function, like DISTINCT * @property {array} [col.args] - The arguments of the function * @property {object} [col.over] - The window function over clause * @property {array} [col.over.$partitionBy] - The partition by clause * @property {array} [col.over.$orderBy] - The order by clause * @property {string} [col.over.$nulls] - The nulls clause, FIRST | LAST * @param {*} params * @param {*} mainEntity * @param {*} aliasMap * @returns {string} */ _buildColumn(col, params, mainEntity, aliasMap) { if (typeof col === 'string') { // it's a string if it's quoted when passed in return (0, _helpers.isRawSql)(col) ? (0, _helpers.extractRawSql)(col) : this._escapeIdWithAlias(col, mainEntity, aliasMap); } if (typeof col === 'number') { return col; } if (typeof col === 'object' && col.$xr) { if (col.alias) { return this._buildColumn(_utils._.omit(col, [ 'alias' ]), params, mainEntity, aliasMap) + ' AS ' + this.escapeId(col.alias); } switch(col.$xr){ // Function case 'Function': const name = col.name.toUpperCase(); if (name === 'COUNT' && col.args.length === 1 && col.args[0] === '*') { return 'COUNT(*)'; } if (this.constructor.windowFunctions.has(name)) { if (!col.over) { throw new _types.InvalidArgument(`"${name}" window function requires over clause.`); } } else if (!this.constructor.windowableFunctions.has(name) && col.over) { throw new _types.InvalidArgument(`"${name}" function does not support over clause.`); } let funcClause = name + '(' + (col.prefix ? `${col.prefix.toUpperCase()} ` : '') + (col.args ? this._buildColumns(col.args, params, mainEntity, aliasMap) : '') + ')'; if (col.over) { funcClause += ' OVER('; if (col.over.$partitionBy) { funcClause += this._buildPartitionBy(col.over.$partitionBy, mainEntity, aliasMap); } if (col.over.$orderBy) { if (!funcClause.endsWith('(')) { funcClause += ' '; } funcClause += this._buildOrderBy(col.over.$orderBy, mainEntity, aliasMap); } if (col.over.$nulls) { funcClause += ' NULLS ' + col.over.$nulls; // FIRST | LAST } funcClause += ')'; } return funcClause; // BinExpr case 'BinExpr': return this._packValue(col, params, mainEntity, aliasMap); // Column case 'Column': return this._escapeIdWithAlias(col.name, mainEntity, aliasMap); case 'OpGet': const accessor = typeof col.key === 'string' ? col.key.split('.').map((k)=>`[${this.escapeValue(k)}]`).join('') : `[${col.key}]`; return this._escapeIdWithAlias(col.field, mainEntity, aliasMap) + accessor; } } throw new _types.ApplicationError(`Unknown column syntax: ${JSON.stringify(col)}`); } /** * Build group by column * @param {string|object} groupBy * @param {*} mainEntity * @param {*} aliasMap * @returns {string} */ _buildGroupByColumn(groupBy, mainEntity, aliasMap) { if (typeof groupBy === 'string') { return (0, _helpers.isRawSql)(groupBy) ? (0, _helpers.extractRawSql)(groupBy) : this._escapeIdWithAlias(groupBy, mainEntity, aliasMap); } if (typeof groupBy === 'object') { if (groupBy.alias) { return this._escapeIdWithAlias(groupBy.alias, mainEntity, aliasMap); } } throw new _types.ApplicationError(`Unknown GROUP BY syntax: ${JSON.stringify(groupBy)}`); } /** * Build group by column list * @param {array|string} groupBy * @param {*} mainEntity * @param {*} aliasMap * @returns {string} */ _buildGroupByList(groupBy, mainEntity, aliasMap) { if (Array.isArray(groupBy)) { return 'GROUP BY ' + groupBy.map((by)=>this._buildGroupByColumn(by, mainEntity, aliasMap)).join(', '); } return 'GROUP BY ' + this._buildGroupByColumn(groupBy, mainEntity, aliasMap); } /** * Build group by clause from groupBy object * @param {array|string} groupBy * @param {string} [mainEntity] - The entity name that has joining * @param {*} aliasMap * @returns {string} */ _buildGroupBy(groupBy, mainEntity, aliasMap) { return this._buildGroupByList(groupBy, mainEntity, aliasMap); } /** * Build partition by clause * @param {*} partitionBy * @param {*} mainEntity * @param {*} aliasMap * @returns {string} */ _buildPartitionBy(partitionBy, mainEntity, aliasMap) { if (typeof partitionBy === 'string') { return 'PARTITION BY ' + this._escapeIdWithAlias(partitionBy, mainEntity, aliasMap); } if (Array.isArray(partitionBy)) { return 'PARTITION BY ' + partitionBy.map((by)=>this._escapeIdWithAlias(by, mainEntity, aliasMap)).join(', '); } throw new _types.ApplicationError(`Unknown PARTITION BY syntax: ${JSON.stringify(partitionBy)}`); } /** * Build order by clause * @param {string|array|object} orderBy * @param {*} mainEntity * @param {*} aliasMap * @returns {string} * * @example * $orderBy: 'name' => 'ORDER BY A.name' * $orderBy: ['name', 'age'] => 'ORDER BY A.name, A.age' * $orderBy: { name: -1, age: 1 } => 'ORDER BY A.name DESC, A.age ASC' */ _buildOrderBy(orderBy, mainEntity, aliasMap) { if (typeof orderBy === 'string') { return 'ORDER BY ' + this._escapeIdWithAlias(orderBy, mainEntity, aliasMap, null, true); } if (Array.isArray(orderBy)) return 'ORDER BY ' + orderBy.map((by)=>this._escapeIdWithAlias(by, mainEntity, aliasMap, null, true)).join(', '); if ((0, _utils.isPlainObject)(orderBy)) { return 'ORDER BY ' + _utils._.map(orderBy, (asc, col)=>this._escapeIdWithAlias(col, mainEntity, aliasMap, null, true) + (asc === false || asc === -1 ? ' DESC' : '')).join(', '); } throw new _types.ApplicationError(`Unknown ORDER BY syntax: ${JSON.stringify(orderBy)}`); } /** * Create a