mm_os
Version:
MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。
2,019 lines (1,841 loc) • 52 kB
JavaScript
const Item = require('mm_machine').Drive;
const fs = require('fs');
if (!$.dict.user_id) {
$.dict.user_id = 'user_id';
}
var keyword_default =
"`user_id` in (SELECT `user_id` FROM `user_account` WHERE `nickname` LIKE '%{0}%' OR `phone` LIKE '%{0}%' OR `wallet_address` LIKE '%{0}%')";
var keyword_default_tip = '昵称、手机号、钱包地址';
/**
* db数据库开发驱动类
* @augments {Item}
* @class
*/
class Drive extends Item {
static config = {
// 库表名称(唯一标识)
name: '',
// 标题(中文名)
title: '',
// 表名
table: '',
// 主键,用于实体模型
key: '',
// 字段
fields: [
/* */
],
index: [],
// 主程序文件 - 默认为空
main: ''
};
/**
* 构造函数
* @param {object} config 配置参数
* @param {object} parent 父对象
* @class
*/
constructor(config, parent) {
super({ ...Drive.config, ...config }, parent);
}
}
/**
* 重置配置参数
*/
Drive.prototype._preset = function () {
this.query_string = ['name', 'title', 'keywords', 'tag', 'description', 'content'];
// 是否设置数值类型为可查询
this.query_number = ['state', 'uin'];
// 是否设置数值类型为关键词可查
this.query_keyword = ['name', 'title', 'keywords', 'tag', 'description'];
// 是否设置以下字段为get查列表SQL时不可见
this.get_not = ['password', 'salt', 'content'];
// 是否设置以下字段为getObj查对象SQL时不可见
this.get_obj_not = ['password', 'salt', 'display'];
// 是否设置默认该表仅用户可访问
this.query_default_table = ['user'];
};
/**
* 解析字段类型
* @param {string} type 字段类型
* @returns {object} 解析结果
*/
Drive.prototype._parseFieldType = function (type) {
var tp = type.left('(', true);
var len = type.between('(', ')');
var max = 0;
var decimal = 0;
if (len) {
max = Number(len.left(',', true));
var d = len.right(',');
if (d) {
decimal = Number(d);
}
}
return { tp, max, decimal };
};
/**
* 解析字段备注
* @param {string} note 字段备注
* @param {string} tp 字段类型
* @param {number} max_length 最大长度
* @returns {object} 解析结果
*/
Drive.prototype._parseFieldNote = function (note, tp, max_length) {
var proc_note = note.replace(':', ':').replace('(', '(').replace(')', ')');
var title = proc_note.left(':', true).trim();
var desc = proc_note.right(':');
var description = '';
var map = '';
var min = 0;
var max = 0;
var local_max_length = max_length;
if (desc) {
var range = desc.between('[', ']');
if (range) {
var min_str = range.left(',', true);
if (min_str) {
min = Number(min_str);
}
var max_str = range.right(',');
if (max_str) {
var n = Number(max_str);
if (tp === 'varchar') {
if (n < local_max_length) {
local_max_length = n;
}
} else {
max = n;
}
}
}
map = desc.between('(', ')');
description = desc.right(']', true).replace(`(${map})`, '').trim();
}
let min_length = 0;
if (min > 0) {
min_length = min;
}
return { title, description, map, min, max, min_length, max_length: local_max_length };
};
/**
* 计算字段最大值
* @param {string} tp 字段类型
* @param {number} max 当前最大值
* @param {number} max_length 最大长度
* @returns {number} 计算后的最大值
*/
Drive.prototype._calcMaxValue = function (tp, max, max_length) {
var local_max = max;
if (local_max === 0) {
switch (tp) {
case 'tinyint':
local_max = 1;
break;
case 'smallint':
local_max = 32767;
break;
case 'mediumint':
local_max = 8388607;
break;
case 'int':
local_max = 2147483647;
break;
case 'bigint':
local_max = 0;
break;
default:
break;
}
if (max_length) {
var num = Math.pow(10, max_length) - 1;
if (local_max > num) {
local_max = num;
}
}
}
return local_max;
};
/**
* 处理字段默认值
* @param {string} tp 字段类型
* @param {boolean} not_null 是否非空
* @param {*} default_val 默认值
* @returns {*} 处理后的默认值
*/
Drive.prototype._handleDefaultValue = function (tp, not_null, default_val) {
if (not_null && !default_val) {
if (tp === 'varchar' || tp === 'text') {
return '';
} else if (tp === 'datetime' || tp === 'timestamp') {
return 'CURRENT_TIMESTAMP';
} else {
return '0';
}
}
return default_val;
};
/**
* 创建字段模型对象
* @param {string} name 字段名
* @param {string} title 字段标题
* @param {string} description 字段描述
* @param {string} tp 字段类型
* @param {boolean} pk 是否主键
* @param {boolean} auto 是否自动
* @param {boolean} not_null 是否非空
* @param {number} min_length 最小长度
* @param {number} max_length 最大长度
* @param {number} min 最小值
* @param {number} max 最大值
* @param {number} decimal 小数位
* @param {*} default_val 默认值
* @param {object} map 转换编排
* @returns {object} 字段模型对象
* @private
*/
Drive.prototype._createFieldModel = function (
name, title, description, tp, pk, auto, not_null,
min_length, max_length, min, max, decimal, default_val, map
) {
return {
// 字段名
'name': name,
// 字段标题
'title': title,
// 字段描述
'description': description,
// 字段类型 smallint短整数、mediumint中长整数、int整数、float浮点数、double双精度、tinyint二进制(0和1的布尔)、text文本、varchar字符串、datetime日期时间、date日期、time时间、timestamp时间戳
'type': tp,
// 是否主键
'key': pk,
// 自动
'auto': auto,
// 是否含符号
'symbol': tp === 'float' || tp === 'decimal',
// 是否填充零,用于数字类型
'fill_zero': false,
// 非空
'not_null': not_null,
// 最小长度
'min_length': min_length,
// 最大长度
'max_length': max_length,
// 最小值
'min': min,
// 最大值
'max': max,
// 小数位
'decimal': decimal,
// 默认值
'default': default_val,
// 转换编排
'map': map
};
};
/**
* 创建字段模型
* @param {object} fields 字段对象
* @param {string} fields.name 字段名称
* @param {object} fields.default 默认值
* @param {object} fields.notnull 是否非空
* @param {object} fields.type 字段类型
* @param {object} fields.pk 是否主键
* @param {object} fields.auto 是否自动增长
* @param {object} fields.note 字段备注
* @returns {object} 返回字段模型
*/
Drive.prototype.model = function (fields) {
var {
name,
notnull,
type,
pk,
note,
auto
} = fields;
var { tp, max: max_length, decimal } = this._parseFieldType(type);
var {
title,
description,
map,
min,
max: parsedMax,
min_length,
max_length: parsedMaxLength
} = this._parseFieldNote(note, tp, max_length);
var max = this._calcMaxValue(tp, parsedMax, parsedMaxLength);
if (name.indexOf('_id') !== -1 || name == 'id') {
min = 0;
}
var not_null = notnull | pk | (tp !== 'varchar' && tp !== 'text');
var config = this.config;
if (pk && !config.key) {
config.key = name;
}
var default_value = this._handleDefaultValue(tp, not_null, fields.default);
return this._createFieldModel(
name, title, description, tp, pk, auto, not_null,
min_length, parsedMaxLength, min, max, decimal,
default_value, map
);
};
/**
* 创建索引字段模型
* @param {object} o 索引对象
* @property {string} o.Key_name 索引名称
* @property {string} o.Non_unique 是否唯一索引
* @property {string} o.Column_name 索引字段
* @property {string} o.Manager_comment 索引备注
* @returns {object} 返回索引模型
*/
Drive.prototype.newIndexModel = function (o) {
return {
name: o.Key_name,
type: o.Non_unique ? 'unique' : 'index',
fields: o.Column_name.replace(/`/g, '').split(','),
comment: o.Manager_comment
};
};
/**
* 从数据库更新索引配置
* @param {object} db 数据库管理器
* @returns {Array} 索引列表
*/
Drive.prototype._updateFile = async function (db) {
var sql = 'SHOW INDEX FROM `' + this.config.table + '`';
var rows = await db.run(sql);
var dict = {};
for (var i = 0; i < rows.length; i++) {
var o = rows[i];
if (o.Key_name !== 'PRIMARY') {
if (!dict[o.Key_name]) {
dict[o.Key_name] = {
Column_name: []
};
}
dict[o.Key_name].Column_name.push(o.Column_name);
}
}
var list = [];
for (var k in dict) {
var m = this.newIndexModel(dict[k]);
list.push(m);
}
return list;
};
/**
* 从数据库更新配置
* @param {object} db 数据库管理器
* @param {boolean} cover 是否覆盖文件
*/
Drive.prototype.updateFile = async function (db, cover) {
var cg = this.config;
var list = [];
// 查询表注释并修改
var sql = "SELECT TABLE_NAME, TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '" + db
.database() +
"' && TABLE_NAME = '" + cg.table + "';";
var lt = await db.run(sql);
if (lt && lt.length > 0) {
var commit = lt[0].TABLE_COMMENT;
var arr = commit.replace(':', ':').split(':');
cg.title = arr[0];
if (arr.length > 1) {
cg.description = arr[1];
}
}
// 设置表名
db.table = cg.table + '';
// 获取所有字段
var fields = await db.fields();
for (var i = 0; i < fields.length; i++) {
var field = this.model(fields[i]);
list.push(field);
}
cg.fields = list;
cg.index = await this._updateFile(db);
if (!cg.name) {
cg.name = cg.table;
}
await this.updateApp(cover);
};
/**
* 更新索引
* @param {object} db 数据库管理器
*/
Drive.prototype._updateDb = async function (db) {
var cg = this.config;
let index = cg.index || [];
// 更新索引
if (index) {
var len = index.length;
for (var i = 0; i < len; i++) {
var o = index[i];
var sql_start = '';
if (o.type === 'unique') {
sql_start = 'CREATE UNIQUE INDEX `';
} else {
sql_start = 'CREATE INDEX `';
}
await db.exec(sql_start + o.name + '` ON `' + table + '` (' + o.fields + ');');
}
}
};
/**
* 初始化数据表
* @param {object} db 数据库管理器
* @param {object} cg 配置对象
* @param {Array} list 字段列表
* @returns {Array} 字段列表
*/
Drive.prototype._initTable = async function (db, cg, list) {
var fields = await db.fields();
if (fields.length === 0) {
var k = cg.key;
var len = list.length;
for (var i = 0; i < len; i++) {
var o = list[i];
if (k === o.name) {
await db.addTable(cg.table, o.name, o.type, o.auto, cg.title + ':' + cg.description);
fields.push({
name: o.name
});
break;
}
}
} else {
var commit = cg.title + ':' + cg.description;
var sql = "alter table `{0}` comment '{1}';".replace('{0}', cg.table).replace('{1}', commit);
await db.exec(sql);
}
return fields;
};
/**
* 删除配置中没有的字段
* @param {object} db 数据库管理器
* @param {Array} fields 数据库字段列表
* @param {Array} list 配置字段列表
*/
Drive.prototype._deleteUnusedFields = async function (db, fields, list) {
for (var i = 0; i < fields.length; i++) {
var o = fields[i];
var obj = list.getObj({
name: o.name
});
if (!obj) {
await db.fieldDel(o.name);
}
}
};
/**
* 生成字段SQL
* @param {object} o 字段对象
* @returns {string} 字段SQL
*/
Drive.prototype._generateFieldSql = function (o) {
var type = this._getTypeSql(o);
var notnull = this._getNotNullSql(o);
var value = this._getValueSql(o);
var note = this._getNoteSql(o);
return this._buildFieldSql(o.name, type, notnull, value, note);
};
/**
* 获取类型SQL
* @param {object} o 字段对象
* @returns {string} 类型SQL
* @private
*/
Drive.prototype._getTypeSql = function (o) {
var type = o.type;
var max_len = o.max_length || 0;
if (max_len > 0) {
// decimal仅对浮点类型有效(int/smallint/bigint等使用decimal=0时不追加)
var is_float_type = o.type.indexOf('float') !== -1 ||
o.type.indexOf('double') !== -1 ||
o.type.indexOf('decimal') !== -1;
if (is_float_type && o.decimal !== undefined && o.decimal !== null) {
type += '(' + max_len + ',' + o.decimal + ')';
} else {
type += '(' + max_len + ')';
}
}
// 类型未包含unsigned且为非字符/日期类型时追加UNSIGNED
if (!o.symbol && type.indexOf('unsigned') === -1 &&
(o.type !== 'varchar' && o.type !== 'longtext' && o.type !== 'text' &&
o.type !== 'date' && o.type !== 'time' && o.type !== 'datetime' && o.type !== 'timestamp')) {
type += ' UNSIGNED';
}
return type;
};
/**
* 获取NOT NULL SQL
* @param {object} o 字段对象
* @returns {string} NOT NULL SQL
* @private
*/
Drive.prototype._getNotNullSql = function (o) {
var notnull = '';
if (this._isNotNullRequired(o)) {
notnull = 'NOT NULL';
}
if (this._isAutoInc(o)) {
notnull += ' AUTO_INCREMENT';
}
return notnull;
};
/**
* 检查是否需要NOT NULL
* @param {object} o 字段对象
* @returns {boolean} 是否需要NOT NULL
* @private
*/
Drive.prototype._isNotNullRequired = function (o) {
return o.not_null || this._isDateTimeType(o.type) || this._isNumericType(o.type);
};
/**
* 检查是否为日期时间类型
* @param {string} type 类型
* @returns {boolean} 是否为日期时间类型
* @private
*/
Drive.prototype._isDateTimeType = function (type) {
return type === 'date' || type === 'time' || type === 'datetime' || type === 'timestamp';
};
/**
* 检查是否为数字类型
* @param {string} type 类型
* @returns {boolean} 是否为数字类型
* @private
*/
Drive.prototype._isNumericType = function (type) {
return type !== 'varchar' && type !== 'longtext' && type !== 'text' && !this._isDateTimeType(type);
};
/**
* 检查是否需要AUTO_INCREMENT
* @param {object} o 字段对象
* @returns {boolean} 是否需要AUTO_INCREMENT
* @private
*/
Drive.prototype._isAutoInc = function (o) {
return !!o.auto && !this._isDateTimeType(o.type);
};
/**
* 获取默认值SQL
* @param {object} o 字段对象
* @returns {string} 默认值SQL
* @private
*/
Drive.prototype._getValueSql = function (o) {
if (this._isAutoTime(o)) {
return 'DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP';
}
if (this._isAutoInc(o)) {
return '';
}
if (o.default === null) {
return 'DEFAULT NULL';
}
if (o.default !== undefined) {
return this._getDefaultValueSql(o);
}
return this._getDefaultEmptySql(o);
};
/**
* 检查是否为自动时间
* @param {object} o 字段对象
* @returns {boolean} 是否为自动时间
* @private
*/
Drive.prototype._isAutoTime = function (o) {
return !!o.auto && this._isDateTimeType(o.type);
};
/**
* 获取默认值SQL
* @param {object} o 字段对象
* @returns {string} 默认值SQL
* @private
*/
Drive.prototype._getDefaultValueSql = function (o) {
if (this._isCurrentDefault(o)) {
return 'DEFAULT ' + o.default;
} else if (this._isStringType(o.type)) {
return "DEFAULT '" + o.default + "'";
} else {
return 'DEFAULT ' + o.default;
}
};
/**
* 检查是否为当前时间默认值
* @param {object} o 字段对象
* @returns {boolean} 是否为当前时间默认值
* @private
*/
Drive.prototype._isCurrentDefault = function (o) {
return this._isDateTimeType(o.type) && o.default && o.default.indexOf('CURR') !== -1;
};
/**
* 检查是否为字符串类型
* @param {string} type 类型
* @returns {boolean} 是否为字符串类型
* @private
*/
Drive.prototype._isStringType = function (type) {
return type === 'varchar' || type === 'longtext' || type === 'text' ||
type === 'date' || type === 'time' || type === 'datetime' || type === 'timestamp';
};
/**
* 获取空默认值SQL
* @param {object} o 字段对象
* @returns {string} 空默认值SQL
* @private
*/
Drive.prototype._getDefaultEmptySql = function (o) {
if (this._isStringType(o.type)) {
return "DEFAULT ''";
} else {
return 'DEFAULT 0';
}
};
/**
* 获取注释SQL
* @param {object} o 字段对象
* @returns {string} 注释SQL
* @private
*/
Drive.prototype._getNoteSql = function (o) {
var note = (o.title || '') + ':';
if (o.type === 'varchar' || o.type === 'text' || o.type === 'longtext') {
if (o.max_length) {
note += '[' + (o.min_length || '0') + ',' + o.max_length + ']';
} else if (o.min_length) {
note += '[' + o.min_length + ']';
}
} else {
if (o.max) {
note += '[' + (o.min || '0') + ',' + o.max + ']';
} else if (o.min) {
note += '[' + o.min + ']';
}
}
note += (o.description || '');
if (o.map) {
note += '(' + o.map + ')';
}
return note;
};
/**
* 构建字段SQL
* @param {string} name 字段名
* @param {string} type 类型
* @param {string} notnull NOT NULL
* @param {string} value 默认值
* @param {string} note 注释
* @returns {string} 字段SQL
* @private
*/
Drive.prototype._buildFieldSql = function (name, type, notnull, value, note) {
var sql = "`{1}` {2} {3} {4} COMMENT '{5}'";
return sql.replace('{1}', name).replace('{2}', type).replace('{3}', notnull).replace('{4}', value)
.replace('{5}', note);
};
/**
* 添加或修改字段
* @param {object} db 数据库管理器
* @param {string} table 表名
* @param {object} o 字段对象
* @param {Array} fields 数据库字段列表
*/
Drive.prototype._addOrUpdateField = async function (db, table, o, fields) {
var arr = fields.filter(function (f) {
return f.name === o.name;
});
var sql = this._generateFieldSql(o);
if (arr.length === 0) {
// 如果没有则添加
await db.exec('alter table `{0}` add '.replace('{0}', table) + sql);
} else {
// 如果有则修改
await db.exec('alter table `{0}` change `{1}` '.replace('{0}', table).replace('{1}', o.name) + sql);
}
};
/**
* 通过配置更新数据库
* @param {object} db 数据库管理器
* @returns {string} 更新成功返回空,否则返回错误提示
*/
Drive.prototype.updateDb = async function (db) {
var cg = this.config;
var table = cg.table + '';
var list = cg.fields;
db.table = table;
var fields = await this._initTable(db, cg, list);
if (fields.length > 0) {
// 删除配置中没有的字段
await this._deleteUnusedFields(db, fields, list);
// 添加或修改配置
var len = list.length;
for (var i = 0; i < len; i++) {
var o = list[i];
await this._addOrUpdateField(db, table, o, fields);
}
} else {
return '数据表更新失败';
}
};
/**
* 确定目录结构
* @param {object} cg 配置对象
* @returns {object} 目录信息
* @private
*/
Drive.prototype._deteDir = function (cg) {
var arr = cg.table.split('_');
var scope = arr[0];
var p = './app/'.fullname();
var dir = ('./' + scope).fullname(p);
var dir_api = ('./plugin/server').fullname(dir);
var db_dir = ('./db').fullname(dir_api);
var config_file = ('./' + cg.table.replace(scope + '_', '') + '.db.json').fullname(db_dir);
return {
scope,
dir,
dir_api,
db_dir,
config_file
};
};
/**
* 确保目录存在
* @param {string} dir 目录路径
* @private
*/
Drive.prototype._ensureDir = function (dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
};
/**
* 处理配置文件
* @param {string} f 文件路径
* @param {boolean} cover 是否覆盖文件
* @private
*/
Drive.prototype._handleConfigFile = function (f, cover) {
if (!f.hasFile()) {
this.save();
} else if (cover) {
var jobj = f.loadJson();
var o = { ...this.config };
if (!o.title) {
delete o.title;
}
if (!o.description) {
delete o.description;
}
$.push(jobj, o, true);
f.saveText(JSON.stringify(jobj, null, 2));
}
};
/**
* 更新应用,根据表生成目录结构和文件
* @param {boolean} cover 是否覆盖文件
*/
Drive.prototype.updateApp = async function (cover) {
var cg = this.config;
var f;
var dir_api;
if (!this.config_file) {
var dirs = this._deteDir(cg);
this._ensureDir(dirs.dir);
this._ensureDir(dirs.dir + '/plugin');
this._ensureDir(dirs.dir_api);
this._ensureDir(dirs.db_dir);
this._dir = dirs.db_dir;
this.config_file = dirs.config_file;
f = dirs.config_file;
dir_api = dirs.dir_api;
} else {
if (this.config_file.endsWith('.db.json')) {
f = this.config_file;
} else {
f = this.config_file + '.db.json';
}
}
if (f) {
f.addDir();
if (!dir_api) {
dir_api = f.dirname().dirname();
}
// 处理db配置文件,生成xxx.db.json文件
this._handleConfigFile(f, cover);
// 更新API及相关配置文件
this.updateApi(dir_api, cover);
}
};
/**
* 确保API目录存在
* @param {string} dir 目录路径
* @private
*/
Drive.prototype._ensureApiDir = function (dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
};
/**
* 确定API目录路径
* @param {string} dir 基础目录
* @param {string} app 应用名称
* @param {string} type 类型 (client 或 manage)
* @param {string} name 目录名称
* @returns {string} 完整目录路径
* @private
*/
Drive.prototype._getApiDir = function (dir, app, type, name) {
var l = $.slash;
let event_api = $.admin.event('api');
var o = event_api.getMod(app + '_' + type);
var api_dir;
if (o) {
api_dir = dir + l + 'api_' + app + '_' + type;
} else {
api_dir = dir + l + 'api_' + type;
}
this._ensureApiDir(api_dir);
api_dir += '/' + name;
this._ensureApiDir(api_dir);
return api_dir;
};
/**
* 更新API及相关配置文件
* @param {string} dir API存放目录
* @param {boolean} cover 是否覆盖文件
*/
Drive.prototype.updateApi = async function (dir, cover) {
var cg = this.config;
var arr = cg.table.split('_');
var name = 'root';
if (arr.length > 1) {
name = cg.table.replace(arr[0], '').trim('_');
}
var app = arr[0];
var client = this._getApiDir(dir, app, 'client', name);
var manage = this._getApiDir(dir, app, 'manage', name);
await this.newSql(client, manage, cover);
await this.newParam(client, manage, cover);
this.newApi(client, manage, cover);
};
/**
* 新建event配置文件和文件
* @param {string} dir 保存的路径
* @param {string} path 检索路径
* @param {string} scope 接口域
*/
Drive.prototype.newEvent = async function (dir, path, scope) {
var f = dir + '/main.js';
if (!f.hasFile()) {
var code = (__dirname + '/event_script.js').loadText();
code = code.replaceAll('{0}', path).replaceAll('{1}', scope);
f.saveText(code);
}
};
/**
* 处理带逗号的列表映射
* @param {Array} list 列表
* @returns {Array} 处理后的列表
* @private
*/
Drive.prototype._handleCommaList = function (list) {
return list.map((o) => {
var arr = o.split(',');
var value = arr[0];
if (arr.length > 0) {
return {
name: arr[1],
value: value
};
} else {
return {
name: value,
value
};
}
});
};
/**
* 处理数字列表映射
* @param {Array} list 列表
* @param {string} map 映射字符串
* @returns {Array} 处理后的列表
* @private
*/
Drive.prototype._handleNumberList = function (list, map) {
if (map.indexOf('0') !== 0) {
list.unshift('');
}
return list.map((value) => {
return value.replace(/[0-9]+/, '');
});
};
/**
* 处理简单列表映射
* @param {Array} list 列表
* @returns {Array} 处理后的列表
* @private
*/
Drive.prototype._handleSimpleList = function (list) {
return list.map((value) => {
return {
name: value,
value
};
});
};
/**
* 处理点分隔的映射
* @param {object} obj 字段对象
* @param {string} map 映射字符串
* @returns {object} 处理后的格式对象
* @private
*/
Drive.prototype._handleDotMap = function (obj, map) {
var arr = map.split('.');
var format = {
table: arr[0],
name: arr[1]
};
if (arr.length > 2) {
format.id = arr[2];
} else {
format.id = obj.name;
}
return format;
};
/**
* 处理简单映射
* @param {object} obj 字段对象
* @param {string} map 映射字符串
* @returns {object} 处理后的格式对象
* @private
*/
Drive.prototype._handleSimpleMap = function (obj, map) {
return {
table: map,
id: obj.name,
name: 'name'
};
};
/**
* 获取格式
* @param {object} obj 字段对象
* @returns {object} 返回格式
*/
Drive.prototype.getFormat = async function (obj) {
var map = obj.map;
var format = {
key: obj.name,
title: obj.title.replace('ID', '').replace('id', '')
};
if (map.indexOf('|') !== -1) {
var list = map.split('|');
if (map.indexOf(',') !== -1) {
format.list = this._handleCommaList(list);
} else if (/^[0-9]+/.test(map)) {
format.list = this._handleNumberList(list, map);
} else {
format.list = this._handleSimpleList(list);
}
} else {
if (map.indexOf('.') !== -1) {
Object.assign(format, this._handleDotMap(obj, map));
} else {
Object.assign(format, this._handleSimpleMap(obj, map));
}
}
return format;
};
/**
* 处理字段选择
* @param {string} field_name 字段名
* @param {string} field_type 字段类型
* @returns {object} 处理结果
* @private
*/
Drive.prototype._procFieldSel = function (field_name, field_type) {
var res = {
field: '',
field_obj: ''
};
if (this.isCan(field_name, this.get_not, field_type)) {
res.field += ',`' + field_name + '`';
}
if (this.isCan(field_name, this.get_obj_not)) {
res.field_obj += ',`' + field_name + '`';
}
return res;
};
/**
* 处理字符串类型字段
* @param {object} query 查询对象
* @param {string} field_name 字段名
* @param {string} keyword 关键词
* @returns {string} 更新后的关键词
* @private
*/
Drive.prototype._procStringQuery = function (query, field_name, keyword) {
var new_keyword = keyword;
query[field_name] = '`' + field_name + "` like '%{0}%'";
if (this.isSet(field_name, this.query_keyword)) {
new_keyword += ' || `' + field_name + "` like '%{0}%'";
}
return new_keyword;
};
/**
* 处理日期时间类型字段
* @param {object} query 查询对象
* @param {string} field_name 字段名
* @private
*/
Drive.prototype._procDateTimeQuery = function (query, field_name) {
query[field_name + '_min'] = '`' + field_name + "` >= '{0}'";
query[field_name + '_max'] = '`' + field_name + "` <= '{0}'";
};
/**
* 处理数字类型字段
* @param {object} query 查询对象
* @param {object} update_obj 更新对象
* @param {string} field_name 字段名
* @param {string} uid 用户ID
* @param {boolean} query_default_user 是否默认用户查询
* @param {string} orderby 排序
* @returns {object} 处理结果
* @private
*/
Drive.prototype._procNumberQuery = function (
query, update_obj, field_name, uid, query_default_user, orderby
) {
var res = {
orderby: orderby,
update: { ...update_obj }
};
if (!field_name.endsWith('id')) {
query[field_name + '_min'] = '`' + field_name + '` >= {0}';
query[field_name + '_max'] = '`' + field_name + '` <= {0}';
res.update[field_name + '_add'] = '`' + field_name + '` = `' + field_name + '` + {0}';
if (field_name === 'sort' || field_name === 'display' || field_name === 'orderby') {
res.orderby = '`' + field_name + '` asc';
}
} else if (field_name === uid && query_default_user) {
res.query_default = '`' + field_name + '` = {' + uid + '}';
} else if (field_name === 'available' || field_name === 'show') {
res.query_default = '`' + field_name + '` = 1';
}
return res;
};
/**
* 处理字段类型
* @param {string} type 字段类型
* @param {string} field_name 字段名
* @param {object} query 查询对象
* @param {object} update_obj 更新对象
* @param {string} uid 用户ID
* @param {boolean} query_default_user 是否默认用户查询
* @param {string} orderby 排序
* @param {string} keyword 关键词
* @returns {object} 处理结果
* @private
*/
Drive.prototype._procFieldType = function (
type, field_name, query, update_obj, uid, query_default_user, orderby, keyword
) {
var result = {
keyword: keyword,
orderby: orderby,
update_obj: update_obj,
query_default: {}
};
if (type === 'varchar' || type === 'text' || type === 'longtext') {
result.keyword = this._procStringQuery(query, field_name, keyword);
} else if (type === 'date' || type === 'time' || type === 'datetime' || type === 'timestamp') {
this._procDateTimeQuery(query, field_name);
} else if (type !== 'tinyint') {
var num_res = this._procNumberQuery(
query, update_obj, field_name, uid, query_default_user, orderby
);
result.orderby = num_res.orderby;
result.update_obj = num_res.update;
if (num_res.query_default) {
result.query_default[field_name] = num_res.query_default;
}
}
return result;
};
/**
* 处理单个字段
* @param {object} o 字段对象
* @param {object} query 查询对象
* @param {object} update_obj 更新对象
* @param {string} uid 用户ID
* @param {boolean} query_default_user 是否默认用户查询
* @param {string} orderby 排序
* @param {string} keyword 关键词
* @returns {object} 处理结果
* @private
*/
Drive.prototype._procSingleField = async function (
o, query, update_obj, uid, query_default_user, orderby, keyword
) {
var { type, name } = o;
var result = {
field: '*',
field_obj: '*',
keyword: keyword,
orderby: orderby,
update_obj: update_obj,
query_default: {},
format: []
};
var field_sel = this._procFieldSel(name, type);
result.field = field_sel.field;
result.field_obj = field_sel.field_obj;
var type_info = this._procFieldType(
type, name, query, update_obj, uid, query_default_user, orderby, keyword
);
result.keyword = type_info.keyword;
result.orderby = type_info.orderby;
result.update_obj = type_info.update_obj;
result.query_default = type_info.query_default;
if (o.map) {
var fmt = await this.getFormat(o);
if (fmt) {
result.format.push(fmt);
}
}
return result;
};
/**
* 处理字段和查询条件
* @param {Array} fields 字段列表
* @param {string} table 表名
* @returns {object} 处理结果
* @private
*/
Drive.prototype._procFields = async function (fields, table) {
var query = {};
var update_obj = {};
var field = '';
var field_obj = '';
var query_default = {};
var format = [];
var orderby = '';
var uid = $.dict.user_id;
var keyword = '';
var query_default_user = this.isSet(table, this.query_default_table);
for (var i = 0; i < fields.length; i++) {
var field_info = await this._procSingleField(
fields[i], query, update_obj, uid, query_default_user, orderby, keyword
);
field += field_info.field;
field_obj += field_info.field_obj;
keyword = field_info.keyword;
orderby = field_info.orderby;
update_obj = field_info.update_obj;
// 合并查询默认值
for (var key in field_info.query_default) {
query_default[key] = field_info.query_default[key];
}
// 合并格式
format = format.concat(field_info.format);
}
return {
query,
update: update_obj,
field: field.trim(','),
field_obj: field_obj.trim(','),
query_default,
format,
orderby,
keyword
};
};
/**
* 处理关键字搜索
* @param {string} keyword 关键字
* @param {string} table 表名
* @returns {object} 处理后的查询对象
* @private
*/
Drive.prototype._procKeyword = function (keyword, table) {
var new_keyword = keyword;
if (table.indexOf('user_') !== -1) {
new_keyword += ' || ' + keyword_default;
}
var query = {};
if (new_keyword) {
query['keyword'] = '(' + new_keyword.replace(' || ', '') + ')';
}
return query;
};
/**
* 创建基础模型
* @param {object} cg 配置对象
* @param {object} processed 处理结果
* @param {object} keyword_query 关键字查询
* @returns {object} 基础模型
* @private
*/
Drive.prototype._createSqlConfig = function (cg, processed, keyword_query) {
return {
name: cg.table,
title: cg.title,
table: cg.table,
key: cg.key,
orderby_default: '`' + cg.key + '` desc',
field_obj: processed.field_obj,
field_default: processed.field,
method: 'get get_obj avg sum count',
query: { ...processed.query, ...keyword_query },
query_default: processed.query_default,
update: processed.update,
format: processed.format
};
};
/**
* 保存客户端配置
* @param {string} client 客户端配置保存路径
* @param {object} base_model 基础模型
* @param {string} orderby 排序
* @param {string} uid 用户ID
* @param {string} table 表名
* @param {boolean} cover 是否覆盖文件
* @private
*/
Drive.prototype._saveClientConfig = function (client, base_model, cover, orderby, uid, table) {
var oj = { ...base_model };
if (orderby) {
oj.orderby_default = orderby;
}
if (oj.orderby_default && oj.query_default[uid] && table.indexOf('user_') !== -1) {
oj.filter = {
'table': 'table',
'page': 'page',
'size': 'size',
'method': 'method',
'orderby': 'orderby',
'field': 'field',
'count_ret': 'count_ret'
};
oj.filter[uid] = uid;
}
this.saveFile(client + '/sql.json', oj, cover);
};
/**
* 保存管理端配置
* @param {string} manage 管理端配置保存路径
* @param {object} base_model 基础模型
* @param {boolean} cover 是否覆盖文件
* @private
*/
Drive.prototype._saveManageConfig = function (manage, base_model, cover) {
var m = { ...base_model };
delete m.method;
m.field_hide = [];
m.name += 2;
m.field_obj = (m.field_obj || '*').replace(',`time_create`', '').replace(',`time_update`', '')
.replace(',`create_time`', '').replace(',`update_time`', '');
delete m.query_default;
this.saveFile(manage + '/sql.json', m, cover);
};
/**
* 新建sql配置文件
* @param {string} client 客户端配置保存路径
* @param {string} manage 管理端配置保存路径
* @param {boolean} cover 是否覆盖文件
*/
Drive.prototype.newSql = async function (client, manage, cover) {
var cg = this.config;
var uid = $.dict.user_id;
var processed = await this._procFields(cg.fields, cg.table);
var keyword_query = this._procKeyword(processed.keyword, cg.table);
var base_model = this._createSqlConfig(cg, processed, keyword_query);
if (client) {
this._saveClientConfig(client, base_model, cover, processed.orderby, uid, cg.table);
}
if (manage) {
this._saveManageConfig(manage, base_model, cover);
}
};
/**
* 保存sql配置
* @param {string} file 文件名
* @param {object} model 配置模型
* @param {boolean} cover 是否覆盖文件
*/
Drive.prototype.saveFile = function (file, model, cover) {
if (!file.hasFile()) {
file.saveText(JSON.stringify(model, null, 2));
} else if (cover) {
var jobj = file.loadJson();
for (var k in model) {
var val = model[k];
if (!val) {
model[k] = jobj[k];
}
}
$.push(jobj, model, true);
file.saveText(JSON.stringify(jobj, null, 2));
}
};
/**
* 新建param配置文件
* @param {string} client 客户端配置保存路径
* @param {string} manage 管理端配置保存路径
* @param {boolean} cover 是否覆盖文件
*/
Drive.prototype.newParam = async function (client, manage, cover) {
var cg = this.config;
var lt = cg.fields;
var cm = this._initParamConfig(cg);
var keyword = '';
for (var i = 0; i < lt.length; i++) {
var o = lt[i];
var p = o.type;
var n = o.name;
var m = this._createParamModel(o, p, n);
if (this._isStringType(p)) {
keyword = this._processStringField(cm, m, o, n, keyword);
} else if (this._isDateTimeType(p)) {
this._processDateTimeField(cm, m, o, n);
} else {
this._processNumberField(cm, m, o, n, cg);
}
}
this._addKeywordConfig(cm, keyword, cg);
this._saveParamFiles(cm, client, manage, cover);
};
/**
* 初始化参数配置对象
* @param {object} cg 配置对象
* @returns {object} 参数配置对象
* @private
*/
Drive.prototype._initParamConfig = function (cg) {
return {
name: cg.table,
title: cg.title,
add: {
body: [],
body_required: []
},
del: {
query: [],
query_required: []
},
set: {
query: [],
query_required: [],
body: [],
body_required: [],
body_not: []
},
get: {
query: [],
query_required: []
},
get_obj: {
query_required: []
},
list: []
};
};
/**
* 创建参数模型
* @param {object} field 字段对象
* @param {string} type 字段类型
* @param {string} name 字段名
* @returns {object} 参数模型对象
* @private
*/
Drive.prototype._createParamModel = function (field, type, name) {
return {
name: name,
title: field.title,
description: field.description + (field.map ? '(' + field.map + ')' : ''),
key: field.pk,
type: '',
dataType: type
};
};
/**
* 检查是否为字符串类型
* @param {string} type 字段类型
* @returns {boolean} 是否为字符串类型
* @private
*/
Drive.prototype._isStringType = function (type) {
return type === 'varchar' || type === 'text' || type === 'longtext';
};
/**
* 检查是否为日期时间类型
* @param {string} type 字段类型
* @returns {boolean} 是否为日期时间类型
* @private
*/
Drive.prototype._isDateTimeType = function (type) {
return type === 'date' || type === 'time' || type === 'datetime' || type === 'timestamp';
};
/**
* 处理字符串类型字段
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {object} o 字段对象
* @param {string} n 字段名
* @param {string} keyword 关键词
* @returns {string} 更新后的关键词
* @private
*/
Drive.prototype._processStringField = function (cm, m, o, n, keyword) {
var new_keyword = keyword;
m.type = 'string';
m.default = o.default;
m.string = {};
this._addStringRange(m, o);
this._addStringFormat(m, n);
if (o.not_null) {
m.string.not_empty = !!o.not_null;
cm.add.body_required.push(n);
} else {
cm.add.body.push(n);
}
if (this.isSet(n, this.query_string)) {
cm.get.query.push(n);
cm.set.query.push(n);
new_keyword += '、' + o.title + '(' + n + ')';
}
cm.set.body.push(n);
cm.list.push(m);
return new_keyword;
};
/**
* 添加字符串范围验证
* @param {object} m 模型对象
* @param {object} o 字段对象
* @private
*/
Drive.prototype._addStringRange = function (m, o) {
var range = (o.min_length && o.max_length) ? [o.min_length, o.max_length] : [];
if (range.length > 0) {
m.string.range = range;
} else if (o.min_length) {
m.string.min = o.min_length;
} else if (o.max_length) {
m.string.max = o.max_length;
}
};
/**
* 检查字段名是否包含指定关键词
* @param {string} field_name 字段名
* @param {string} keyword 关键词
* @returns {boolean} 是否包含
* @private
*/
Drive.prototype._hasKeyword = function (field_name, keyword) {
return field_name.has(keyword);
};
/**
* 检查字段名是否等于指定值
* @param {string} field_name 字段名
* @param {Array} values 值数组
* @returns {boolean} 是否等于
* @private
*/
Drive.prototype._isEqual = function (field_name, values) {
return values.includes(field_name);
};
/**
* 添加字符串格式验证
* @param {object} m 模型对象
* @param {string} n 字段名
* @private
*/
Drive.prototype._addStringFormat = function (m, n) {
if (this._hasKeyword(n, 'phone') || this._isEqual(n, ['tel'])) {
m.string.format = 'phone';
} else if (this._hasKeyword(n, 'url') || this._isEqual(n, ['src', 'source'])) {
m.string.format = 'url';
} else if (this._hasKeyword(n, 'date')) {
m.string.format = 'date';
} else if (this._isEqual(n, ['num', 'number', 'count'])) {
m.string.format = 'digits';
} else if (this._isEqual(n, ['money', 'coin'])) {
m.string.format = 'number';
} else if (this._hasKeyword(n, 'email')) {
m.string.format = 'email';
}
};
/**
* 处理日期时间类型字段
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {object} o 字段对象
* @param {string} n 字段名
* @private
*/
Drive.prototype._processDateTimeField = function (cm, m, o, n) {
var format = (o.type === 'timestamp') ? 'datetime' : o.type;
m.type = 'string';
m.default = '';
m.string = {
not_empty: true,
format: format
};
cm.list.push(m);
this._addDateTimeRange(cm, m, n);
if (n.indexOf('create') !== -1 && n.indexOf('update') !== -1) {
cm.add.body.push(n);
cm.set.body.push(n);
}
cm.get.query.push(n + '_min');
cm.get.query.push(n + '_max');
};
/**
* 添加日期时间范围模型
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {string} n 字段名
* @private
*/
Drive.prototype._addDateTimeRange = function (cm, m, n) {
var m_min = { ...m };
m_min.name = n + '_min';
m_min.title += '——开始时间';
cm.list.push(m_min);
var m_max = { ...m };
m_max.name = n + '_max';
m_max.title += '——结束时间';
cm.list.push(m_max);
};
/**
* 处理数字类型字段
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {object} o 字段对象
* @param {string} n 字段名
* @param {object} cg 配置对象
* @private
*/
Drive.prototype._processNumberField = function (cm, m, o, n, cg) {
m.type = 'number';
m.default = Number(o.default) + '';
m.number = {};
this._addNumberRange(m, o);
if (o.name === cg.key) {
this._processPrimaryKey(cm, m, n);
} else {
this._processNonPrimaryKey(cm, m, n);
}
};
/**
* 添加数字范围验证
* @param {object} m 模型对象
* @param {object} o 字段对象
* @private
*/
Drive.prototype._addNumberRange = function (m, o) {
var range = (o.min && o.max) ? [o.min, o.max] : [];
if (range.length > 0) {
m.number.range = range;
} else if (o.min) {
m.number.min = o.min;
} else if (o.max) {
m.number.max = o.max;
}
};
/**
* 处理主键字段
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {string} n 字段名
* @private
*/
Drive.prototype._processPrimaryKey = function (cm, m, n) {
cm.del.query_required.push(n);
cm.set.query.push(n);
cm.get.query.push(n);
cm.get_obj.query_required.push(n);
cm.list.push(m);
};
/**
* 处理非主键字段
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {string} n 字段名
* @private
*/
Drive.prototype._processNonPrimaryKey = function (cm, m, n) {
cm.add.body.push(n);
cm.set.body.push(n);
cm.list.push(m);
if (m.dataType !== 'tinyint') {
this._processNonTinyInt(cm, m, n);
} else {
cm.set.query.push(n);
cm.get.query.push(n);
}
};
/**
* 处理非tinyint字段
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {string} n 字段名
* @private
*/
Drive.prototype._processNonTinyInt = function (cm, m, n) {
var ne = n;
if (!ne.endsWith('id')) {
this._addNumberRangeFields(cm, m, n);
} else {
cm.get.query.push(n);
if (this.isSet(n, this.query_number)) {
cm.set.query.push(n);
}
}
};
/**
* 添加数字范围字段
* @param {object} cm 参数配置对象
* @param {object} m 模型对象
* @param {string} n 字段名
* @private
*/
Drive.prototype._addNumberRangeFields = function (cm, m, n) {
cm.get.query.push(n + '_min');
cm.get.query.push(n + '_max');
cm.set.query.push(n + '_min');
cm.set.query.push(n + '_max');
cm.set.body.push(n + '_add');
var m_min = { ...m };
m_min.name = n + '_min';
m_min.title += '——最小值';
cm.list.push(m_min);
var m_max = { ...m };
m_max.name = n + '_max';
m_max.title += '——最大值';
cm.list.push(m_max);
};
/**
* 添加关键词配置
* @param {object} cm 参数配置对象
* @param {string} keyword 关键词
* @param {object} cg 配置对象
* @private
*/
Drive.prototype._addKeywordConfig = function (cm, keyword, cg) {
if (keyword) {
cm.get.query.push('keyword');
cm.set.query.push('keyword');
var new_keyword = keyword;
if (cg.table.indexOf('user_') !== -1) {
new_keyword += '、' + keyword_default_tip;
}
var m_k = {
name: 'keyword',
title: '关键词',
description: '用于搜索' + new_keyword.replace('、', ''),
type: 'string',
string: {}
};
cm.list.push(m_k);
}
};
/**
* 保存参数配置文件
* @param {object} cm 参数配置对象
* @param {string} client 客户端配置保存路径
* @param {string} manage 管理端配置保存路径
* @param {boolean} cover 是否覆盖文件
* @private
*/
Drive.prototype._saveParamFiles = function (cm, client, manage, cover) {
if (client) {
this.saveFile(client + '/param.json', cm, cover);
}
if (manage) {
var manage_cm = { ...cm };
delete manage_cm.method;
manage_cm.name += 2;
this.saveFile(manage + '/param.json', manage_cm, cover);
}
};
/**
* 是否设置
* @param {string} name 名称
* @param {Array} arr 匹配的对象
* @returns {boolean} 是否设置
*/
Drive.prototype.isSet = function (name, arr) {
if (!arr) {
return false;
}
var bl = false;
for (var i = 0; i < arr.length; i++) {
if (name.indexOf(arr[i]) !== -1) {
bl = true;
break;
}
}
return bl;
};
/**
* 是否排除
* @param {string} name 名称
* @param {Array} arr 匹配的对象
* @param {string} type 数据类型
* @returns {boolean} 是否可以
*/
Drive.prototype.isCan = function (name, arr, type) {
if (type === 'longtext') {
return false;
}
if (!arr) {
return true;
}
var bl = true;
for (var i = 0; i < arr.length; i++) {
if (name.indexOf(arr[i]) !== -1) {
bl = false;
break;
}
}
return bl;
};
/**
* 确定API路径
* @param {object} cg 配置对象
* @returns {string} API路径
* @private
*/
Drive.prototype._deteApiPath = function (cg) {
var arr = cg.table.split('_');
var p = '/api/';
if (arr.length > 1) {
p += cg.table.replace('_', '/');
} else {
p += arr[0];
}
return p;
};
/**
* 创建基础API配置
* @param {object} cg 配置对象
* @param {string} path API路径
* @returns {object} API配置
* @private
*/
Drive.prototype._createApiConfig = function (cg, path) {
return {
'name': cg.table,
'title': cg.title,
'description': cg.description,
'path': path,
'method': 'ALL',
'cache': 0,
'client_cache': false,
'param_path': './param.json',
'sql_path': './sql.json',
'check_param': true
};
};
/**
* 检查是否包含用户ID字段
* @param {Array} fields 字段列表
* @returns {boolean} 是否包含用户ID字段
* @private
*/
Drive.prototype._hasUserIdField = function (fields) {
for (var i = 0, item; item = fields[i++];) {
var name = item.name;
if (name == $.dict.user_id || name === 'uid' || name === 'user_id' || name === 'userid') {
return true;
}
}
return false;
};
/**
* 保存客户端API配置
* @param {string} client 客户端配置保存路径
* @param {object} base_config 基础配置
* @param {object} cg 配置对象
* @param {boolean} cover 是否覆盖文件
* @private
*/
Drive.prototype._saveClientApiConfig = function (client, base_config, cg, cover) {
var o = { ...base_config };
if (cg.title.indexOf('user') !== -1) {
if (this._hasUserIdField(cg.fields)) {
o.oauth = {
'scope': true,
'sign_in': true,
'vip': 0,
'user_group': []
};
}
}
o.method = 'GET';
this.saveFile(client + '/api.json', o, cover);
};
/**
* 保存管理端API配置
* @param {string} manage 管理端配置保存路径
* @param {object} base_config 基础配置
* @param {boolean} cover 是否覆盖文件
* @private
*/
Drive.prototype._saveManageApiConfig = function (manage, base_config, cover) {
var o = { ...base_config };
o.oauth = {
'scope': true,
'sign_in': true,
'gm': 2,
'user_admin': []
};
o.path = o.path.replace('/api/', '/apis/');
o.name += '_manage';
this.saveFile(manage + '/api.json', o, cover);
};
/**
* 新建api配置文件
* @param {string} client 客户端配置保存路径
* @param {string} manage 管理端配置保存路径
* @param {boolean} cover 是否覆盖文件
*/
Drive.prototype.newApi = async function (client, manage, cover) {
var cg = this.config;
var path = this._deteApiPath(cg);
var base_config = this._createApiConfig(cg, path);
if (client) {
this._saveClientApiConfig(client, base_config, cg, cover);
}
if (manage) {
this._saveManageApiConfig(manage, base_config, cover);
}
};
/**
* 获取模型
* @param {string} type 模型类型
* @returns {object} 返回获取到的模型
*/
Drive.prototype.getModel = function (type) {
let model = { ...this.config };
let dir = this.getDir();
let l = $.slash;
let app_name = dir.between('app' + l, l);
let plugin_name = dir.between('plugin' + l, l);
let name = dir.basename();
model.app = app_name;
model.plugin = plugin_name;
model.name = model.name || name;
return model;
};
module.exports = Drive;