UNPKG

database-proxy

Version:

Through a set of access control rules configuration database access to realize the client directly access the database via HTTP.

612 lines (611 loc) 21.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqlQueryBuilder = exports.SqlBuilder = void 0; const assert = require("assert"); const types_1 = require("../types"); /** * SqlBuilder: Mongo 操作语法生成 SQL 语句 */ class SqlBuilder { constructor(params) { this._values = []; this._values = []; this.params = params; } static from(params) { return new SqlBuilder(params); } get table() { return this.params.collection; } get query() { return this.params.query || {}; } get projection() { return this.params.projection || {}; } get orders() { return this.params.order || []; } get data() { return this.params.data || {}; } get joins() { return this.params.joins || []; } select() { const fields = this.buildProjection(); const joins = this.buildJoins(); const query = this.buildQuery(); const orderBy = this.buildOrder(); const limit = this.buildLimit(); const sql = `select ${fields} from ${this.table} ${joins}${query} ${orderBy} ${limit}`; const values = this.values(); return { sql, values, }; } update() { this.checkData(); const data = this.buildUpdateData(); const joins = this.buildJoins(); const query = this.buildQuery(); // 当 multi 为 true 时允许多条更新,反之则只允许更新一条数据 // multi 默认为 false const multi = this.params.multi; const limit = multi ? '' : `limit 1`; const orderBy = this.buildOrder(); const sql = `update ${this.table} ${data} ${joins}${query} ${orderBy} ${limit}`; const values = this.values(); return { sql, values }; } delete() { const joins = this.buildJoins(); const query = this.buildQuery(); // 当 multi 为 true 时允许多条更新,反之则只允许更新一条数据 const multi = this.params.multi; const limit = multi ? '' : `limit 1`; const orderBy = this.buildOrder(); const sql = `delete from ${this.table} ${joins}${query} ${orderBy} ${limit} `; const values = this.values(); return { sql, values, }; } insert() { this.checkData(); const data = this.buildInsertData(); const sql = `insert into ${this.table} ${data}`; const values = this.values(); return { sql, values }; } count() { const joins = this.buildJoins(); const query = this.buildQuery(); const sql = `select count(*) as total from ${this.table} ${joins}${query}`; const values = this.values(); return { sql, values, }; } addValues(values) { this._values.push(...values); } // 构建联表语句(join) buildJoins() { const joins = this.joins; const leftTable = this.params.collection; if (joins.length === 0) return ''; const strs = []; for (const join of joins) { const { collection, leftKey, rightKey, type } = join; assert(this.checkJoinType(type), `invalid join type: ${type}`); this.checkField(collection); this.checkField(leftKey); this.checkField(rightKey); const rightTable = collection; const str = `${type} join ${rightTable} on ${leftTable}.${leftKey} = ${rightTable}.${rightKey}`; strs.push(str); } const ret = strs.join(' '); /** * 因为 join 功能是后加的, 空 joins 拼入 sql 后,会增加两 * 这样会导致,原来的单元测试用例都无法通过的问题 * 如果 joins 为空,那么就只插入空串,无空格即可 */ const wrapped_joins = ret == '' ? '' : ` ${ret} `; return wrapped_joins; } checkJoinType(joinType) { const types = [ types_1.JoinType.FULL, types_1.JoinType.INNER, types_1.JoinType.LEFT, types_1.JoinType.RIGHT, ]; return types.includes(joinType); } // build query string buildQuery() { const builder = SqlQueryBuilder.from(this.query); const sql = builder.build(); const values = builder.values(); this.addValues(values); return sql; } // build update data string: set x=a, y=b ... /** * * ```js * { * action: 'database.updateDocument', * collection: 'categories', * query: { _id: '6024f815acbf480fbb9648ce' }, * data: { * '$set': { title: 'updated-title' }, * '$inc': { age: 1 }, * '$unset': { content: '' } * }, * merge: true * } * ``` */ buildUpdateData() { const strs = []; // sql 不支持 merge 为 false 的情况(合并更新,即替换) assert(this.params.merge, 'invalid params: {merge} should be true in sql'); // $set if (this.data[types_1.UPDATE_COMMANDS.SET]) { const _data = this.data[types_1.UPDATE_COMMANDS.SET]; assert(typeof _data === 'object', 'invalid data: value of $set must be object'); for (const key in _data) { const _val = _data[key]; assert(this.isBasicValue(_val), `invalid data: value of data only support BASIC VALUE(number|boolean|string|undefined|null), {${key}:${_val}} given`); this.addValues([_val]); strs.push(`${key}=?`); } } // $inc if (this.data[types_1.UPDATE_COMMANDS.INC]) { const _data = this.data[types_1.UPDATE_COMMANDS.INC]; assert(typeof _data === 'object', 'invalid data: value of $inc must be object'); for (const key in _data) { const _val = _data[key]; assert(typeof _val === 'number', `invalid data: value of $inc property only support number, {${key}:${_val}} given`); this.addValues([_val]); strs.push(`${key}= ${key} + ?`); } } // $mul if (this.data[types_1.UPDATE_COMMANDS.MUL]) { const _data = this.data[types_1.UPDATE_COMMANDS.MUL]; assert(typeof _data === 'object', 'invalid data: value of $mul must be object'); for (const key in _data) { const _val = _data[key]; assert(typeof _val === 'number', `invalid data: value of $mul property only support number, {${key}:${_val}} given`); this.addValues([_val]); strs.push(`${key}= ${key} * ?`); } } // $unset if (this.data[types_1.UPDATE_COMMANDS.REMOVE]) { const _data = this.data[types_1.UPDATE_COMMANDS.REMOVE]; assert(typeof _data === 'object', 'invalid data: value of $unset must be object'); for (const key in _data) { strs.push(`${key}= null`); } } assert(strs.length, 'invalid data: set statement in sql is empty'); return 'set ' + strs.join(','); } // build insert data string: (field1, field2) values (a, b, c) ... buildInsertData() { const fields = Object.keys(this.data); const values = fields.map((key) => { const _val = this.data[key]; assert(this.isBasicValue(_val), `invalid data: value of data only support BASIC VALUE(number|boolean|string|undefined|null), {${key}:${_val}} given`); this.addValues([_val]); return '?'; }); const s_fields = fields.join(','); const s_values = values.join(','); return `(${s_fields}) values (${s_values})`; } _buildData() { const fields = Object.keys(this.data); const values = fields.map((key) => { const _val = this.data[key]; assert(this.isBasicValue(_val), `invalid data: value of data only support BASIC VALUE(number|boolean|string|undefined|null), {${key}:${_val}} given`); return _val; }); return { fields, values }; } buildOrder() { if (this.orders.length === 0) { return ''; } const strs = this.orders.map((ord) => { assert([types_1.Direction.ASC, types_1.Direction.DESC].includes(ord.direction), `invalid query: order value of {${ord.field}:${ord.direction}} MUST be 'desc' or 'asc'`); return `${ord.field} ${ord.direction}`; }); return 'order by ' + strs.join(','); } buildLimit(_limit) { const offset = this.params.offset || 0; const limit = this.params.limit || _limit || 100; assert(typeof offset === 'number', 'invalid query: offset must be number'); assert(typeof limit === 'number', 'invalid query: limit must be number'); return `limit ${offset},${limit}`; } /** * 指定返回的字段 * @tip 在 mongo 中可以指定只显示哪些字段 或者 不显示哪些字段,而在 SQL 中我们只支持[只显示哪些字段] * 示例数据: `projection: { age: 1, f1: 1}` */ buildProjection() { const fields = []; for (const key in this.projection) { this.checkProjection(key); const value = this.projection[key]; assert(value, `invalid query: value of projection MUST be {true} or {1}, {false} or {0} is not supported in sql`); fields.push(key); } if (fields.length === 0) { return '*'; } return fields.join(','); } values() { return this._values || []; } // 是否为值属性(number, string, boolean, undefine, null) isBasicValue(value) { if (value === null) { return true; } const type = typeof value; return ['number', 'string', 'boolean', 'undefined'].includes(type); } // data 不可为空 checkData() { assert(this.data, `invalid data: data can NOT be ${this.data}`); assert(typeof this.data === 'object', `invalid data: data must be an object`); assert(!(this.data instanceof Array), `invalid data: data cannot be Array while using SQL`); const keys = Object.keys(this.data); keys.forEach(this.checkField); assert(keys.length, `invalid data: data can NOT be empty object`); } checkField(field_name) { if (SecurityCheck.checkField(field_name) === false) throw new Error(`invalid field : '${field_name}'`); } checkProjection(name) { if (SecurityCheck.checkProjection(name) === false) { throw new Error(`invalid projection field : '${name}'`); } } } exports.SqlBuilder = SqlBuilder; /** * Mongo 查询转换为 SQL 查询 */ class SqlQueryBuilder { constructor(query) { this._values = []; // SQL 参数化使用,收集SQL参数值 this.query = query; } static from(query) { return new SqlQueryBuilder(query); } // build() { assert(this.hasNestedFieldInQuery() === false, 'invalid query: nested property in query'); let strs = ['where 1=1']; // 遍历查询属性 for (const key in this.query) { const v = this.buildOne(key, this.query[key]); strs.push(v); } strs = strs.filter((s) => s != '' && s != undefined); if (strs.length === 1) { return strs[0]; } return strs.join(' and '); } values() { return this._values; } // 处理一条查询属性(逻辑操作符属性、值属性、查询操作符属性) buildOne(key, value) { this.checkField(key); // 若是逻辑操作符 if (this.isLogicOperator(key)) { return this.processLogicOperator(key, value); } // 若是值属性(number, string, boolean) if (this.isBasicValue(value)) { return this.processBasicValue(key, value, types_1.QUERY_COMMANDS.EQ); } // 若是查询操作符(QUERY_COMMANDS) if (typeof value === 'object') { return this.processQueryOperator(key, value); } throw new Error(`unknow query property found: {${key}: ${value}}`); } // 递归处理逻辑操作符的查询($and $or) /** ```js query = { f1: 0, '$or': [ { f2: 1}, { f6: { '$lt': 4000 } }, { '$and': [ { f6: { '$gt': 6000 } }, { f6: { '$lt': 8000 } } ] } ] } // where 1=1 and f1 = 0 and (f2 = 1 and f6 < 4000 or (f6 > 6000 and f6 < 8000)) ``` */ processLogicOperator(operator, value) { const that = this; function _process(key, _value) { // 如果是逻辑符,则 value 为数组遍历子元素 if (that.isLogicOperator(key)) { assert(_value instanceof Array, `invalid query: value of logic operator must be array, but ${_value} given`); const result = []; for (const item of _value) { // 逻辑符子项遍历 for (const k in item) { // 操作 const r = _process(k, item[k]); result.push(r); } } // 将逻辑符中每个子项的结果用 逻辑符 连接起来 const op = that.mapLogicOperator(key); const _v = result.join(` ${op} `); // keep spaces in both ends return `(${_v})`; // } // 若是值属性(number, string, boolean) if (that.isBasicValue(_value)) { return that.processBasicValue(key, _value, types_1.QUERY_COMMANDS.EQ); } // 若是查询操作符(QUERY_COMMANDS) if (typeof _value === 'object') { return that.processQueryOperator(key, _value); } } return _process(operator, value); } // 处理值属性 processBasicValue(field, value, operator) { this.checkField(field); const op = this.mapQueryOperator(operator); let _v = null; // $in $nin 值是数组, 需单独处理 const { IN, NIN } = types_1.QUERY_COMMANDS; if ([IN, NIN].includes(operator)) { ; value.forEach((v) => this.addValue(v)); const arr = value.map((_) => '?'); const vals = arr.join(','); _v = `(${vals})`; } else { assert(this.isBasicValue(value), `invalid query: typeof '${field}' must be number|string|boolean|undefined|null, but ${typeof value} given`); this.addValue(value); _v = '?'; } return `${field} ${op} ${_v}`; } // 处理查询操作符属性 processQueryOperator(field, value) { let strs = []; // key 就是查询操作符 for (const key in value) { this.checkField(key); // @todo 暂且跳过[非]查询操作符,这种情况应该报错? if (!this.isQueryOperator(key)) { continue; } const sub_value = value[key]; const result = this.processBasicValue(field, sub_value, key); strs.push(result); } strs = strs.filter((s) => s != '' && s != undefined); if (strs.length === 0) { return ''; } return strs.join(' and '); } addValue(value) { this._values.push(value); } // 是否为值属性(number, string, boolean) isBasicValue(value) { const type = typeof value; return ['number', 'string', 'boolean'].includes(type); } // 是否为逻辑操作符 isLogicOperator(key) { const keys = Object.keys(types_1.LOGIC_COMMANDS).map((k) => types_1.LOGIC_COMMANDS[k]); return keys.includes(key); } // 是否为查询操作符(QUERY_COMMANDS) isQueryOperator(key) { const keys = Object.keys(types_1.QUERY_COMMANDS).map((k) => types_1.QUERY_COMMANDS[k]); return keys.includes(key); } // 是否为操作符 isOperator(key) { return this.isLogicOperator(key) || this.isQueryOperator(key); } // 获取所有的查询操作符 // @TODO not used getQueryOperators() { const logics = Object.keys(types_1.LOGIC_COMMANDS).map((key) => types_1.LOGIC_COMMANDS[key]); const queries = Object.keys(types_1.QUERY_COMMANDS).map((key) => types_1.QUERY_COMMANDS[key]); return [...logics, ...queries]; } // 判断 Query 中是否有属性嵌套 hasNestedFieldInQuery() { for (const key in this.query) { // 忽略对象顶层属性操作符 if (this.isOperator(key)) { continue; } // 子属性是否有对象 const obj = this.query[key]; if (typeof obj !== 'object') { continue; } if (this.hasObjectIn(obj)) { return true; } } return false; } // 判断给定对象(Object)中是否存在某个属性为非操作符对象 hasObjectIn(object) { for (const key in object) { // 检测到非操作符对象,即判定存在 if (!this.isOperator(key)) { return true; } } return false; } // 转换 mongo 查询操作符到 sql mapQueryOperator(operator) { assert(this.isQueryOperator(operator), `invalid query: operator ${operator} must be query operator`); let op = ''; switch (operator) { case types_1.QUERY_COMMANDS.EQ: op = '='; break; case types_1.QUERY_COMMANDS.NEQ: op = '<>'; break; case types_1.QUERY_COMMANDS.GT: op = '>'; break; case types_1.QUERY_COMMANDS.GTE: op = '>='; break; case types_1.QUERY_COMMANDS.LT: op = '<'; break; case types_1.QUERY_COMMANDS.LTE: op = '<='; break; case types_1.QUERY_COMMANDS.IN: op = 'in'; break; case types_1.QUERY_COMMANDS.NIN: op = 'not in'; break; case types_1.QUERY_COMMANDS.LIKE: op = 'like'; break; } assert(op != '', `invalid query: unsupperted query operator ${operator}`); return op; } // 转换 mongo 逻辑操作符到 sql mapLogicOperator(operator) { assert(this.isLogicOperator(operator), `invalid query: operator ${operator} must be logic operator`); let op = ''; switch (operator) { case types_1.LOGIC_COMMANDS.AND: op = 'and'; break; case types_1.LOGIC_COMMANDS.OR: op = 'or'; break; } assert(op != '', `invalid query: unsupperted logic operator ${operator}`); return op; } checkField(field_name) { if (SecurityCheck.checkField(field_name) === false) throw new Error(`invalid field : '${field_name}'`); } } exports.SqlQueryBuilder = SqlQueryBuilder; /** * 安全检测工具: SQL注入,字段合法性 */ class SecurityCheck { // 检查字段名是否合法:data field, query field static checkField(name) { if (this.isOperator(name)) { return true; } const black_list = [ ' ', '#', // ' or ', ';', `'`, `"`, '`', '-', '/', '*', '\\', '+', '%', ]; if (this.containStrs(name, black_list)) { return false; } return true; } // 检查字段名是否合法:data field, query field static checkProjection(name) { const black_list = [ '#', ' or ', ';', `'`, `"`, '`', '+', '-', '/', '\\', '%', ]; if (this.containStrs(name, black_list)) { return false; } return true; } static containStrs(source, str_list) { for (const ch of str_list) { if (source.indexOf(ch) >= 0) return true; } return false; } // 是否为逻辑操作符 static isLogicOperator(key) { const keys = Object.keys(types_1.LOGIC_COMMANDS).map((k) => types_1.LOGIC_COMMANDS[k]); return keys.includes(key); } // 是否为查询操作符(QUERY_COMMANDS) static isQueryOperator(key) { const keys = Object.keys(types_1.QUERY_COMMANDS).map((k) => types_1.QUERY_COMMANDS[k]); return keys.includes(key); } // 是否为操作符 static isOperator(key) { return this.isLogicOperator(key) || this.isQueryOperator(key); } }