@kitmi/data
Version:
Jacaranda Framework Data Access Model
1,012 lines • 50.7 kB
JavaScript
"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