UNPKG

mm_os

Version:

MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。

2,019 lines (1,841 loc) 52 kB
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;