enhancer-data-bridge
Version:
A bridge between Enhancer Clould and user business datasource
489 lines (451 loc) • 17.5 kB
JavaScript
;
/**
* Database Service Base Class
* @created by zyz
* @date 03/14/2017
*/
var escape = require('mysql').escape;
var escapeId = require('mysql').escapeId;
var logger = require('log4js').getLogger('io');
var pattern = {
variableAndIdentifier: /(@(\d+-)?\w+(\.\w+)*@)|(\$(\d+-)?\w+(\.\w+)*\$)/g,
variable: /@(\d+-)?\w+(\.\w+)*@/g,
//varExp match eg:/ #@2-RESULT@.data.id + 2# | @12-OBJ@.data.name/
varExp: /(#[^#]+#)|(@(\d+-)?\w+(\.\w+)*@((\.\w+)|(\[\s*\d+\s*\])|(\[\s*'[^']+'\s*\])|(\[\s*"[^"]+"\s*\]))+)/g,
clientVariable: /(@\d+-\w+(\.\w+)*@)|(\$\d+-\w+(\.\w+)*\$)/g,
identifier: /\$(\d+-)?\w+(\.\w+)*\$/g
};
class Service {
constructor(dbConfig) {
this.config = dbConfig;
}
/**
* {Function} Criteria Query - This method must be implemented by subclass.
* @param criteria {Object} Query criteria
* eg: {
id: "d101",
query: "SELECT * FROM TABLE_$curr_year$ WHERE UID = @user_id@ AND NUM = @12-number@",
serverVars: {...}
params: {"1-userid": "zyz"},
countRecords: true || false,
metaData: true || false
filters: {...}, // @see this.__parseFiltersToSqlConditions()
format: "object" || "array",
paged: false || true,
page: 0,
rownum: 10,
sortBy: "uid asc, num desc"
}
* @param cb {Function}
- err
- result
*/
criteriaQuery(criteria, cb) {}
/**
* {Function} Execute - This method must be implemented by subclass.
* @param sql {String} The sql which can be executed by database.
* @param params {Object|Array} The parameters used by sql.
* @param cb {Function} Callback
*/
execute(sql, params, cb) {}
/**
* {Function} beginTransaction
*/
beginTransaction(cb) {}
/**
* {getConnection}
*/
getConnection(cb) {}
/**
* @param criteria - The same as required by query method above.
* @param paramPlaceHolder {String} This placeholder will replace the variables
* in sql according to different different sql writting in different database.
* @return executable sql
*/
prepareSQLStatement(criteria, paramPlaceHolder) {
// 1. Replace identifier.
var sql = criteria.query.replace( pattern.identifier, function( s ) {
var val;
if ( /\$\w+(\.\w+)*\$/.test(s) ) {
val = criteria.serverVars[ s.replace(/\$/g, '').toUpperCase() ]
} else {
val = criteria.params[ s.replace(/\$/g, '').toUpperCase() ];
}
if ( typeof val === "undefined" ) {
throw new Error( "Lack of parameter: " + s.replace(/\$/g, '') );
}
if ( !val ) {
return "";
}
if ( val.length > 64 ) {
throw new Error( "The length of value of identifier is exceeded. value = '"
+ val + "', length = " + val.length + ", max length: 64." );
}
if ( !/^\w+(\.\w+)*$/.test( val ) ) {
throw new Error( "Invalid value injected in SQL: '" + val + "'" );
}
return val;
} );
// 2. Remove comments.
sql = sql.replace(/\/\*(.|\s)+?\*\//g, '')
.replace(/--[^'\n]*('[^'\n]*'[^'\n]*)+/g, '')
.replace(/'([^'\-\n]*\-[^'\-\n]*)+'/g, function(s) {
return s.replace(/-/g, '!@!');
})
.replace(/--[^'\n]+/g, '')
.replace(/!@!/g, '-');
var params = [];
// 3. Replace JavaScript such as #@2-RESULT@.data.id + 3#, @12-IMG@.url
// and caculate the value into params.
sql = sql.replace(/'[^']'/g, function(s) {
return s.replace(/#/g, '{SHARP}');
})
.replace(pattern.varExp, function(expr) {
var exp = expr.replace(/#/g, '');
exp = '(function(sv, pa){var __ret = '
+ exp.replace(pattern.variable, function(ss) {
var name = ss.split(/\$|\@/)[1];
if (/^\w+(\.\w+)*$/.test(name)) {
return ' sv["' + name.toUpperCase() + '"]';
} else {
return ' pa["' + name.toUpperCase() + '"]';
}
}) + '; return __ret;})';
var f, vname, ret;
try {
f = eval(exp);
} catch(e) {
throw new Error('The ' + expr + ' is not a valid JavaScript expression. Caused by: '
+ e.message);
}
try {
vname = '0-TEMP_VAR_' + Math.round(Math.random() * 10000000);
ret = f(criteria.serverVars, criteria.params);
if (typeof ret === 'undefined') {
ret = null;
}
criteria.params[vname] = ret;
} catch(e) {
throw new Error('Error occured when executing '
+ expr + '. Caused by: '+ e.message);
}
return '@' + vname + '@';
})
.replace(/\{SHARP\}/g, function(s) {
return '#';
});
// 4. Replace Variables
sql = sql.replace( pattern.variable, function( s ) {
var val;
if ( /\@\w+(\.\w+)*\@/.test(s) ) {
val = criteria.serverVars[ s.replace(/\@/g, '').toUpperCase() ]
} else {
val = criteria.params[ s.replace(/\@/g, '').toUpperCase() ];
}
if ( typeof val === "undefined" ) {
throw new Error( "Lack of parameter: " + s.replace(/\@/g, '') );
}
params.push( val );
return paramPlaceHolder ? paramPlaceHolder : '?';
} );
sql = sql.replace(/;$/, '');
// 5. Add filters
var filters = this.__parseFiltersToSqlConditions(criteria.filters);
if ( filters ) {
// Eliminate where in string
var sql2 = sql.replace( /'[^']*'/g, 'XXX' );
// Eliminate where clause in nested structure.
sql2 = sql2.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ');
if ( sql2.search( /\sWHERE\s/i ) === -1 ) {
sql = sql + ' WHERE ' + filters;
} else {
sql = sql + ' AND ( ' + filters + ' )';
}
// TODO: handle the union case ...
}
var sourceId = (criteria.sourceId || '').split('_');
var from = '(page ' + sourceId[0] + ', window ' + sourceId[1] + ', no ' + sourceId[2] + ')';
criteria.sortBy = criteria.sortBy ? criteria.sortBy.trim() : '';
if (criteria.sortBy) {
var sortBy = [];
criteria.sortBy.split(',').forEach(function(s) {
s = s.trim();
var ss = s.split(/\s+/);
var field = escapeId(ss[0]).replace(/\`/g, '');
if (!/^(\w|[^\x00-\xff]|[\u4e00-\u9fa5])+(\.(\w|[^\x00-\xff]|[\u4e00-\u9fa5])+)?$/.test(field)) {
logger.error('Invalid sort by params:');
logger.error(field);
logger.error('SQL', from ,':\n', sql);
logger.error('ORDER BY:', criteria.sortBy);
return
}
var order = (ss[1] || 'ASC').toUpperCase();
if (order === 'ASC' || order === 'DESC') {
sortBy.push(field + ' ' + order);
}
});
criteria.sortBy = sortBy.length ? sortBy.join(',') : '';
}
logger.debug('SQL', from ,':\n', sql);
logger.debug('ORDER BY:', criteria.sortBy);
logger.debug('PARAMS:\n', JSON.stringify(params));
return {
sql: sql,
params: params,
isSelect: /^SELECT\s/i.test(sql.trim().replace(/^\s*\(\s*/, ''))
};
}
/**
* Parse filters to SQL Conditions
* @param filters {Object}
* eg: {
"groupOp": "OR", // 条件运算: OR | AND
"groups": [ // 条件组,相当于 SQL 条件加括号,可以递归地定义下去。
{
"groupOp": "OR",
"rules": [
{
"field": "birth_place",
"op": "eq",
"data": "浙江"
}
],
"groups": [...] // 继续嵌套下级
}
],
"rules": [ // 条件数组,每个元素是一个单独的条件
{
"field": "id", // 字段
"op": "eq", // 操作,取值含义对照:
// eq 等于, ne 不等于, lt 小于, le 小于等于, gt 大于, ge 大于等于, bw 以开头, bn 不以开头,
// ew 以结尾, en 不以结尾, cn 包含, nc 不包含, nu 为空, nn 不为空, in 在集合中, nn 不在集合中
"data": 1 //数据
},
{
"field": "id",
"op": "eq",
"data": 2
}
]
}
* @return {Sting} SQL Conditions
*/
__parseFiltersToSqlConditions(filters) {
if (!filters || !filters.groupOp || !filters.rules) {
return '';
}
var that = this;
var group = filters;
function parse(group) {
if (!group || !group.groupOp) {
return '';
}
group.groupOp = {AND: 'AND', OR: 'OR'}[group.groupOp.toUpperCase()]
|| 'AND';
var conditions = '';
if (group.rules instanceof Array) {
conditions = group.rules.map(function(rule) {
return that.__parseRuleToSqlCondition(rule);
}).join(' ' + group.groupOp + ' ');
}
if (group.groups instanceof Array && group.groups.length) {
var gcond = group.groups.map(function(g) {
var c = parse(g);
return c ? '(' + parse(g) + ')' : '';
}).filter(function(r) {
return !!r;
})
.join(' ' + group.groupOp + ' ');
if (gcond) {
conditions = gcond + (conditions ? ' ' + group.groupOp + ' ' + conditions
: '');
}
}
return conditions;
}
return parse(group);
}
/**
* Parse rule to SQL condition
* @param rule {Object}
eg: {
field: 'ID', // Field name
op: 'eq', // Operation name
data: '2312' // Data
}
* @return SQL condition
eg: id = '2313'
*/
__parseRuleToSqlCondition(rule) {
if (!rule.field || !rule.op) {
return '';
}
rule.field = escapeId(rule.field).replace(/\`/g, '');
if (!/^(\w|[^\x00-\xff]|[\u4e00-\u9fa5])+(\.(\w|[^\x00-\xff]|[\u4e00-\u9fa5])+)?$/.test(rule.field)) {
logger.error('Invalid query field:');
logger.error(rule.field);
return '';
}
var dataType = typeof rule.data;
var isNormalType = dataType === 'number'
|| dataType === 'string'
|| dataType === 'boolean'
|| rule.data === null;
switch (rule.op) {
case 'eq':
return isNormalType
? rule.field + ' = ' + escape(rule.data)
: '';
case 'is':
return isNormalType
? rule.field + ' is ' + escape(rule.data)
: '';
case 'ne':
return isNormalType
? rule.field + ' <> ' + escape(rule.data)
: '';
case 'lt':
return isNormalType
? rule.field + ' < ' + escape(rule.data)
: '';
case 'le':
return isNormalType
? rule.field + ' <= ' + escape(rule.data)
: '';
case 'gt':
return isNormalType
? rule.field + ' > ' + escape(rule.data)
: '';
case 'ge':
return isNormalType
? rule.field + ' >= ' + escape(rule.data)
: '';
case 'bw':
return rule.field
+ ' LIKE '
+ escape(rule.data + '%');
case 'bn':
return rule.field
+ ' NOT LIKE '
+ escape(rule.data + '%');
case 'ew':
return rule.field
+ ' LIKE '
+ escape('%' + rule.data);
case 'en':
return rule.field
+ ' NOT LIKE '
+ escape(rule.data + '%');
case 'cn':
return rule.field
+ ' LIKE '
+ escape('%' + rule.data + '%');
case 'nc':
return rule.field
+ ' NOT LIKE '
+ escape('%' + rule.data + '%');
case 'nu':
return rule.field
+ ' IS NULL';
case 'nn':
return rule.field
+ ' IS NOT NULL';
case 'in':
return rule.field
+ ' IN ('
+ (rule.data instanceof Array ? rule.data
: (isNormalType ? [rule.data] : []))
.map(function(item) {
return escape(item);
}).join(',')
+ ')';
case 'ni':
return rule.field
+ ' NOT IN ('
+ (rule.data instanceof Array ? rule.data
: (isNormalType ? [rule.data] : []))
.map(function(item) {
return escape(item);
}).join(',')
+ ')';
default:
return '';
}
}
getCountSql(sql, params, paramPlaceHolder) {
sql = sql.trim();
if (!sql) {
return;
}
// Eliminate nested structure.
var sql2 = sql.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ')
.replace(/\([^\(\)]+\)/ig, ' XXX ');
if (sql2.search(/\sUNION\s/i) !== -1 || sql2.search(/\sGROUP\sBY\s/i) !== -1) {
return {
sql: "SELECT count(*) records FROM (" + sql + ") A",
params: params
}
}
var tag = 0;
var found = 0; // Mark the first FROM word is found.
sql2 = 'SELECT COUNT(*) records FROM ';
var ret = sql.replace(/SELECT\*/ig, 'SELECT *')
.replace(/SELECT\(/ig, 'SELECT (')
.replace(/FROM\(/ig, 'FROM (')
.split(/(\s?SELECT(\s|\(|\*))|(\sFROM(\s|\())/i)
.filter(function(l) {
return !!l;
});
ret.forEach(function(l) {
if (!found && /\s?SELECT(\s|\(|\*)/i.test(l)) {
tag++;
} else if (!found && /\sFROM(\s|\()/i.test(l)) {
tag--;
if (tag === 0) {
found = 1;
}
} else if (found) {
sql2 = sql2 + (l || '');
return
}
});
// Resort params.
paramPlaceHolder = paramPlaceHolder || '\\\?';
var pReg = new RegExp(paramPlaceHolder, 'g');
var countParams = [];
var num = (sql2.match(pReg) || []).length;
var len = params.length;
if (num === len) {
countParams = params;
} else {
for (var i = len - num; i < len; i++) {
if (i < 0) {
continue;
}
countParams.push(params[i]);
}
}
return {
sql: sql2,
params: countParams
}
}
};
module.exports = Service;