mm_mysql 2.5.7 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -3,22 +3,22 @@
3
3
  * @description 参考 mm_redis 结构,index.js 为薄包装,类定义在 lib/mysql.js
4
4
  */
5
5
  const Mysql = require('./lib/mysql');
6
- const Admin = require('mm_corejs').Admin;
6
+ const { reg } = require('mm_admin');
7
7
 
8
8
  /**
9
9
  * Mysql 管理器实例(Admin 工厂)
10
10
  * 支持 os.js 调用:require('mm_mysql').admin.setServer(os) -> admin.get('os', config)
11
11
  */
12
- let admin = new Admin();
12
+ const admin = reg('mm_mysql');
13
13
  admin.setClass(Mysql);
14
14
 
15
15
  module.exports = {
16
16
  Mysql,
17
17
  admin,
18
18
  mysqlAdmin(scope, config) {
19
- if (!scope) {
20
- scope = 'os';
21
- }
22
- return admin.get(scope, config);
19
+ return admin.get(scope || 'os', config);
23
20
  }
24
- };
21
+ };
22
+ /* mm-tool-logic-export 工具能力层:其它模块可 require("<pkg>").tools.<name> 直接引用 */
23
+ Object.defineProperty(module.exports, 'tools', { enumerable: true, configurable: true,
24
+ get: function () { return require('./lib/tools'); } });
package/lib/mysql.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const mysql = require('mysql2/promise');
2
2
  const { Base } = require('mm_mod');
3
3
  const { DB } = require('./db');
4
+ const makeQueryError = require('./query_error');
4
5
 
5
6
  /**
6
7
  * 优化版MySQL数据库操作类
@@ -194,12 +195,13 @@ Mysql.prototype.run = async function (sql, params = []) {
194
195
  let [rows] = await conn.query(sql, params);
195
196
  ret = rows;
196
197
  } catch (err) {
197
- console.error('SQL执行失败', err);
198
+ // D-12:查询失败不能吞成空数组——「没数据」和「查询炸了」在业务层必须可区分
198
199
  this.sql = err.sql || sql;
199
200
  this.error = {
200
- code: err.errno,
201
- message: err.sqlMessage
201
+ code: err.errno || err.code,
202
+ message: err.sqlMessage || err.message
202
203
  };
204
+ throw makeQueryError(err, sql, params);
203
205
  }
204
206
  finally {
205
207
  await releaseConn(conn);
@@ -207,6 +209,7 @@ Mysql.prototype.run = async function (sql, params = []) {
207
209
  return ret;
208
210
  };
209
211
 
212
+
210
213
  /**
211
214
  * 执行SQL语句(优化版,支持预处理语句缓存)
212
215
  * @param {string} sql - SQL语句
@@ -232,12 +235,13 @@ Mysql.prototype.exec = async function (sql, params = []) {
232
235
  let [result] = await conn.execute(sql, params);
233
236
  ret = result.insertId || result.affectedRows || result.changedRows || 1;
234
237
  } catch (error) {
235
- console.error('SQL执行失败', error);
238
+ // 写操作失败同样上浮:返回 0 会被当成「没有行受影响」正常结果
236
239
  this.sql = error.sql || sql;
237
240
  this.error = {
238
- code: error.errno,
239
- message: error.sqlMessage
241
+ code: error.errno || error.code,
242
+ message: error.sqlMessage || error.message
240
243
  };
244
+ throw makeQueryError(error, sql, params);
241
245
  }
242
246
  finally {
243
247
  await releaseConn(conn);
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 构造带上下文的数据层错误
5
+ *
6
+ * 独立函数而非实例方法:DB 包装层以 `super(sqlite.run, sqlite.exec)` 传递未绑定的方法引用,
7
+ * 调用时 this 指向 DB 实例而不是 SQLite 实例,任何 this.xxx 辅助方法都会失效。
8
+ * 用模块级函数可同时覆盖「SQLite 直接被调用」与「经 DB 包装调用」两条路径。
9
+ *
10
+ * @param {Error} cause 驱动抛出的原始异常
11
+ * @param {string} sql 出错的 SQL
12
+ * @param {Array} params 绑定参数
13
+ * @returns {Error} 携带 code / sql / params / cause 的错误对象
14
+ */
15
+ module.exports = function makeQueryError(cause, sql, params) {
16
+ const error = new Error(`SQL执行失败: ${cause.message || cause}`);
17
+ error.code = cause.code || cause.errno;
18
+ error.sql = sql;
19
+ error.params = params;
20
+ error.cause = cause;
21
+ return error;
22
+ };
package/lib/sql.js CHANGED
@@ -237,6 +237,10 @@ Sql.prototype.delSql = function (where) {
237
237
  * @returns {Promise | object} 执行结果
238
238
  */
239
239
  Sql.prototype.setSql = function (where, set) {
240
+ // 空 SET 会拼出非法 SQL,按“无变更”处理
241
+ if (!set) {
242
+ return 0;
243
+ }
240
244
  // 使用直接拼接的方式,避免模板替换错误
241
245
  var sql = `UPDATE \`${this.table}\` SET ${set} WHERE ${where};`;
242
246
  return this.exec(sql);
@@ -646,6 +650,12 @@ Sql.prototype.set = async function (query, body, like) {
646
650
  if (!this.table || !body || typeof body !== 'object') {
647
651
  throw new Error('表名或数据未设置');
648
652
  }
653
+ // 待更新字段全为 undefined/null 时 toSet 会得到空串,
654
+ // 此时执行会产生 `UPDATE t SET WHERE ...` 的非法 SQL,直接按“无变更”返回 0
655
+ if (!this.toSet(body)) {
656
+ this.sql = '';
657
+ return 0;
658
+ }
649
659
  let sql = this.toSetSql(query, body, like);
650
660
  this.sql = sql;
651
661
  let bl = await this.exec(sql);
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ /** 模块能力层:懒加载工具实现,避免模块入口被 require 时加载全部实现 */
5
+ module.exports = new Proxy({}, {
6
+ get(_t, key) {
7
+ if (typeof key !== 'string') return undefined;
8
+ if (!fs.existsSync(path.join(__dirname, key, 'index.js'))) return undefined;
9
+ return require('./' + key);
10
+ },
11
+ has(_t, key) {
12
+ return typeof key === 'string' && fs.existsSync(path.join(__dirname, key, 'index.js'));
13
+ },
14
+ ownKeys() { return fs.readdirSync(__dirname, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); },
15
+ getOwnPropertyDescriptor(_t, key) { return this.has(_t, key) ? { enumerable: true, configurable: true } : undefined; }
16
+ });
@@ -0,0 +1,208 @@
1
+ /**
2
+ * MySQL管理器
3
+ * 管理 MySQL 连接和操作(基于 mm_mysql 连接池)
4
+ * SQL 方法: query/execute/upsert, ORM 方法: add/del/set/get/getObj/count
5
+ */
6
+ async function main(params, ctx, options) {
7
+ const { mysqlAdmin } = require('mm_mysql');
8
+ var action = params.action || '';
9
+ var table = params.table || '';
10
+ var sql = params.sql || '';
11
+ var sql_params = params.params || [];
12
+ var query = params.query || {};
13
+ var body = params.body || {};
14
+ var unique = params.unique || [];
15
+
16
+ var opts = this.getOptions();
17
+
18
+ var mysql = mysqlAdmin('os', opts);
19
+ await mysql.open();
20
+
21
+ ctx.output = await dispatch(mysql, action, table, sql, sql_params, query, body, unique);
22
+ return ctx.output;
23
+ }
24
+
25
+ /**
26
+ * 分发 action 到对应的处理方法
27
+ */
28
+ async function dispatch(mysql, action, table, sql, sql_params, query, body, unique) {
29
+ const SQL_METHODS = ['query', 'run', 'execute', 'exec', 'insert', 'update', 'delete'];
30
+ const ORM_METHODS = ['add', 'del', 'set', 'get', 'getObj', 'list', 'count'];
31
+
32
+ if (SQL_METHODS.indexOf(action) !== -1) {
33
+ return handleSql(mysql, action, sql, sql_params);
34
+ }
35
+ if (ORM_METHODS.indexOf(action) !== -1) {
36
+ return handleOrm(mysql, action, table, query, body);
37
+ }
38
+ if (action === 'upsert') {
39
+ return handleUpsert(mysql, table, body, unique);
40
+ }
41
+ if (action === 'batch') {
42
+ return handleBatch(mysql, sql_params);
43
+ }
44
+ if (action === 'runSql') {
45
+ return handleRunSql(mysql, sql);
46
+ }
47
+ return { error: { code: 'MGR_UNKNOWN_ACTION', message: '不支持的操作: ' + action } };
48
+ }
49
+
50
+ /**
51
+ * 处理 SQL 方法: query/run → SELECT, execute/exec → INSERT/UPDATE/DELETE
52
+ */
53
+ async function handleSql(mysql, action, sql, sql_params) {
54
+ var is_query = action === 'query' || action === 'run';
55
+ var result = is_query ? await mysql.run(sql, sql_params) : await mysql.exec(sql, sql_params);
56
+
57
+ if (mysql.error) {
58
+ return _errorResult('MGR_SQL_ERROR', mysql.error.message);
59
+ }
60
+ return is_query
61
+ ? {
62
+ list: Array.isArray(result) ? result : [],
63
+ count: Array.isArray(result) ? result.length : 0
64
+ }
65
+ : { affected: result || 0 };
66
+ }
67
+
68
+ /**
69
+ * 处理 ORM 方法: add/del/set/get/getObj/count
70
+ */
71
+ async function handleOrm(mysql, action, table, query, body) {
72
+ var db = mysql.db();
73
+ db.table = table;
74
+
75
+ var result;
76
+ switch (action) {
77
+ case 'add':
78
+ result = await db.add(body);
79
+ break;
80
+ case 'del':
81
+ result = await db.del(query);
82
+ break;
83
+ case 'set':
84
+ result = await db.set(query, body);
85
+ break;
86
+ case 'list':
87
+ result = await db.list(query);
88
+ break;
89
+ case 'getObj':
90
+ result = await db.getObj(query);
91
+ break;
92
+ case 'count':
93
+ result = await db.count(query);
94
+ break;
95
+ }
96
+
97
+ if (db.error) {
98
+ return _errorResult('MGR_ORM_ERROR', db.error.message);
99
+ }
100
+ return result;
101
+ }
102
+
103
+ /**
104
+ * 处理 upsert: INSERT ... ON DUPLICATE KEY UPDATE
105
+ */
106
+ async function handleUpsert(mysql, table, body, unique) {
107
+ var upsert_sql = _buildUpsert(table, body, unique);
108
+ var result = await mysql.exec(upsert_sql.sql, upsert_sql.params);
109
+
110
+ if (mysql.error) {
111
+ return _errorResult('MGR_UPSERT_ERROR', mysql.error.message);
112
+ }
113
+ return { affected: 1, id: result || 0 };
114
+ }
115
+
116
+ /**
117
+ * 批量执行 SQL 列表(用于建表等初始化操作)
118
+ * @param {object} mysql 数据库实例
119
+ * @param {Array} sqls SQL 语句数组
120
+ * @returns {object} { affected, count }
121
+ */
122
+ async function handleBatch(mysql, sqls) {
123
+ if (!Array.isArray(sqls) || sqls.length === 0) {
124
+ return { error: { code: 'MGR_BATCH_EMPTY', message: 'SQL 列表不能为空' } };
125
+ }
126
+ var affected = 0;
127
+ for (var i = 0; i < sqls.length; i++) {
128
+ var r = await mysql.exec(sqls[i], []);
129
+ if (mysql.error) {
130
+ return _errorResult('MGR_BATCH_ERROR', mysql.error.message + ' [第' + (i + 1) + '条SQL]');
131
+ }
132
+ affected += r || 0;
133
+ }
134
+ return { affected: affected, count: sqls.length };
135
+ }
136
+
137
+ /**
138
+ * 执行多语句 SQL 脚本(支持分号分隔的建表/插入语句)
139
+ * @param {object} mysql 数据库实例
140
+ * @param {string} sql SQL 脚本内容
141
+ * @returns {object} { success, statements, affected_rows }
142
+ */
143
+ async function handleRunSql(mysql, sql) {
144
+ if (!sql) {
145
+ return _errorResult('MGR_SQL_EMPTY', 'SQL 语句不能为空');
146
+ }
147
+ var result = await mysql.runSql(sql);
148
+ if (mysql.error) {
149
+ return _errorResult('MGR_RUNSQL_ERROR', mysql.error.message);
150
+ }
151
+ return {
152
+ success: result.success,
153
+ statements: result.statements,
154
+ affected_rows: result.affected_rows
155
+ };
156
+ }
157
+
158
+ /**
159
+ * 构建错误结果对象
160
+ */
161
+ function _errorResult(code, message) {
162
+ return { error: { code: code, message: message } };
163
+ }
164
+
165
+ /**
166
+ * 构建 upsert SQL (INSERT ... ON DUPLICATE KEY UPDATE)
167
+ * @param {string} table 表名
168
+ * @param {object} body 数据对象 { field: value }
169
+ * @param {array} unique 唯一约束字段列表(可选)
170
+ * @returns {object} { sql, params }
171
+ */
172
+ function _buildUpsert(table, body, unique) {
173
+ var fields = Object.keys(body);
174
+ var values = [];
175
+ var placeholders = [];
176
+ for (var i = 0; i < fields.length; i++) {
177
+ values.push(body[fields[i]]);
178
+ placeholders.push('?');
179
+ }
180
+
181
+ var updates = [];
182
+ var update_fields =
183
+ unique.length > 0
184
+ ? fields.filter(function (f) {
185
+ return unique.indexOf(f) === -1;
186
+ })
187
+ : fields;
188
+ for (var j = 0; j < update_fields.length; j++) {
189
+ var f = update_fields[j];
190
+ updates.push('`' + f + '` = VALUES(`' + f + '`)');
191
+ }
192
+
193
+ var sql_str =
194
+ 'INSERT INTO `' +
195
+ table +
196
+ '` (`' +
197
+ fields.join('`, `') +
198
+ '`) VALUES (' +
199
+ placeholders.join(', ') +
200
+ ')';
201
+ if (updates.length > 0) {
202
+ sql_str += ' ON DUPLICATE KEY UPDATE ' + updates.join(', ');
203
+ }
204
+
205
+ return { sql: sql_str, params: values };
206
+ }
207
+
208
+ module.exports = { main };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * MySQL 读写分离管理器
3
+ * 基于 mm_mysql 的 mysqlAdmin 管理连接池,实现读写分离路由
4
+ * SQL 方法: query(SELECT→读库) / insert/update/delete(→写库)
5
+ * ORM 方法: add/set/del(→写库) / getObj/list/count(→读库)
6
+ * 读库从 replicas 随机选择一台,未配置从库时读写都走主库
7
+ */
8
+ /**
9
+ * 判断 SQL 是否为读操作
10
+ * @param {string} sql SQL 语句
11
+ * @returns {boolean} 是否为读操作
12
+ */
13
+ function _isReadOp(sql) {
14
+ var trimmed = sql.trim().toUpperCase();
15
+ return (
16
+ trimmed.startsWith('SELECT') ||
17
+ trimmed.startsWith('SHOW') ||
18
+ trimmed.startsWith('DESCRIBE') ||
19
+ trimmed.startsWith('EXPLAIN')
20
+ );
21
+ }
22
+
23
+ /**
24
+ * 选择读库(随机)
25
+ * @param {Array} replicas 读库配置列表
26
+ * @returns {object} 读库配置
27
+ */
28
+ function _pickReplica(replicas) {
29
+ if (!replicas || replicas.length === 0) {
30
+ return null;
31
+ }
32
+ var idx = Math.floor(Math.random() * replicas.length);
33
+ return replicas[idx];
34
+ }
35
+
36
+ /**
37
+ * 判断操作路由: 写→master, 读→replica
38
+ * @param {string} action 操作类型
39
+ * @param {string} sql SQL 语句
40
+ * @returns {boolean} 是否为读操作
41
+ */
42
+ function _isReadAction(action, sql) {
43
+ var orm_write = ['add', 'set', 'del', 'insert', 'update', 'delete'];
44
+ var orm_read = ['getObj', 'list', 'count'];
45
+ if (action === 'runSql') return false;
46
+ if (orm_write.indexOf(action) !== -1) return false;
47
+ if (orm_read.indexOf(action) !== -1) return true;
48
+ if (action === 'query') return true;
49
+ if (action === 'execute' || action === 'exec') return false;
50
+ if (sql) return _isReadOp(sql);
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * 从模块 options 定义中递归提取默认配置
56
+ * 支持扁平键(default)与嵌套对象(properties),如 master.properties.host.default
57
+ * @param {object} options 模块 options 定义
58
+ * @returns {object} 提取的默认配置
59
+ */
60
+ function _extractDefaults(options) {
61
+ var out = {};
62
+ for (var k in options) {
63
+ var o = options[k];
64
+ if (o && typeof o === 'object' && o.default !== undefined) {
65
+ out[k] = o.default;
66
+ } else if (o && typeof o === 'object' && o.properties) {
67
+ out[k] = _extractDefaults(o.properties);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * 主函数
75
+ * @param {object} ctx 上下文
76
+ * @param {object} params 参数
77
+ * @returns {object} 执行结果
78
+ */
79
+ async function main(params, ctx, options) {
80
+ const { mysqlAdmin } = require('mm_mysql');
81
+ // 从模块 options 读取主从配置,无 master 键时以扁平 options 作为主库
82
+ var rw_opts = _extractDefaults((this.config && this.config.options) || {});
83
+ var master_cfg;
84
+ var replicas = [];
85
+ if (rw_opts.master) {
86
+ master_cfg = rw_opts.master;
87
+ replicas = rw_opts.replicas || [];
88
+ } else {
89
+ master_cfg = rw_opts;
90
+ replicas = [];
91
+ }
92
+
93
+ var action = params.action || '';
94
+ var sql = params.sql || '';
95
+ var values = params.values || [];
96
+ var table = params.table || '';
97
+ var query = params.query || {};
98
+ var body = params.body || {};
99
+
100
+ // 读写分离路由
101
+ var is_read = _isReadAction(action, sql);
102
+ var target_cfg = master_cfg;
103
+ var route = 'master';
104
+
105
+ if (is_read && replicas.length > 0) {
106
+ var replica = _pickReplica(replicas);
107
+ if (replica) {
108
+ target_cfg = replica;
109
+ route = 'replica';
110
+ }
111
+ }
112
+
113
+ // SQL 方法必须提供 sql 语句
114
+ var SQL_ACTIONS = ['query', 'run', 'insert', 'update', 'delete', 'execute', 'exec', 'runSql'];
115
+ if (SQL_ACTIONS.indexOf(action) !== -1 && !sql) {
116
+ ctx.output = { error: { code: 'SQL_EMPTY', message: 'SQL 语句不能为空' } };
117
+ return ctx.output;
118
+ }
119
+
120
+ try {
121
+ // 使用 mm_mysql 的 mysqlAdmin 管理连接池(按 scope 缓存实例)
122
+ var scope = route === 'replica' ? 'rw_replica' : 'rw_master';
123
+ var mysql = mysqlAdmin(scope, target_cfg);
124
+ await mysql.open();
125
+
126
+ // ORM 方法: add/set/del/getObj/list/count
127
+ var ORM_METHODS = ['add', 'del', 'set', 'list', 'getObj', 'count'];
128
+ if (ORM_METHODS.indexOf(action) !== -1) {
129
+ if (!table) {
130
+ ctx.output = { error: { code: 'SQL_TABLE_EMPTY', message: 'ORM 操作缺少 table 参数', route: route } };
131
+ return ctx.output;
132
+ }
133
+ var db = mysql.db();
134
+ db.table = table;
135
+ var result;
136
+ switch (action) {
137
+ case 'add':
138
+ result = await db.add(body);
139
+ break;
140
+ case 'del':
141
+ result = await db.del(query);
142
+ break;
143
+ case 'set':
144
+ result = await db.set(query, body);
145
+ break;
146
+ case 'list':
147
+ result = await db.list(query);
148
+ break;
149
+ case 'getObj':
150
+ result = await db.getObj(query);
151
+ break;
152
+ case 'count':
153
+ result = await db.count(query);
154
+ break;
155
+ }
156
+ if (db.error) {
157
+ ctx.output = { error: { code: 'ORM_ERROR', message: db.error.message, route: route } };
158
+ return ctx.output;
159
+ }
160
+ ctx.output = { result: result, route: route };
161
+ return ctx.output;
162
+ }
163
+
164
+ // SQL 方法: query / insert / update / delete / execute
165
+ switch (action) {
166
+ case 'runSql': {
167
+ var rs = await mysql.runSql(sql);
168
+ if (mysql.error) {
169
+ ctx.output = { error: { code: 'SQL_ERROR', message: mysql.error.message, route: route } };
170
+ return ctx.output;
171
+ }
172
+ ctx.output = {
173
+ success: rs.success,
174
+ statements: rs.statements,
175
+ affected_rows: rs.affected_rows,
176
+ route: route
177
+ };
178
+ break;
179
+ }
180
+ case 'query':
181
+ case 'run': {
182
+ var rows = await mysql.run(sql, values);
183
+ if (mysql.error) {
184
+ ctx.output = { error: { code: 'SQL_ERROR', message: mysql.error.message, route: route } };
185
+ return ctx.output;
186
+ }
187
+ ctx.output = {
188
+ list: Array.isArray(rows) ? rows : [],
189
+ count: Array.isArray(rows) ? rows.length : 0,
190
+ route: route
191
+ };
192
+ break;
193
+ }
194
+ case 'insert':
195
+ case 'update':
196
+ case 'delete':
197
+ case 'execute':
198
+ case 'exec': {
199
+ var affected = await mysql.exec(sql, values);
200
+ if (mysql.error) {
201
+ ctx.output = { error: { code: 'SQL_ERROR', message: mysql.error.message, route: route } };
202
+ return ctx.output;
203
+ }
204
+ ctx.output = {
205
+ affected: affected || 0,
206
+ route: route
207
+ };
208
+ break;
209
+ }
210
+ default:
211
+ ctx.output = {
212
+ error: {
213
+ code: 'SQL_UNKNOWN_ACTION',
214
+ message: '不支持的操作: ' + action + ',支持: query/insert/update/delete/execute/runSql + ORM(add/set/del/getObj/list/count)',
215
+ route: route
216
+ }
217
+ };
218
+ }
219
+ } catch (err) {
220
+ ctx.output = {
221
+ error: { code: 'SQL_ERROR', message: err.message, route: route }
222
+ };
223
+ }
224
+
225
+ return ctx.output;
226
+ }
227
+
228
+ module.exports = { main };
@@ -0,0 +1,181 @@
1
+ /**
2
+ * SQL导出管理器
3
+ * 将数据库表导出为 SQL 文件(表结构 + 数据),基于 mm_mysql
4
+ * 导出内容: SHOW CREATE TABLE 建表语句 + INSERT 数据语句
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * 主函数
11
+ * @param {object} ctx 上下文对象
12
+ * @param {object} params 参数 { table, output_file }
13
+ * @returns {object} 导出结果 { file_path, table, statements, rows }
14
+ */
15
+ async function main(params, ctx, options) {
16
+ const { mysqlAdmin } = require('mm_mysql');
17
+ var table = params.table || '';
18
+ var output_file = params.output_file || '';
19
+ // 未提供的可选参数在模板中会保留为占位符,此时使用默认输出路径
20
+ if (output_file.indexOf('{{') !== -1) {
21
+ output_file = '';
22
+ }
23
+
24
+ if (!table) {
25
+ ctx.output = _errorResult('MGR_TABLE_EMPTY', '表名不能为空');
26
+ return ctx.output;
27
+ }
28
+
29
+ var opts = this.getOptions();
30
+
31
+ try {
32
+ var mysql = mysqlAdmin('os', opts);
33
+ await mysql.open();
34
+
35
+ var create_sql = await _getCreateSql(mysql, table);
36
+ if (mysql.error) {
37
+ ctx.output = _errorResult('MGR_DUMP_ERROR', '查询表结构失败: ' + mysql.error.message);
38
+ return ctx.output;
39
+ }
40
+
41
+ var rows = await _getRows(mysql, table);
42
+ if (mysql.error) {
43
+ ctx.output = _errorResult('MGR_DUMP_ERROR', '查询表数据失败: ' + mysql.error.message);
44
+ return ctx.output;
45
+ }
46
+
47
+ var insert_sqls = _buildInserts(table, rows);
48
+ var sql_text = _buildSqlText(table, create_sql, insert_sqls);
49
+ var file_path = _writeFile(table, sql_text, output_file);
50
+
51
+ ctx.output = {
52
+ file_path: file_path,
53
+ table: table,
54
+ statements: 1 + insert_sqls.length,
55
+ rows: rows.length
56
+ };
57
+ return ctx.output;
58
+ } catch (err) {
59
+ ctx.output = _errorResult('MGR_DUMP_ERROR', err.message);
60
+ return ctx.output;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 查询表结构
66
+ * @param {object} mysql 数据库实例
67
+ * @param {string} table 表名
68
+ * @returns {string} 建表语句
69
+ */
70
+ async function _getCreateSql(mysql, table) {
71
+ var list = await mysql.run('SHOW CREATE TABLE `' + table + '`', []);
72
+ if (!Array.isArray(list) || list.length === 0) {
73
+ return '';
74
+ }
75
+ var first = list[0];
76
+ var create_sql = first['Create Table'] || Object.values(first).pop() || '';
77
+ create_sql = String(create_sql).trim();
78
+ if (create_sql && !create_sql.endsWith(';')) {
79
+ create_sql += ';';
80
+ }
81
+ return create_sql;
82
+ }
83
+
84
+ /**
85
+ * 查询表数据
86
+ * @param {object} mysql 数据库实例
87
+ * @param {string} table 表名
88
+ * @returns {Array} 数据行数组
89
+ */
90
+ async function _getRows(mysql, table) {
91
+ var list = await mysql.run('SELECT * FROM `' + table + '`', []);
92
+ return Array.isArray(list) ? list : [];
93
+ }
94
+
95
+ /**
96
+ * 生成 INSERT 语句数组
97
+ * @param {string} table 表名
98
+ * @param {Array} rows 数据行
99
+ * @returns {Array} INSERT 语句数组
100
+ */
101
+ function _buildInserts(table, rows) {
102
+ var sqls = [];
103
+ for (var i = 0; i < rows.length; i++) {
104
+ var row = rows[i];
105
+ var cols = Object.keys(row);
106
+ var vals = [];
107
+ for (var j = 0; j < cols.length; j++) {
108
+ vals.push(_sqlValue(row[cols[j]]));
109
+ }
110
+ sqls.push(
111
+ 'INSERT INTO `' + table + '` (`' + cols.join('`, `') + '`) VALUES (' + vals.join(', ') + ');'
112
+ );
113
+ }
114
+ return sqls;
115
+ }
116
+
117
+ /**
118
+ * 转换值为 SQL 字面量
119
+ * @param {*} value 字段值
120
+ * @returns {string} SQL 字面量
121
+ */
122
+ function _sqlValue(value) {
123
+ if (value === null || value === undefined) return 'NULL';
124
+ if (typeof value === 'number') return String(value);
125
+ if (typeof value === 'boolean') return value ? '1' : '0';
126
+ if (value instanceof Date) {
127
+ var dt = value.toISOString().slice(0, 19).replace('T', ' ');
128
+ return "'" + dt + "'";
129
+ }
130
+ return "'" + String(value).replace(/'/g, "''") + "'";
131
+ }
132
+
133
+ /**
134
+ * 拼接完整导出 SQL 文本
135
+ * @param {string} table 表名
136
+ * @param {string} create_sql 建表语句
137
+ * @param {Array} insert_sqls INSERT 语句数组
138
+ * @returns {string} 完整 SQL 文本
139
+ */
140
+ function _buildSqlText(table, create_sql, insert_sqls) {
141
+ var lines = [];
142
+ lines.push('-- 表结构导出: `' + table + '`');
143
+ lines.push(create_sql);
144
+ if (insert_sqls.length > 0) {
145
+ lines.push('');
146
+ lines.push('-- 表数据导出: `' + table + '` (' + insert_sqls.length + ' 行)');
147
+ for (var i = 0; i < insert_sqls.length; i++) {
148
+ lines.push(insert_sqls[i]);
149
+ }
150
+ }
151
+ return lines.join('\n') + '\n';
152
+ }
153
+
154
+ /**
155
+ * 写入导出文件
156
+ * @param {string} table 表名
157
+ * @param {string} sql_text SQL 内容
158
+ * @param {string} output_file 输出路径(可选)
159
+ * @returns {string} 文件绝对路径
160
+ */
161
+ function _writeFile(table, sql_text, output_file) {
162
+ var base = process.cwd();
163
+ var file_path = output_file
164
+ ? path.resolve(base, output_file)
165
+ : path.resolve(base, 'backup', table + '.sql');
166
+ fs.mkdirSync(path.dirname(file_path), { recursive: true });
167
+ fs.writeFileSync(file_path, sql_text, 'utf8');
168
+ return file_path;
169
+ }
170
+
171
+ /**
172
+ * 构建错误结果对象
173
+ * @param {string} code 错误码
174
+ * @param {string} message 错误信息
175
+ * @returns {object} 错误对象
176
+ */
177
+ function _errorResult(code, message) {
178
+ return { error: { code: code, message: message } };
179
+ }
180
+
181
+ module.exports = { main };
package/package.json CHANGED
@@ -1,50 +1,51 @@
1
- {
2
- "name": "mm_mysql",
3
- "version": "2.5.7",
4
- "description": "这是超级美眉mysql帮助函数模块,用于便捷操作mysql,使用await方式,可以避免嵌套函数",
5
- "main": "index.js",
6
- "dependencies": {
7
- "mm_corejs": "^1.1.4",
8
- "mm_mod": "^1.0.1",
9
- "mm_helper": "^1.0.0",
10
- "mysql2": "^3.24.2",
11
- "sqlstring": "^2.3.3"
12
- },
13
- "scripts": {
14
- "start": "node index.js",
15
- "test": "node test.js",
16
- "lint": "eslint . --config eslint.config.js",
17
- "lint:fix": "eslint . --config eslint.config.js --fix"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "https://gitee.com/qiuwenwu91/mm_mysql.git"
22
- },
23
- "keywords": [
24
- "mm_mod",
25
- "mm_adapter",
26
- "mm_persist",
27
- "mysql",
28
- "async",
29
- "await",
30
- "promise",
31
- "mysql2",
32
- "database",
33
- "query",
34
- "transaction",
35
- "sql",
36
- "db"
37
- ],
38
- "layer": "adapter",
39
- "group": "persist",
40
- "author": "qww",
41
- "license": "ISC",
42
- "engines": {
43
- "node": ">=12.0.0"
44
- },
45
- "devDependencies": {
46
- "eslint": "^10.9.1",
47
- "eslint-plugin-jsdoc": "^64.2.1",
48
- "mm_eslint": "^1.7.7"
49
- }
50
- }
1
+ {
2
+ "name": "mm_mysql",
3
+ "version": "2.6.0",
4
+ "description": "这是超级美眉mysql帮助函数模块,用于便捷操作mysql,使用await方式,可以避免嵌套函数",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "mm_admin": "^1.1.0",
8
+ "mm_eslint": "^1.0.0",
9
+ "mm_helper": "^1.0.0",
10
+ "mm_mod": "^1.0.1",
11
+ "mysql2": "^3.24.2",
12
+ "sqlstring": "^2.3.3"
13
+ },
14
+ "scripts": {
15
+ "start": "node index.js",
16
+ "test": "node test.js",
17
+ "lint": "eslint . --config eslint.config.js",
18
+ "lint:fix": "eslint . --config eslint.config.js --fix"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://gitee.com/qiuwenwu91/mm_mysql.git"
23
+ },
24
+ "keywords": [
25
+ "mm_mod",
26
+ "mm_adapter",
27
+ "mm_persist",
28
+ "mysql",
29
+ "async",
30
+ "await",
31
+ "promise",
32
+ "mysql2",
33
+ "database",
34
+ "query",
35
+ "transaction",
36
+ "sql",
37
+ "db"
38
+ ],
39
+ "layer": "infrastructure",
40
+ "group": "persistence",
41
+ "author": "qww",
42
+ "license": "ISC",
43
+ "engines": {
44
+ "node": ">=12.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "eslint": "^10.9.1",
48
+ "eslint-plugin-jsdoc": "^64.2.1",
49
+ "mm_eslint": "^1.7.7"
50
+ }
51
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * mysql —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/mysql/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_mysql').tools.mysql 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/mysql');
@@ -0,0 +1,118 @@
1
+ {
2
+ "name": "mysql",
3
+ "handler": "manager",
4
+ "title": "MySQL管理器",
5
+ "description": "操作 MySQL 数据库,支持 SQL 与 ORM 两种方式。适用场景:需要持久化数据、增删改查、统计计数时。工作流提示:action 必填(query/execute/runSql 配 sql,add/del/set/list/getObj/count/upsert 配 table/query/body),读结果在 list/count、写结果在 affected/id。",
6
+ "type": "data",
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "action": {
11
+ "type": "string",
12
+ "description": "操作类型: SQL方法(query/execute/runSql) + ORM方法(add/del/set/list/getObj/count) + upsert + batch",
13
+ "enum": [
14
+ "query",
15
+ "execute",
16
+ "runSql",
17
+ "add",
18
+ "del",
19
+ "set",
20
+ "list",
21
+ "getObj",
22
+ "count",
23
+ "upsert",
24
+ "batch"
25
+ ]
26
+ },
27
+ "sql": {
28
+ "type": "string",
29
+ "description": "SQL语句(SQL方法需要)"
30
+ },
31
+ "params": {
32
+ "type": "array",
33
+ "description": "SQL参数(可选)"
34
+ },
35
+ "table": {
36
+ "type": "string",
37
+ "description": "表名(ORM方法需要)"
38
+ },
39
+ "query": {
40
+ "type": "object",
41
+ "description": "查询条件(ORM方法需要)"
42
+ },
43
+ "body": {
44
+ "type": "object",
45
+ "description": "数据对象(add/set/upsert 操作需要)"
46
+ },
47
+ "unique": {
48
+ "type": "array",
49
+ "description": "唯一约束字段列表(upsert 操作,不更新的字段)"
50
+ }
51
+ },
52
+ "required": [
53
+ "action"
54
+ ]
55
+ },
56
+ "returns": {
57
+ "description": "管理操作结果,读操作返回 list+count,写操作返回 affected,异常时返回 error",
58
+ "type": "object",
59
+ "properties": {
60
+ "list": {
61
+ "type": "array",
62
+ "description": "查询结果列表(query/run 操作)"
63
+ },
64
+ "count": {
65
+ "type": "number",
66
+ "description": "列表条目数/统计数"
67
+ },
68
+ "affected": {
69
+ "type": "number",
70
+ "description": "受影响的行数(写操作)"
71
+ },
72
+ "id": {
73
+ "type": "number",
74
+ "description": "插入的自增ID(upsert 操作)"
75
+ }
76
+ }
77
+ },
78
+ "options": {
79
+ "host": {
80
+ "type": "string",
81
+ "description": "数据库主机地址",
82
+ "default": "localhost"
83
+ },
84
+ "port": {
85
+ "type": "number",
86
+ "description": "数据库端口",
87
+ "default": 3306
88
+ },
89
+ "user": {
90
+ "type": "string",
91
+ "description": "数据库用户名",
92
+ "default": "root"
93
+ },
94
+ "password": {
95
+ "type": "string",
96
+ "description": "数据库密码",
97
+ "default": "Asd159357"
98
+ },
99
+ "database": {
100
+ "type": "string",
101
+ "description": "数据库名",
102
+ "default": "mm"
103
+ },
104
+ "charset": {
105
+ "type": "string",
106
+ "description": "字符集",
107
+ "default": "utf8mb4"
108
+ },
109
+ "pool_size": {
110
+ "type": "number",
111
+ "description": "连接池大小",
112
+ "default": 10,
113
+ "min": 1,
114
+ "max": 100
115
+ }
116
+ },
117
+ "group": "storage"
118
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * mysql_rw —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/mysql_rw/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_mysql').tools.mysql_rw 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/mysql_rw');
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "mysql_rw",
3
+ "handler": "manager",
4
+ "title": "MySQL读写分离",
5
+ "description": "MySQL 读写分离,SELECT 走读库、写操作走主库,多读库负载均衡。适用场景:高并发读、读写分离部署时。工作流提示:action 必填(SQL 配 sql/values,ORM 配 table/query/body),读在 list/count、写后 affected、路由在 route。",
6
+ "type": "data",
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "action": {
11
+ "type": "string",
12
+ "description": "操作类型: SQL(query/insert/update/delete/execute/runSql) + ORM(add/del/set/getObj/list/count),写操作走主库,读操作走读库",
13
+ "enum": [
14
+ "query",
15
+ "insert",
16
+ "update",
17
+ "delete",
18
+ "execute",
19
+ "runSql",
20
+ "add",
21
+ "del",
22
+ "set",
23
+ "getObj",
24
+ "list",
25
+ "count"
26
+ ]
27
+ },
28
+ "sql": {
29
+ "type": "string",
30
+ "description": "SQL 语句(runSql 可传多条以分号分隔的建表/插入语句,强制走主库)"
31
+ },
32
+ "values": {
33
+ "type": "array",
34
+ "description": "SQL 参数值(使用参数化查询防注入)"
35
+ },
36
+ "table": {
37
+ "type": "string",
38
+ "description": "表名(ORM 方法需要)"
39
+ },
40
+ "query": {
41
+ "type": "object",
42
+ "description": "查询条件(ORM 方法需要),如 {username: 'demo'} 或 {id: 1}"
43
+ },
44
+ "body": {
45
+ "type": "object",
46
+ "description": "数据对象(add/set 操作需要)"
47
+ }
48
+ },
49
+ "required": [
50
+ "action"
51
+ ]
52
+ },
53
+ "returns": {
54
+ "description": "操作结果,返回 route 字段标识实际路由到的库",
55
+ "type": "object",
56
+ "properties": {
57
+ "list": {
58
+ "type": "array",
59
+ "description": "查询结果列表(query操作)"
60
+ },
61
+ "count": {
62
+ "type": "number",
63
+ "description": "查询结果数量(query操作)"
64
+ },
65
+ "affected": {
66
+ "type": "number",
67
+ "description": "影响行数或插入ID(insert/update/delete操作)"
68
+ },
69
+ "route": {
70
+ "type": "string",
71
+ "description": "实际路由: master/replica"
72
+ }
73
+ }
74
+ },
75
+ "options": {
76
+ "master": {
77
+ "type": "object",
78
+ "description": "写库(主库)配置",
79
+ "properties": {
80
+ "host": { "type": "string", "description": "主机", "default": "localhost" },
81
+ "port": { "type": "number", "description": "端口", "default": 3306 },
82
+ "user": { "type": "string", "description": "用户名", "default": "root" },
83
+ "password": { "type": "string", "description": "密码", "default": "Asd159357" },
84
+ "database": { "type": "string", "description": "数据库名", "default": "mm" },
85
+ "connectionLimit": { "type": "number", "description": "连接池大小", "default": 10 }
86
+ }
87
+ },
88
+ "replicas": {
89
+ "type": "array",
90
+ "description": "读库(从库)配置列表,多台随机负载均衡",
91
+ "items": {
92
+ "type": "object",
93
+ "properties": {
94
+ "host": { "type": "string", "description": "主机" },
95
+ "port": { "type": "number", "description": "端口", "default": 3306 },
96
+ "user": { "type": "string", "description": "用户名" },
97
+ "password": { "type": "string", "description": "密码" },
98
+ "database": { "type": "string", "description": "数据库名" },
99
+ "connectionLimit": { "type": "number", "description": "连接池大小", "default": 10 }
100
+ }
101
+ }
102
+ }
103
+ },
104
+ "group": "system"
105
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * sql_dump —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/sql_dump/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_mysql').tools.sql_dump 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/sql_dump');
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "sql_dump",
3
+ "handler": "manager",
4
+ "title": "SQL导出管理器",
5
+ "description": "将 MySQL 表导出为 SQL 文件(建表语句+数据)。适用场景:备份表结构数据、迁移或重建环境时。工作流提示:table 必填导出表名,output_file 指定路径(默认 ./backup/{table}.sql),路径在 file_path、条数与行数在 statements/rows。",
6
+ "type": "data",
7
+ "params": {
8
+ "type": "object",
9
+ "properties": {
10
+ "table": {
11
+ "type": "string",
12
+ "description": "要导出的表名"
13
+ },
14
+ "output_file": {
15
+ "type": "string",
16
+ "description": "输出文件路径(可选,默认 ./backup/{table}.sql)"
17
+ }
18
+ },
19
+ "required": ["table"]
20
+ },
21
+ "returns": {
22
+ "description": "导出结果",
23
+ "type": "object",
24
+ "properties": {
25
+ "file_path": {
26
+ "type": "string",
27
+ "description": "导出文件绝对路径"
28
+ },
29
+ "table": {
30
+ "type": "string",
31
+ "description": "导出的表名"
32
+ },
33
+ "statements": {
34
+ "type": "number",
35
+ "description": "生成的 SQL 语句条数"
36
+ },
37
+ "rows": {
38
+ "type": "number",
39
+ "description": "导出的数据行数"
40
+ }
41
+ }
42
+ },
43
+ "options": {
44
+ "host": {
45
+ "type": "string",
46
+ "description": "数据库主机地址",
47
+ "default": "localhost"
48
+ },
49
+ "port": {
50
+ "type": "number",
51
+ "description": "数据库端口",
52
+ "default": 3306
53
+ },
54
+ "user": {
55
+ "type": "string",
56
+ "description": "数据库用户名",
57
+ "default": "root"
58
+ },
59
+ "password": {
60
+ "type": "string",
61
+ "description": "数据库密码",
62
+ "default": "Asd159357"
63
+ },
64
+ "database": {
65
+ "type": "string",
66
+ "description": "数据库名",
67
+ "default": "mm"
68
+ },
69
+ "charset": {
70
+ "type": "string",
71
+ "description": "字符集",
72
+ "default": "utf8mb4"
73
+ },
74
+ "pool_size": {
75
+ "type": "number",
76
+ "description": "连接池大小",
77
+ "default": 10,
78
+ "min": 1,
79
+ "max": 100
80
+ }
81
+ },
82
+ "group": "storage"
83
+ }