befly 3.17.0 → 3.17.2

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.
Files changed (150) hide show
  1. package/README.md +6 -0
  2. package/apis/admin/cacheRefresh.js +122 -0
  3. package/apis/admin/del.js +34 -0
  4. package/apis/admin/detail.js +23 -0
  5. package/apis/admin/ins.js +69 -0
  6. package/apis/admin/list.js +28 -0
  7. package/apis/admin/upd.js +95 -0
  8. package/apis/api/all.js +24 -0
  9. package/apis/api/list.js +31 -0
  10. package/apis/auth/login.js +123 -0
  11. package/apis/auth/sendSmsCode.js +24 -0
  12. package/apis/dashboard/configStatus.js +39 -0
  13. package/apis/dashboard/environmentInfo.js +43 -0
  14. package/apis/dashboard/performanceMetrics.js +20 -0
  15. package/apis/dashboard/permissionStats.js +27 -0
  16. package/apis/dashboard/serviceStatus.js +75 -0
  17. package/apis/dashboard/systemInfo.js +19 -0
  18. package/apis/dashboard/systemOverview.js +30 -0
  19. package/apis/dashboard/systemResources.js +106 -0
  20. package/apis/dict/all.js +23 -0
  21. package/apis/dict/del.js +16 -0
  22. package/apis/dict/detail.js +27 -0
  23. package/apis/dict/ins.js +51 -0
  24. package/apis/dict/items.js +30 -0
  25. package/apis/dict/list.js +36 -0
  26. package/apis/dict/upd.js +74 -0
  27. package/apis/dictType/all.js +16 -0
  28. package/apis/dictType/del.js +38 -0
  29. package/apis/dictType/detail.js +20 -0
  30. package/apis/dictType/ins.js +37 -0
  31. package/apis/dictType/list.js +26 -0
  32. package/apis/dictType/upd.js +51 -0
  33. package/apis/email/config.js +25 -0
  34. package/apis/email/logList.js +23 -0
  35. package/apis/email/send.js +66 -0
  36. package/apis/email/verify.js +21 -0
  37. package/apis/loginLog/list.js +23 -0
  38. package/apis/menu/all.js +41 -0
  39. package/apis/menu/list.js +25 -0
  40. package/apis/operateLog/list.js +23 -0
  41. package/apis/role/all.js +21 -0
  42. package/apis/role/apiSave.js +43 -0
  43. package/apis/role/apis.js +22 -0
  44. package/apis/role/del.js +49 -0
  45. package/apis/role/detail.js +32 -0
  46. package/apis/role/ins.js +46 -0
  47. package/apis/role/list.js +27 -0
  48. package/apis/role/menuSave.js +42 -0
  49. package/apis/role/menus.js +22 -0
  50. package/apis/role/save.js +40 -0
  51. package/apis/role/upd.js +50 -0
  52. package/apis/sysConfig/all.js +16 -0
  53. package/apis/sysConfig/del.js +36 -0
  54. package/apis/sysConfig/get.js +49 -0
  55. package/apis/sysConfig/ins.js +50 -0
  56. package/apis/sysConfig/list.js +24 -0
  57. package/apis/sysConfig/upd.js +62 -0
  58. package/checks/api.js +55 -0
  59. package/checks/config.js +107 -0
  60. package/checks/hook.js +38 -0
  61. package/checks/menu.js +58 -0
  62. package/checks/plugin.js +38 -0
  63. package/checks/table.js +78 -0
  64. package/configs/beflyConfig.json +61 -0
  65. package/configs/beflyMenus.json +85 -0
  66. package/configs/constConfig.js +34 -0
  67. package/configs/regexpAlias.json +55 -0
  68. package/hooks/auth.js +34 -0
  69. package/hooks/cors.js +39 -0
  70. package/hooks/parser.js +90 -0
  71. package/hooks/permission.js +71 -0
  72. package/hooks/validator.js +43 -0
  73. package/index.js +326 -0
  74. package/lib/cacheHelper.js +483 -0
  75. package/lib/cacheKeys.js +42 -0
  76. package/lib/connect.js +120 -0
  77. package/lib/dbHelper/builders.js +698 -0
  78. package/lib/dbHelper/context.js +131 -0
  79. package/lib/dbHelper/dataOps.js +505 -0
  80. package/lib/dbHelper/execute.js +65 -0
  81. package/lib/dbHelper/index.js +27 -0
  82. package/lib/dbHelper/transaction.js +43 -0
  83. package/lib/dbHelper/util.js +58 -0
  84. package/lib/dbHelper/validate.js +549 -0
  85. package/lib/emailHelper.js +110 -0
  86. package/lib/logger.js +604 -0
  87. package/lib/redisHelper.js +684 -0
  88. package/lib/sqlBuilder/batch.js +113 -0
  89. package/lib/sqlBuilder/check.js +150 -0
  90. package/lib/sqlBuilder/compiler.js +347 -0
  91. package/lib/sqlBuilder/errors.js +60 -0
  92. package/lib/sqlBuilder/index.js +218 -0
  93. package/lib/sqlBuilder/parser.js +296 -0
  94. package/lib/sqlBuilder/util.js +260 -0
  95. package/lib/validator.js +303 -0
  96. package/package.json +19 -12
  97. package/paths.js +112 -0
  98. package/plugins/cache.js +16 -0
  99. package/plugins/config.js +11 -0
  100. package/plugins/email.js +27 -0
  101. package/plugins/logger.js +20 -0
  102. package/plugins/mysql.js +36 -0
  103. package/plugins/redis.js +34 -0
  104. package/plugins/tool.js +155 -0
  105. package/router/api.js +140 -0
  106. package/router/static.js +71 -0
  107. package/sql/admin.sql +18 -0
  108. package/sql/api.sql +12 -0
  109. package/sql/dict.sql +13 -0
  110. package/sql/dictType.sql +12 -0
  111. package/sql/emailLog.sql +20 -0
  112. package/sql/loginLog.sql +25 -0
  113. package/sql/menu.sql +12 -0
  114. package/sql/operateLog.sql +22 -0
  115. package/sql/role.sql +14 -0
  116. package/sql/sysConfig.sql +16 -0
  117. package/sync/api.js +93 -0
  118. package/sync/cache.js +13 -0
  119. package/sync/dev.js +171 -0
  120. package/sync/menu.js +99 -0
  121. package/tables/admin.json +56 -0
  122. package/tables/api.json +26 -0
  123. package/tables/dict.json +30 -0
  124. package/tables/dictType.json +24 -0
  125. package/tables/emailLog.json +61 -0
  126. package/tables/loginLog.json +86 -0
  127. package/tables/menu.json +24 -0
  128. package/tables/operateLog.json +68 -0
  129. package/tables/role.json +32 -0
  130. package/tables/sysConfig.json +43 -0
  131. package/utils/calcPerfTime.js +13 -0
  132. package/utils/cors.js +17 -0
  133. package/utils/deepMerge.js +78 -0
  134. package/utils/fieldClear.js +65 -0
  135. package/utils/formatYmdHms.js +23 -0
  136. package/utils/formatZodIssues.js +109 -0
  137. package/utils/getClientIp.js +47 -0
  138. package/utils/importDefault.js +51 -0
  139. package/utils/is.js +462 -0
  140. package/utils/loggerUtils.js +185 -0
  141. package/utils/processInfo.js +39 -0
  142. package/utils/regexpUtil.js +52 -0
  143. package/utils/response.js +114 -0
  144. package/utils/scanFiles.js +124 -0
  145. package/utils/scanSources.js +68 -0
  146. package/utils/sortModules.js +75 -0
  147. package/utils/toSessionTtlSeconds.js +14 -0
  148. package/utils/util.js +374 -0
  149. package/befly.js +0 -16413
  150. package/befly.min.js +0 -72
@@ -0,0 +1,131 @@
1
+ import { isNonEmptyString, isNullable, isNumber, isPlainObject, isString } from "../../utils/is.js";
2
+ import { canConvertToNumber } from "../../utils/util.js";
3
+
4
+ export function quoteIdentMySql(identifier) {
5
+ if (!isString(identifier)) {
6
+ throw new Error(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`, {
7
+ cause: null,
8
+ code: "validation"
9
+ });
10
+ }
11
+
12
+ const trimmed = identifier.trim();
13
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
14
+ throw new Error(`无效的 SQL 标识符: ${trimmed}`, {
15
+ cause: null,
16
+ code: "validation"
17
+ });
18
+ }
19
+
20
+ return `\`${trimmed}\``;
21
+ }
22
+
23
+ export function hasBegin(sql) {
24
+ return typeof sql.begin === "function";
25
+ }
26
+
27
+ export class DbSqlError extends Error {
28
+ constructor(message, options) {
29
+ super(message);
30
+ this.originalError = options.originalError;
31
+ this.params = options.params;
32
+ this.duration = options.duration;
33
+ this.sqlInfo = options.sqlInfo;
34
+ }
35
+ }
36
+
37
+ export class TransAbortError extends Error {
38
+ constructor(payload) {
39
+ super("TRANSACTION_ABORT");
40
+ this.payload = payload;
41
+ }
42
+ }
43
+
44
+ export function isBeflyResponse(value) {
45
+ if (!isPlainObject(value)) {
46
+ return false;
47
+ }
48
+
49
+ const record = value;
50
+ return isNumber(record["code"]) && isString(record["msg"]);
51
+ }
52
+
53
+ export function convertBigIntFields(arr, fields) {
54
+ if (isNullable(arr)) {
55
+ return arr;
56
+ }
57
+
58
+ const defaultFields = ["id", "pid", "sort"];
59
+
60
+ const buildFields = (userFields) => {
61
+ if (!userFields || userFields.length === 0) {
62
+ return defaultFields;
63
+ }
64
+
65
+ const merged = ["id", "pid", "sort"];
66
+ for (const f of userFields) {
67
+ if (!isString(f)) {
68
+ continue;
69
+ }
70
+ if (!isNonEmptyString(f)) {
71
+ continue;
72
+ }
73
+ const trimmed = f.trim();
74
+ if (!merged.includes(trimmed)) {
75
+ merged.push(trimmed);
76
+ }
77
+ }
78
+ return merged;
79
+ };
80
+
81
+ const effectiveFields = buildFields(fields);
82
+ const fieldSet = new Set(effectiveFields);
83
+
84
+ const convertRecord = (source) => {
85
+ const converted = {};
86
+
87
+ for (const [key, value] of Object.entries(source)) {
88
+ let nextValue = value;
89
+
90
+ if (!isNullable(value)) {
91
+ const shouldConvert = fieldSet.has(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
92
+
93
+ if (shouldConvert) {
94
+ let bigintValue = null;
95
+ if (typeof value === "bigint") {
96
+ bigintValue = value;
97
+ } else if (isString(value)) {
98
+ if (/^-?\d+$/.test(value)) {
99
+ try {
100
+ bigintValue = BigInt(value);
101
+ } catch {
102
+ bigintValue = null;
103
+ }
104
+ }
105
+ }
106
+
107
+ if (bigintValue !== null) {
108
+ const convertedNumber = canConvertToNumber(bigintValue);
109
+ if (convertedNumber !== null) {
110
+ nextValue = convertedNumber;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ converted[key] = nextValue;
117
+ }
118
+
119
+ return converted;
120
+ };
121
+
122
+ if (Array.isArray(arr)) {
123
+ return arr.map((item) => convertRecord(item));
124
+ }
125
+
126
+ if (typeof arr === "object") {
127
+ return convertRecord(arr);
128
+ }
129
+
130
+ return arr;
131
+ }
@@ -0,0 +1,505 @@
1
+ import { isNullable, isNumber } from "../../utils/is.js";
2
+ import { snakeCase } from "../../utils/util.js";
3
+ import { Logger } from "../logger.js";
4
+ import { SqlBuilder } from "../sqlBuilder/index.js";
5
+ import { quoteIdentMySql } from "./context.js";
6
+ import { toNumberFromSql } from "./util.js";
7
+ import { addDefaultStateFilter, buildInsertRow, buildPartialUpdateData, buildUpdateRow, clearDeep, whereKeysToSnake } from "./builders.js";
8
+ import { assertBatchInsertRowsConsistent, assertNoUndefinedInRecord, validateGeneratedBatchId, validateIncrementOptions, validateInsertBatchSize, validateNoJoinReadOptions, validatePageLimitRange, validateSafeFieldName, validateTableBatchDataOptions, validateTableDataOptions, validateTableName, validateTableWhereOptions } from "./validate.js";
9
+
10
+ export const dataOpsMethods = {
11
+ // 读取操作
12
+ async getCount(options) {
13
+ const { table, where, joins, tableQualifier } = await this.prepareQueryOptions(options, "getCount.options");
14
+ const hasJoins = Array.isArray(joins) && joins.length > 0;
15
+
16
+ const whereFiltered = addDefaultStateFilter(where, tableQualifier, hasJoins);
17
+ const result = await this.fetchCount({ table: table, joins: joins }, whereFiltered, "COUNT(*) as count");
18
+
19
+ return {
20
+ data: result.total,
21
+ sql: result.sql
22
+ };
23
+ },
24
+
25
+ async getOne(options) {
26
+ const { table, fields, where, joins, tableQualifier } = await this.prepareQueryOptions(options, "getOne.options");
27
+ const hasJoins = Array.isArray(joins) && joins.length > 0;
28
+
29
+ const whereFiltered = addDefaultStateFilter(where, tableQualifier, hasJoins);
30
+ const builder = this.createSqlBuilder().select(fields).from(table).where(whereFiltered);
31
+ this.applyJoins(builder, joins);
32
+
33
+ const { sql, params } = builder.toSelectSql();
34
+ const executeRes = await this.execute(sql, params);
35
+ const result = executeRes.data;
36
+
37
+ const data = this.normalizeRowData(result?.[0] || null, options.bigint);
38
+ return {
39
+ data: data,
40
+ sql: executeRes.sql
41
+ };
42
+ },
43
+
44
+ async getDetail(options) {
45
+ return await this.getOne(options);
46
+ },
47
+
48
+ async getList(options) {
49
+ const prepared = await this.prepareQueryOptions(options, "getList.options");
50
+ validatePageLimitRange(prepared, options.table);
51
+
52
+ const hasJoins = Array.isArray(prepared.joins) && prepared.joins.length > 0;
53
+ const whereFiltered = addDefaultStateFilter(prepared.where, prepared.tableQualifier, hasJoins);
54
+ const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
55
+ const total = countResult.total;
56
+
57
+ const offset = (prepared.page - 1) * prepared.limit;
58
+ const dataBuilder = this.createListBuilder(prepared, whereFiltered, prepared.limit, offset);
59
+ const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
60
+
61
+ if (total === 0) {
62
+ return {
63
+ data: {
64
+ lists: [],
65
+ total: 0,
66
+ page: prepared.page,
67
+ limit: prepared.limit,
68
+ pages: 0
69
+ },
70
+ sql: {
71
+ count: countResult.sql,
72
+ data: {
73
+ sql: dataSql,
74
+ params: dataParams,
75
+ duration: 0
76
+ }
77
+ }
78
+ };
79
+ }
80
+ const listExecuteRes = await this.execute(dataSql, dataParams);
81
+ const list = listExecuteRes.data || [];
82
+
83
+ return {
84
+ data: {
85
+ lists: this.normalizeListData(list, options.bigint),
86
+ total: total,
87
+ page: prepared.page,
88
+ limit: prepared.limit,
89
+ pages: Math.ceil(total / prepared.limit)
90
+ },
91
+ sql: {
92
+ count: countResult.sql,
93
+ data: listExecuteRes.sql
94
+ }
95
+ };
96
+ },
97
+
98
+ async getAll(options) {
99
+ const MAX_LIMIT = 10000;
100
+ const WARNING_LIMIT = 1000;
101
+ const prepareOptions = this.buildQueryOptions(options, { page: 1, limit: 10 });
102
+ const prepared = await this.prepareQueryOptions(prepareOptions, "getAll.options");
103
+
104
+ const hasJoins = Array.isArray(prepared.joins) && prepared.joins.length > 0;
105
+ const whereFiltered = addDefaultStateFilter(prepared.where, prepared.tableQualifier, hasJoins);
106
+ const countResult = await this.fetchCount(prepared, whereFiltered, "COUNT(*) as total");
107
+ const total = countResult.total;
108
+
109
+ if (total === 0) {
110
+ return {
111
+ data: {
112
+ lists: [],
113
+ total: 0
114
+ },
115
+ sql: {
116
+ count: countResult.sql
117
+ }
118
+ };
119
+ }
120
+
121
+ const dataBuilder = this.createListBuilder(prepared, whereFiltered, MAX_LIMIT, null);
122
+
123
+ const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
124
+ const listExecuteRes = await this.execute(dataSql, dataParams);
125
+ const result = listExecuteRes.data || [];
126
+
127
+ if (result.length >= WARNING_LIMIT) {
128
+ Logger.warn("getAll 返回数据过多,建议使用 getList 分页查询", { table: options.table, count: result.length, total: total });
129
+ }
130
+
131
+ if (result.length >= MAX_LIMIT) {
132
+ Logger.warn(`getAll 达到最大限制 ${MAX_LIMIT},实际总数 ${total},只返回前 ${MAX_LIMIT} 条`, { table: options.table, limit: MAX_LIMIT, total: total });
133
+ }
134
+
135
+ const lists = this.normalizeListData(result, options.bigint);
136
+
137
+ return {
138
+ data: {
139
+ lists: lists,
140
+ total: total
141
+ },
142
+ sql: {
143
+ count: countResult.sql,
144
+ data: listExecuteRes.sql
145
+ }
146
+ };
147
+ },
148
+
149
+ async exists(options) {
150
+ validateNoJoinReadOptions(options, "exists", "exists 不支持 joins(请使用显式 query 或拆分查询)");
151
+ const snakeTable = snakeCase(options.table);
152
+ const snakeWhere = whereKeysToSnake(clearDeep(options.where || {}));
153
+ const whereFiltered = addDefaultStateFilter(snakeWhere, snakeTable, false);
154
+
155
+ const builder = this.createSqlBuilder().selectRaw("COUNT(1) as cnt").from(snakeTable).where(whereFiltered).limit(1);
156
+ const { sql, params } = builder.toSelectSql();
157
+ const executeRes = await this.execute(sql, params);
158
+ const exists = (executeRes.data?.[0]?.cnt || 0) > 0;
159
+ return { data: exists, sql: executeRes.sql };
160
+ },
161
+
162
+ async getFieldValue(options) {
163
+ validateNoJoinReadOptions(options, "getFieldValue", "getFieldValue 不支持 joins(请使用 getOne/getList 并自行取字段)");
164
+ const field = options.field;
165
+ validateSafeFieldName(field);
166
+
167
+ const oneOptions = {
168
+ table: options.table
169
+ };
170
+ if (options.where !== undefined) oneOptions.where = options.where;
171
+ if (options.joins !== undefined) oneOptions.joins = options.joins;
172
+ if (options.orderBy !== undefined) oneOptions.orderBy = options.orderBy;
173
+ if (options.page !== undefined) oneOptions.page = options.page;
174
+ if (options.limit !== undefined) oneOptions.limit = options.limit;
175
+ oneOptions.fields = [field];
176
+
177
+ const oneRes = await this.getOne(oneOptions);
178
+
179
+ const result = oneRes.data;
180
+ const value = this.resolveFieldValue(result, field);
181
+ return {
182
+ data: value,
183
+ sql: oneRes.sql
184
+ };
185
+ },
186
+
187
+ // 写入操作
188
+
189
+ async insData(options) {
190
+ validateTableDataOptions(options, "insData");
191
+ const { table, data } = options;
192
+ const snakeTable = snakeCase(table);
193
+ const now = Date.now();
194
+
195
+ let processed;
196
+ if (this.idMode === "autoId") {
197
+ processed = buildInsertRow({ idMode: "autoId", data: data, now: now });
198
+ } else {
199
+ let id;
200
+ try {
201
+ id = await this.redis.genTimeID();
202
+ } catch (error) {
203
+ throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, {
204
+ cause: error,
205
+ code: "runtime",
206
+ subsystem: "db",
207
+ operation: "genTimeId",
208
+ table: table
209
+ });
210
+ }
211
+ processed = buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
212
+ }
213
+
214
+ assertNoUndefinedInRecord(processed, `insData 插入数据 (table: ${snakeTable})`);
215
+
216
+ const builder = this.createSqlBuilder();
217
+ const { sql, params } = builder.toInsertSql(snakeTable, processed);
218
+ const executeRes = await this.execute(sql, params);
219
+
220
+ const processedId = processed["id"];
221
+ const processedIdNum = isNumber(processedId) ? processedId : 0;
222
+ const lastInsertRowidNum = toNumberFromSql(executeRes.data?.lastInsertRowid);
223
+
224
+ const insertedId = this.idMode === "autoId" ? lastInsertRowidNum || 0 : processedIdNum || lastInsertRowidNum || 0;
225
+ if (this.idMode === "autoId" && insertedId <= 0) {
226
+ throw new Error(`插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`, {
227
+ cause: null,
228
+ code: "runtime"
229
+ });
230
+ }
231
+ return {
232
+ data: insertedId,
233
+ sql: executeRes.sql
234
+ };
235
+ },
236
+
237
+ async insBatch(table, dataList) {
238
+ validateTableBatchDataOptions(table, dataList, "insBatch");
239
+ if (dataList.length === 0) {
240
+ return {
241
+ data: [],
242
+ sql: { sql: "", params: [], duration: 0 }
243
+ };
244
+ }
245
+
246
+ const MAX_BATCH_SIZE = 1000;
247
+ validateInsertBatchSize(dataList.length, MAX_BATCH_SIZE);
248
+
249
+ const snakeTable = snakeCase(table);
250
+ const now = Date.now();
251
+ let ids = [];
252
+
253
+ let processedList;
254
+ if (this.idMode === "autoId") {
255
+ processedList = dataList.map((data) => {
256
+ return buildInsertRow({ idMode: "autoId", data: data, now: now });
257
+ });
258
+ } else {
259
+ const nextIds = [];
260
+ for (let i = 0; i < dataList.length; i++) {
261
+ nextIds.push(await this.redis.genTimeID());
262
+ }
263
+ ids = nextIds;
264
+ processedList = dataList.map((data, index) => {
265
+ const id = nextIds[index];
266
+ validateGeneratedBatchId(id, snakeTable, index);
267
+ return buildInsertRow({ idMode: "timeId", data: data, id: id, now: now });
268
+ });
269
+ }
270
+
271
+ const insertFields = assertBatchInsertRowsConsistent(processedList, { table: snakeTable });
272
+ const builder = this.createSqlBuilder();
273
+ const { sql, params } = builder.toInsertSql(snakeTable, processedList);
274
+
275
+ try {
276
+ const executeRes = await this.execute(sql, params);
277
+
278
+ if (this.idMode === "autoId") {
279
+ const firstId = toNumberFromSql(executeRes.data?.lastInsertRowid);
280
+ if (firstId <= 0) {
281
+ throw new Error(`批量插入失败:autoId 模式下无法获取 lastInsertRowid (table: ${table})`, {
282
+ cause: null,
283
+ code: "runtime"
284
+ });
285
+ }
286
+
287
+ const outIds = [];
288
+ for (let i = 0; i < dataList.length; i++) {
289
+ outIds.push(firstId + i);
290
+ }
291
+
292
+ return {
293
+ data: outIds,
294
+ sql: executeRes.sql
295
+ };
296
+ }
297
+
298
+ return {
299
+ data: ids,
300
+ sql: executeRes.sql
301
+ };
302
+ } catch (error) {
303
+ Logger.error("批量插入失败", error, {
304
+ table: table,
305
+ snakeTable: snakeTable,
306
+ count: dataList.length,
307
+ fields: insertFields
308
+ });
309
+ throw new Error("批量插入失败", {
310
+ cause: error,
311
+ code: "runtime",
312
+ subsystem: "sql",
313
+ operation: "insBatch",
314
+ table: table,
315
+ snakeTable: snakeTable,
316
+ count: dataList.length,
317
+ fields: insertFields
318
+ });
319
+ }
320
+ },
321
+
322
+ async delForceBatch(table, ids) {
323
+ validateTableName(table, "delForceBatch.table");
324
+ if (ids.length === 0) {
325
+ return {
326
+ data: 0,
327
+ sql: { sql: "", params: [], duration: 0 }
328
+ };
329
+ }
330
+
331
+ const snakeTable = snakeCase(table);
332
+
333
+ const query = SqlBuilder.toDeleteInSql({
334
+ table: snakeTable,
335
+ idField: "id",
336
+ ids: ids,
337
+ quoteIdent: quoteIdentMySql
338
+ });
339
+ const executeRes = await this.execute(query.sql, query.params);
340
+ const changes = toNumberFromSql(executeRes.data?.changes);
341
+ return {
342
+ data: changes,
343
+ sql: executeRes.sql
344
+ };
345
+ },
346
+
347
+ async updBatch(table, dataList) {
348
+ validateTableBatchDataOptions(table, dataList, "updBatch");
349
+ if (dataList.length === 0) {
350
+ return {
351
+ data: 0,
352
+ sql: { sql: "", params: [], duration: 0 }
353
+ };
354
+ }
355
+
356
+ const snakeTable = snakeCase(table);
357
+ const now = Date.now();
358
+
359
+ const processedList = [];
360
+ const fieldSet = new Set();
361
+
362
+ for (const item of dataList) {
363
+ const userData = buildPartialUpdateData({ data: item.data, allowState: true });
364
+
365
+ for (const key of Object.keys(userData)) {
366
+ fieldSet.add(key);
367
+ }
368
+
369
+ processedList.push({ id: item.id, data: userData });
370
+ }
371
+
372
+ const fields = Array.from(fieldSet).sort();
373
+ if (fields.length === 0) {
374
+ return {
375
+ data: 0,
376
+ sql: { sql: "", params: [], duration: 0 }
377
+ };
378
+ }
379
+
380
+ const query = SqlBuilder.toUpdateCaseByIdSql({
381
+ table: snakeTable,
382
+ idField: "id",
383
+ rows: processedList,
384
+ fields: fields,
385
+ quoteIdent: quoteIdentMySql,
386
+ updatedAtField: "updated_at",
387
+ updatedAtValue: now,
388
+ stateField: "state",
389
+ stateGtZero: true
390
+ });
391
+
392
+ const executeRes = await this.execute(query.sql, query.params);
393
+ const changes = toNumberFromSql(executeRes.data?.changes);
394
+ return {
395
+ data: changes,
396
+ sql: executeRes.sql
397
+ };
398
+ },
399
+
400
+ async updData(options) {
401
+ validateTableDataOptions(options, "updData");
402
+ validateTableWhereOptions(options, "updData", true);
403
+ const { table, data, where } = options;
404
+ const snakeTable = snakeCase(table);
405
+ const snakeWhere = whereKeysToSnake(clearDeep(where));
406
+
407
+ const processed = buildUpdateRow({ data: data, now: Date.now(), allowState: true });
408
+ const whereFiltered = addDefaultStateFilter(snakeWhere, snakeTable, false);
409
+ const builder = this.createSqlBuilder().where(whereFiltered);
410
+ const { sql, params } = builder.toUpdateSql(snakeTable, processed);
411
+
412
+ const executeRes = await this.execute(sql, params);
413
+ const changes = toNumberFromSql(executeRes.data?.changes);
414
+ return {
415
+ data: changes,
416
+ sql: executeRes.sql
417
+ };
418
+ },
419
+
420
+ async delData(options) {
421
+ validateTableWhereOptions(options, "delData", true);
422
+ const { table, where } = options;
423
+
424
+ return await this.updData({
425
+ table: table,
426
+ data: { state: 0, deleted_at: Date.now() },
427
+ where: where
428
+ });
429
+ },
430
+
431
+ async delForce(options) {
432
+ validateTableWhereOptions(options, "delForce", true);
433
+ const { table, where } = options;
434
+
435
+ const snakeTable = snakeCase(table);
436
+ const snakeWhere = whereKeysToSnake(clearDeep(where));
437
+
438
+ const builder = this.createSqlBuilder().where(snakeWhere);
439
+ const { sql, params } = builder.toDeleteSql(snakeTable);
440
+
441
+ const executeRes = await this.execute(sql, params);
442
+ const changes = toNumberFromSql(executeRes.data?.changes);
443
+ return {
444
+ data: changes,
445
+ sql: executeRes.sql
446
+ };
447
+ },
448
+
449
+ async disableData(options) {
450
+ validateTableWhereOptions(options, "disableData", true);
451
+ const { table, where } = options;
452
+
453
+ return await this.updData({
454
+ table: table,
455
+ data: {
456
+ state: 2
457
+ },
458
+ where: where
459
+ });
460
+ },
461
+
462
+ async enableData(options) {
463
+ validateTableWhereOptions(options, "enableData", true);
464
+ const { table, where } = options;
465
+
466
+ return await this.updData({
467
+ table: table,
468
+ data: {
469
+ state: 1
470
+ },
471
+ where: where
472
+ });
473
+ },
474
+
475
+ async increment(table, field, where, value = 1) {
476
+ validateIncrementOptions(table, field, where, value, "increment");
477
+ const snakeTable = snakeCase(table);
478
+ const snakeField = snakeCase(field);
479
+
480
+ const snakeWhere = whereKeysToSnake(clearDeep(where));
481
+ const whereFiltered = addDefaultStateFilter(snakeWhere, snakeTable, false);
482
+ const builder = this.createSqlBuilder().where(whereFiltered);
483
+ const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
484
+
485
+ const quotedTable = quoteIdentMySql(snakeTable);
486
+ const quotedField = quoteIdentMySql(snakeField);
487
+ const sql = whereClause ? `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ? WHERE ${whereClause}` : `UPDATE ${quotedTable} SET ${quotedField} = ${quotedField} + ?`;
488
+
489
+ const params = [value];
490
+ for (const param of whereParams) {
491
+ params.push(param);
492
+ }
493
+
494
+ const executeRes = await this.execute(sql, params);
495
+ const changes = toNumberFromSql(executeRes.data?.changes);
496
+ return {
497
+ data: changes,
498
+ sql: executeRes.sql
499
+ };
500
+ },
501
+
502
+ async decrement(table, field, where, value = 1) {
503
+ return await this.increment(table, field, where, -value);
504
+ }
505
+ };
@@ -0,0 +1,65 @@
1
+ import { toSqlParams } from "../sqlBuilder/util.js";
2
+ import { snakeCase } from "../../utils/util.js";
3
+ import { DbSqlError } from "./context.js";
4
+ import { validateExecuteSql } from "./validate.js";
5
+
6
+ export const executeMethods = {
7
+ async execute(sql, params) {
8
+ if (!this.sql) {
9
+ throw new Error("数据库连接未初始化", {
10
+ cause: null,
11
+ code: "runtime"
12
+ });
13
+ }
14
+
15
+ validateExecuteSql(sql);
16
+
17
+ const startTime = Date.now();
18
+ const safeParams = toSqlParams(params);
19
+
20
+ try {
21
+ let queryResult;
22
+ if (safeParams.length > 0) {
23
+ queryResult = await this.sql.unsafe(sql, safeParams);
24
+ } else {
25
+ queryResult = await this.sql.unsafe(sql);
26
+ }
27
+
28
+ const duration = Date.now() - startTime;
29
+
30
+ return {
31
+ data: queryResult,
32
+ sql: {
33
+ sql: sql,
34
+ params: safeParams,
35
+ duration: duration
36
+ }
37
+ };
38
+ } catch (error) {
39
+ const duration = Date.now() - startTime;
40
+ const msg = error instanceof Error ? error.message : String(error);
41
+
42
+ throw new DbSqlError(`SQL执行失败: ${msg}`, {
43
+ originalError: error,
44
+ params: safeParams,
45
+ duration: duration,
46
+ sqlInfo: {
47
+ sql: sql,
48
+ params: safeParams,
49
+ duration: duration
50
+ }
51
+ });
52
+ }
53
+ },
54
+
55
+ async tableExists(tableName) {
56
+ const snakeTableName = snakeCase(tableName);
57
+ const executeRes = await this.execute("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
58
+ const exists = (executeRes.data?.[0]?.count || 0) > 0;
59
+
60
+ return {
61
+ data: exists,
62
+ sql: executeRes.sql
63
+ };
64
+ }
65
+ };