nxkit
Version:
This is a collection of tools, independent of any other libraries
837 lines (836 loc) • 27.9 kB
JavaScript
;
/* ***** 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;
},
};