UNPKG

nxkit

Version:

This is a collection of tools, independent of any other libraries

837 lines (836 loc) 27.9 kB
"use strict"; /* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2015, xuewen.chu * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of xuewen.chu nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL xuewen.chu BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ Object.defineProperty(exports, "__esModule", { value: true }); const util_1 = require("./util"); const path = require("path"); const xml_1 = require("./xml"); const db_1 = require("./db"); const fs = require("./fs"); const model_1 = require("./model"); const mysql_1 = require("./mysql"); const original_handles = {}; const original_files = {}; const REG = /\{(.+?)\}/g; const _eval = (s) => globalThis.eval(s); exports.defaultConfig = { db: db_1.defaultOptions, type: 'mysql', redis: undefined, original: '', }; ; class DataAccessShortcuts extends Proxy { constructor(ds, table, info) { var target = {}; super(target, { get(target, methodName, receiver) { var method = target[methodName]; if (!method) { var type = info[methodName]; util_1.default.assert(type, `Dao table method not defined, ${table}.${methodName}`); var fullname = table + '/' + methodName; method = ((param, options) => ds[type](fullname, param, options)); method.exec = (param, options) => ds.exec(fullname, param, options); if (type == 'gets') { method.get = (param, options) => ds.get(fullname, param, options); } target[methodName] = method; } return method; } }); } } exports.DataAccessShortcuts = DataAccessShortcuts; class DataAccessObject extends Proxy { constructor(ds, info) { var target = {}; super(target, { get(target, tableName, receiver) { var shortcuts = target[tableName]; if (!shortcuts) { var methodsInfo = info[tableName]; util_1.default.assert(methodsInfo, `Dao table not defined, ${tableName}`); target[tableName] = new DataAccessShortcuts(ds, name, methodsInfo); } return shortcuts; } }); } } exports.DataAccessObject = DataAccessObject; /** * @createTime 2012-01-18 * @author xuewen.chu <louis.tru@gmail.com> */ class Transaction { constructor(host) { this.map = host; this.db = get_db(host); this.db.transaction(); // start transaction this.m_on = true; this.dao = new DataAccessObject(this, host.tablesInfo); } primaryKey(table) { return this.map.primaryKey(table); } /** * @func get(name, param) */ get(name, param, opts) { return funcs.get(this.map, this.db, this.m_on, name, param, opts); } /** * @func gets(name, param) */ gets(name, param, opts) { return funcs.gets(this.map, this.db, this.m_on, name, param, opts); } /** * @func post(name, param) */ post(name, param, opts) { return funcs.post(this.map, this.db, this.m_on, name, param, opts); } /** * @func exec(name, param, cb) */ exec(name, param, opts) { return funcs.exec(this.map, this.db, this.m_on, name, param, opts); } /** * commit transaction */ commit() { this.m_on = false; this.db.commit(); this.db.close(); } /** * rollback transaction */ rollback() { this.m_on = false; this.db.rollback(); this.db.close(); } } /** * @createTime 2012-01-18 * @author xuewen.chu <louis.tru@gmail.com> */ function parse_map_node(self, el) { var ls = []; var obj = { method: el.tagName, child: ls, props: {} }; var attributes = el.attributes; for (var i = 0, l = attributes.length; i < l; i++) { var n = attributes.item(i); obj.props[n.name] = n.value; } var ns = el.childNodes; for (i = 0; i < ns.length; i++) { var node = ns.item(i); switch (node.nodeType) { case xml_1.NODE_TYPE.ELEMENT_NODE: ls.push(parse_map_node(self, node)); break; case xml_1.NODE_TYPE.TEXT_NODE: case xml_1.NODE_TYPE.CDATA_SECTION_NODE: ls.push(node.nodeValue); break; } } return obj; } function read_original_mapinfo(self, original_path, table) { var doc = new xml_1.default(); doc.load(fs.readFileSync(original_path + '.xml').toString('utf8')); var ns = doc.getElementsByTagName('map'); if (!ns.length) throw new Error(name + ' : not map the root element'); var map = ns.item(0); if (!map /*|| map.nodeType != NODE_TYPE.ELEMENT_NODE*/) throw new Error('map cannot empty'); var attrs = {}; var infos = {}; var map_attrs = map.attributes; for (var i = 0; i < map_attrs.length; i++) { var attr = map_attrs.item(i); attrs[attr.name] = attr.value; } attrs.primaryKey = (attrs.primaryKey || `${table}_id`); // default primaryKey ns = map.childNodes; for (var i = 0; i < ns.length; i++) { var node = ns.item(i); if (node.nodeType === xml_1.NODE_TYPE.ELEMENT_NODE) { var info = parse_map_node(self, node); info.is_select = (info.method.indexOf('select') > -1); info.table = table; infos[node.tagName] = info; original_handles[original_path + '/' + node.tagName] = info; } } original_files[original_path] = fs.statSync(original_path + '.xml').mtime; return { attrs, infos }; } function get_original_mapinfo(self, name) { var info = self.m_original_mapinfo[name]; if (info && !util_1.default.debug) { return info; } var table_name = path.dirname(name); var method_name = path.basename(name); var original_path = path.resolve(self.original, table_name); if (original_path in original_files) { if (util_1.default.debug) { if (fs.statSync(original_path + '.xml').mtime != original_files[original_path]) { read_original_mapinfo(self, original_path, table_name); } } } else { read_original_mapinfo(self, original_path, table_name); } info = original_handles[original_path + '/' + method_name]; self.m_original_mapinfo[name] = info; if (!info) { throw new Error(name + ' : can not find the map'); } return info; } //get db function get_db(self) { var db_class = null; switch (self.type) { case 'mysql': db_class = mysql_1.Mysql; break; case 'mssql': case 'oracle': default: break; } util_1.default.assert(db_class, 'Not supporting database, {0}', self.type); return new db_class(self.config.db); } // exec script function execExp(self, exp, param) { return _eval(`(function (g, ctx){with(ctx){return(${exp})}})`)(globalThis, param); } //format sql function format_sql(self, sql, param) { return sql.replace(REG, function (all, exp) { return db_1.default.escape(execExp(self, exp, param)); }); } // join map function parse_sql_join(self, item, param, asql) { var name = item.props.name || 'ids'; var value = param[name]; if (!value) return ''; var ls = Array.toArray(value); for (var i = 0, l = ls.length; i < l; i++) { ls[i] = db_1.default.escape(ls[i]); } asql.sql.push(ls.join(item.props.value || ',')); } // if function parse_if_sql(self, el, param, options, asql, is_select, is_total) { var props = el.props; var exp = props.exp; var name = props.name; var not = name && name[0] == '!'; if (not) { name = name.substr(1); } if (props.default && name) { param = { [name]: props.default, ...param }; } if (exp) { if (!execExp(self, exp, param)) { return null; } } else if (name) { var val = param[name]; if (not) { if (val !== undefined && val !== null) { return null; } } else if (val === undefined || val === null) { return null; } } if (el.child.length) { parse_sql_ls(self, el.child, param, options, asql, is_select, is_total); } else { // name util_1.default.assert(name, 'name prop cannot be empty'); var val = param[name]; if (Array.isArray(val)) { asql.sql.push(` ${name} in (${val.map(e => db_1.default.escape(e)).join(',')}) `); } else { val = db_1.default.escape(val); if (val == "'NULL'" || val == "NULL") { asql.sql.push(` ${name} is NULL `); } else { asql.sql.push(` ${name} = ${val} `); } } } return { prepend: props.prepend, }; } // ls function parse_sql_ls(self, ls, param, options, asql, is_select, is_total) { var result_count = 0; for (var i = 0, l = ls.length; i < l; i++) { var el = ls[i]; var end_pos = asql.sql.length; if (typeof el == 'string') { var sql = format_sql(self, el, param).trim(); if (sql) { asql.sql.push(` ${sql} `); } } else { var tag = el.method; if (tag == 'if') { var r = parse_if_sql(self, el, param, options, asql, is_select, is_total); if (r && asql.sql.length > end_pos) { var prepend = result_count ? (r.prepend || '') + ' ' : ''; asql.sql[end_pos] = ' ' + prepend + asql.sql[end_pos]; } } else if (tag == 'where') { parse_sql_ls(self, el.child, param, options, asql, is_select, is_total); if (asql.sql.length > end_pos) { asql.sql[end_pos] = ' where' + asql.sql[end_pos]; if (options.where) { asql.sql[end_pos] += ' ' + options.where; } } else if (options.where) { asql.sql.push(' where ' + options.where.replace(/^.*?(and|or)/i, '')); } } else if (tag == 'join') { parse_sql_join(self, el, param, asql); } else if (is_select) { if (tag == 'out') { var value = ` ${el.props.value || '*'} `; if (el.child.length) { parse_sql_ls(self, el.child, param, options, asql, is_select, is_total); if (asql.sql.length > end_pos) { asql.out.push([end_pos, asql.sql.length - 1]); } else { asql.out.push([end_pos, end_pos]); asql.sql.push(value); } } else { asql.out.push([end_pos, end_pos]); asql.sql.push(value); } } else if (tag == 'group') { let value = param.group_str || el.props.default; if (value) { asql.group.push(end_pos); asql.sql.push(` group by ${value} `); if (asql.out.length) { var index = asql.out.indexReverse(0)[1]; asql.sql[index] += ' , count(*) as data_count '; asql.out.pop(); } } } else if (tag == 'order' && !is_total) { let value = param.order_str || el.props.default; if (value) { asql.order.push(end_pos); asql.sql.push(` order by ${value} `); } } else if (tag == 'limit') { let value = Number(param.limit) || Number(el.props.default); if (value) { asql.limit.push(end_pos); asql.sql.push(` limit ${value} `); } } else { //... } } } if (asql.sql.length > end_pos) { result_count++; } } } // parse sql str function parseSql(self, name, param, options, is_total) { var map = get_original_mapinfo(self, name); var asql = { sql: [], out: [], group: [], order: [], limit: [] }; if (map.is_select) { if (param.group) { param.group_str = ''; if (typeof param.group == 'string') { param.group_str = param.group; } else if (Array.isArray(param.group)) { param.group_str = param.group.join(','); } else { param.group_str = Object.entries(param.group).map((k, v) => `${k} ${v}`).join(','); } } if (param.order) { param.order_str = ''; if (typeof param.order == 'string') { param.order_str = param.order; } else if (Array.isArray(param.order)) { param.order_str = param.order.join(','); } else { param.order_str = Object.entries(param.order).map((k, v) => `${k} ${v}`).join(','); } } if (param.limit === '0') { // check type param.limit = 0; } if (is_total) { param.limit = 1; param.order_str = ''; } } var r = parse_if_sql(self, map, param, options, asql, map.is_select, is_total); if (map.is_select) { // limit if (param.limit && !asql.limit.length) { asql.limit.push(asql.sql.length); asql.sql.push(` limit ${param.limit}`); } // order if (param.order_str && !asql.order.length) { if (asql.limit.length) { var index = asql.limit.indexReverse(0); var sql = asql.sql[index]; asql.sql[index] = ` order by ${param.order_str} ${sql} `; asql.order.push(index); } else { asql.order.push(asql.sql.length); asql.sql.push(` order by ${param.order_str} `); } } // group if (param.group_str && !asql.group.length) { if (asql.order.length) { var index = asql.order.indexReverse(0); var sql = asql.sql[index]; asql.sql[index] = ` group by ${param.group_str} ${sql} `; asql.group.push(index); } else if (asql.limit.length) { var index = asql.limit.indexReverse(0); var sql = asql.sql[index]; asql.sql[index] = ` group by ${param.group_str} ${sql} `; asql.group.push(index); } else { asql.group.push(asql.sql.length); asql.sql.push(` group by ${param.group_str} `); } if (asql.out.length) { var index = asql.out.indexReverse(0)[1]; asql.sql[index] += ' , count(*) as data_count '; asql.out.pop(); } } else if (is_total) { if (asql.out.length) { var index = asql.out.indexReverse(0)[1]; asql.sql[index] += ' , count(*) as data_count '; asql.out.pop(); } } } var sql = asql.sql.join(''); map.sql = r ? String.format('{0} {1}', r.prepend || '', sql) : ''; map.cacheTime = Number(options.cacheTime || map.props.cacheTime); return map; } class LocalCache { constructor(host) { this.m_host = host; this.m_cache = new Map(); this.m_tntervalid = setInterval(() => this._ClearTimeout(), 3e4 /*30s*/); } async get(key) { var data = this.m_cache.get(key); if (data) { if (data.timeout > Date.now()) { this.m_cache.delete(key); return null; } return data.data; } return null; } async set(key, data, cacheTime = 0) { this.m_cache.set(key, { data: data, timeout: cacheTime ? Date.now() + cacheTime : 0 }); return true; } async remove(key) { this.m_cache.delete(key); return true; } _ClearTimeout() { var now = Date.now(); var cache = this.m_cache; for (var [key, data] of cache) { if (data.timeout) { if (data.timeout < now) { cache.delete(key); // clear data } } } } close() { clearInterval(this.m_tntervalid); this.m_cache = new Map(); } } exports.LocalCache = LocalCache; // exec query function exec(self, db, is_transaction, type, name, cb, param, options, is_total) { if (type == 'get') { param = { ...param, limit: 1 }; } else if (type == 'gets') { type = 'get'; param = { limit: 10, ...param }; } else { param = { ...param }; } // param = new Proxy(param, { // get: (target: QueryParams, name: string)=>target[name], // has:()=>true, // }); try { var map = parseSql(self, name, param, options || {}, is_total); var cacheTime = map.cacheTime; var sql = map.sql, key; var table = map.table; if (util_1.default.debug) { console.log(sql); } function handle(err, data) { if (!is_transaction) { db.close(); // Non transaction, shut down immediately after the query } if (err) { cb(err, []); } else { if (type == 'get') { if (cacheTime > 0) { self.cache.set(key, data, cacheTime); } } cb(null, data, table); } } if (type == 'get') { // use cache if (cacheTime > 0) { key = util_1.default.hash('get:' + sql); self.cache.get(key).then(e => { if (e) { cb(null, e, table); } else { db.query(sql, handle); } }).catch(e => { console.error(e); db.query(sql, handle); }); } else { db.query(sql, handle); } } else { db.query(sql, handle); } } catch (err) { if (db) { if (!is_transaction) { db.close(); } } cb(err, []); } } async function execAfterFetch(self, model, afterFetch) { if (afterFetch) { for (var i of afterFetch) { var args = i; if (!Array.isArray(i)) { args = [i]; } var [table, ..._args] = args; if (table[0] == '@') { await model.fetchChild(table.substr(1), ..._args); } else { await model.fetch(table, ..._args); } } } } const funcs = { get: async function (self, db, is_t, name, param, opts) { var { afterFetch, fetchTotal, onlyFetchTotal, ..._param } = (param || {}); var model = await new Promise((resolve, reject) => { exec(self, db, is_t, 'get', name, function (err, data, table) { if (err) { reject(err); } else { var [{ rows }] = data; var value = rows ? (rows[0] || null) : null; resolve(value ? new model_1.Model(value, { dataSource: self, table }) : null); } }, _param, opts); }); if (model) { await execAfterFetch(self, model, afterFetch); } return model; }, gets: async function (self, db, is_t, name, param, opts) { var { afterFetch, fetchTotal, onlyFetchTotal, ..._param } = param || {}; var total, table; if (fetchTotal || onlyFetchTotal) { total = await new Promise((resolve, reject) => { exec(self, db, is_t, 'get', name, function (err, data, t) { if (err) { reject(err); } else { table = t; var [{ rows }] = data; var value = rows ? (rows[0] || null) : null; resolve(value ? value.data_count : 0); } }, _param, opts, true); }); if (!total || onlyFetchTotal) { return Object.assign(new model_1.Collection([], { dataSource: self, table }), { total }); } } var col = await new Promise((resolve, reject) => { exec(self, db, is_t, 'gets', name, function (err, data, table) { if (err) { reject(err); } else { var [{ rows = [] }] = data; var value = rows.map((e) => new model_1.Model(e, { dataSource: self, table })); resolve(new model_1.Collection(value, { dataSource: self, table })); } }, _param, opts); }); if (Array.isArray(_param.limit) && _param.limit.length > 1) { col.index = Number(_param.limit[0]) || 0; } if (total) { col.total = total; } if (col.length) { await execAfterFetch(self, col, afterFetch); } return col; }, post: function (self, db, is_t, name, param, opts) { var { afterFetch, fetchTotal, onlyFetchTotal, ..._param } = param || {}; return new Promise((resolve, reject) => { exec(self, db, is_t, 'post', name, function (err, data) { if (err) { reject(err); } else { resolve(data[0]); } }, _param, opts); }); }, exec: function (self, db, is_t, name, param, opts) { var { afterFetch, fetchTotal, onlyFetchTotal, ..._param } = param || {}; return new Promise((resolve, reject) => { exec(self, db, is_t, 'query', name, function (err, data) { if (err) { reject(err); } else { resolve(data); } }, _param, opts); }); }, }; class SqlMap { /** * @constructor * @arg [conf] {Object} Do not pass use center server config */ constructor(conf) { var _a; this.m_original_mapinfo = {}; this.m_tables = {}; this.map = this; this.config = Object.assign({}, exports.defaultConfig, conf); this.config.db = Object.assign({}, exports.defaultConfig.db, (_a = conf) === null || _a === void 0 ? void 0 : _a.db); this.tablesInfo = {}; fs.readdirSync(this.original).forEach(e => { if (path.extname(e) == '.xml') { var name = path.basename(e); var table = name.substr(0, name.length - 4); var { attrs, infos } = read_original_mapinfo(this, this.original + '/' + table, table); var methods = {}; for (let [method, { props: { type } }] of Object.entries(infos)) { type = type || method.indexOf('select') >= 0 ? 'get' : 'post'; type = type == 'get' ? 'gets' : 'post'; methods[method] = type; } this.tablesInfo[table] = methods; this.m_tables[table] = attrs; } }); this.dao = new DataAccessObject(this, this.tablesInfo); this.m_cache = new LocalCache(this); } /** * @field {Cache} is use cache */ get cache() { return this.m_cache; } set cache(value) { this.m_cache = value; } /** * original xml base path */ get original() { return this.config.original || ''; } get type() { return this.config.type || 'mysql'; } primaryKey(table) { return this.m_tables[table].primaryKey; } /** * @func get(name, param) */ get(name, param, opts) { return funcs.get(this, get_db(this), false, name, param, opts); } /** * @func gets(name, param) */ gets(name, param, opts) { return funcs.gets(this, get_db(this), false, name, param, opts); } /** * @func post(name, param) */ post(name, param, opts) { return funcs.post(this, get_db(this), false, name, param, opts); } /** * @func query(name, param, cb) */ exec(name, param, opts) { return funcs.exec(this, get_db(this), false, name, param, opts); } /** * start transaction * @return {Transaction} */ transaction(cb) { util_1.default.assert(cb); var tr = new Transaction(this); return cb(tr, tr.dao).then((e) => { tr.commit(); return e; }).catch((e) => { tr.rollback(); throw e; }); } } exports.SqlMap = SqlMap; var shared = null; exports.default = { SqlMap: SqlMap, /** * @func setShared */ setShared: function (sqlmap) { shared = sqlmap; }, /** * get default dao * @return {SqlMap} * @static */ get shared() { return shared; }, };