befly 3.74.2 → 3.75.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.
Files changed (65) hide show
  1. package/apis/dashboard/systemResources.js +3 -1
  2. package/apis/dict/_dict.js +1 -1
  3. package/apis/role/_role.js +2 -2
  4. package/apis/tongJi/dailyStats.js +2 -2
  5. package/apis/tongJi/dailyStatsDistribution.js +3 -3
  6. package/apis/tongJi/errorList.js +2 -2
  7. package/apis/tongJi/errorStats.js +6 -6
  8. package/checks/api.js +11 -6
  9. package/checks/items.js +56 -0
  10. package/checks/menu.js +1 -1
  11. package/checks/table.js +4 -5
  12. package/configs/beflyConfig.json +5 -10
  13. package/configs/constConfig.js +44 -3
  14. package/hooks/auth.js +3 -2
  15. package/hooks/parser.js +15 -4
  16. package/hooks/permission.js +3 -2
  17. package/hooks/rateLimit.js +16 -3
  18. package/hooks/validator.js +2 -1
  19. package/index.js +53 -24
  20. package/lib/cacheHelper.js +83 -229
  21. package/lib/connect.js +3 -13
  22. package/lib/dbHelper.js +57 -142
  23. package/lib/dbParse.js +65 -248
  24. package/lib/dbUtil.js +19 -63
  25. package/lib/logger.js +45 -22
  26. package/lib/redisHelper.js +186 -420
  27. package/lib/smtpText.js +7 -18
  28. package/lib/sqlBuilder.js +22 -51
  29. package/lib/xmlParse.js +15 -59
  30. package/package.json +1 -1
  31. package/plugins/cache.js +2 -3
  32. package/plugins/config.js +3 -1
  33. package/plugins/cron.js +2 -2
  34. package/plugins/email.js +3 -1
  35. package/plugins/logger.js +2 -2
  36. package/plugins/mysql.js +5 -10
  37. package/plugins/redis.js +4 -11
  38. package/plugins/tool.js +3 -3
  39. package/plugins/xmlParse.js +3 -1
  40. package/router/api.js +2 -6
  41. package/router/static.js +9 -10
  42. package/schemas/config.js +4 -4
  43. package/scripts/syncDb/context.js +4 -7
  44. package/scripts/syncDb/diff.js +2 -1
  45. package/scripts/syncDb/index.js +2 -6
  46. package/sync/api.js +7 -8
  47. package/sync/cache.js +3 -1
  48. package/sync/dev.js +28 -65
  49. package/sync/menu.js +5 -6
  50. package/sync/syncUtil.js +25 -0
  51. package/utils/calcPerfTime.js +2 -3
  52. package/utils/crypto.js +0 -40
  53. package/utils/deepMerge.js +2 -6
  54. package/utils/error.js +64 -0
  55. package/utils/fieldClear.js +13 -56
  56. package/utils/importDefault.js +11 -12
  57. package/utils/is.js +5 -182
  58. package/utils/loggerUtils.js +3 -1
  59. package/utils/response.js +12 -8
  60. package/utils/scanFiles.js +2 -53
  61. package/utils/scanSources.js +9 -0
  62. package/utils/util.js +25 -222
  63. package/checks/hook.js +0 -39
  64. package/checks/plugin.js +0 -39
  65. package/utils/processInfo.js +0 -39
package/lib/dbHelper.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { createError, getRootCause, validationError } from "../utils/error.js";
1
2
  import { fieldClear } from "../utils/fieldClear.js";
2
- import { isNonEmptyString, isNullable, isNumber, isPlainObject, isString } from "../utils/is.js";
3
+ import { isLowerCamelCaseName, isNonEmptyString, isNullable, isNumber, isPlainObject, isString } from "../utils/is.js";
3
4
  import { camelCase, canConvertToNumber, keysToSnake, snakeCase } from "../utils/util.js";
4
5
  import { DbParse } from "./dbParse.js";
5
6
  import { deserializeArrayFields, parseTableRef, serializeArrayFields } from "./dbUtil.js";
@@ -8,18 +9,12 @@ import { SqlBuilder } from "./sqlBuilder.js";
8
9
 
9
10
  function quoteIdentMySql(identifier) {
10
11
  if (!isString(identifier)) {
11
- throw new Error(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`, {
12
- cause: null,
13
- code: "validation"
14
- });
12
+ throw validationError(`quoteIdentifier 需要字符串类型标识符 (identifier: ${String(identifier)})`);
15
13
  }
16
14
 
17
15
  const trimmed = identifier.trim();
18
16
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
19
- throw new Error(`无效的 SQL 标识符: ${trimmed}`, {
20
- cause: null,
21
- code: "validation"
22
- });
17
+ throw validationError(`无效的 SQL 标识符: ${trimmed}`);
23
18
  }
24
19
 
25
20
  return `\`${trimmed}\``;
@@ -114,28 +109,19 @@ function getExecuteErrorMessage(error) {
114
109
 
115
110
  function assertGeneratedBatchId(id, table, index) {
116
111
  if (typeof id !== "number") {
117
- throw new Error(`批量插入生成 ID 失败:ids[${index}] 不是 number (table: ${table})`, {
118
- cause: null,
119
- code: "runtime"
120
- });
112
+ throw createError(`批量插入生成 ID 失败:ids[${index}] 不是 number (table: ${table})`, { code: "runtime" });
121
113
  }
122
114
  }
123
115
 
124
116
  function assertTimeIdValue(id) {
125
117
  if (!Number.isFinite(id) || id <= 0) {
126
- throw new Error(`buildInsertRow(timeId) 失败:id 必须为 > 0 的有限 number (id: ${String(id)})`, {
127
- cause: null,
128
- code: "validation"
129
- });
118
+ throw validationError(`buildInsertRow(timeId) 失败:id 必须为 > 0 的有限 number (id: ${String(id)})`);
130
119
  }
131
120
  }
132
121
 
133
122
  function assertInsertBatchSize(count, maxBatchSize) {
134
123
  if (count > maxBatchSize) {
135
- throw new Error(`批量插入数量 ${count} 超过最大限制 ${maxBatchSize}`, {
136
- cause: null,
137
- code: "validation"
138
- });
124
+ throw validationError(`批量插入数量 ${count} 超过最大限制 ${maxBatchSize}`);
139
125
  }
140
126
  }
141
127
 
@@ -144,10 +130,7 @@ function assertWriteDataHasFields(data, message, table) {
144
130
  return;
145
131
  }
146
132
 
147
- throw new Error(`${message} (table: ${table})`, {
148
- cause: null,
149
- code: "validation"
150
- });
133
+ throw validationError(`${message} (table: ${table})`);
151
134
  }
152
135
 
153
136
  function getTableFieldNames(tableInfo) {
@@ -173,8 +156,7 @@ function assertWriteFieldNamesDefined(tableInfo, fieldNames, table, label) {
173
156
  return;
174
157
  }
175
158
 
176
- throw new Error(`${label} 包含表 ${table} 未定义字段: ${invalidFields.join(", ")}`, {
177
- cause: null,
159
+ throw createError(`${label} 包含表 ${table} 未定义字段: ${invalidFields.join(", ")}`, {
178
160
  code: "validation",
179
161
  table: table,
180
162
  invalidFields: invalidFields,
@@ -189,74 +171,47 @@ function assertWriteFieldsDefined(tableInfo, data, table, label) {
189
171
  function assertNoUndefinedInRecord(row, label) {
190
172
  for (const [key, value] of Object.entries(row)) {
191
173
  if (value === undefined) {
192
- throw new Error(`${label} 存在 undefined 字段值 (field: ${key})`, {
193
- cause: null,
194
- code: "validation"
195
- });
174
+ throw validationError(`${label} 存在 undefined 字段值 (field: ${key})`);
196
175
  }
197
176
  }
198
177
  }
199
178
 
200
179
  function assertBatchInsertRowsConsistent(rows, options) {
201
180
  if (!Array.isArray(rows)) {
202
- throw new Error("批量插入 rows 必须是数组", {
203
- cause: null,
204
- code: "validation"
205
- });
181
+ throw validationError("批量插入 rows 必须是数组");
206
182
  }
207
183
  if (rows.length === 0) {
208
- throw new Error(`插入数据不能为空 (table: ${options.table})`, {
209
- cause: null,
210
- code: "validation"
211
- });
184
+ throw validationError(`插入数据不能为空 (table: ${options.table})`);
212
185
  }
213
186
 
214
187
  const first = rows[0];
215
188
  if (!first || typeof first !== "object" || Array.isArray(first)) {
216
- throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`, {
217
- cause: null,
218
- code: "validation"
219
- });
189
+ throw validationError(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`);
220
190
  }
221
191
 
222
192
  const fields = Object.keys(first);
223
193
  if (fields.length === 0) {
224
- throw new Error(`插入数据必须至少有一个字段 (table: ${options.table})`, {
225
- cause: null,
226
- code: "validation"
227
- });
194
+ throw validationError(`插入数据必须至少有一个字段 (table: ${options.table})`);
228
195
  }
229
196
 
230
197
  const fieldSet = new Set(fields);
231
198
  for (let i = 0; i < rows.length; i++) {
232
199
  const row = rows[i];
233
200
  if (!row || typeof row !== "object" || Array.isArray(row)) {
234
- throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`, {
235
- cause: null,
236
- code: "validation"
237
- });
201
+ throw validationError(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`);
238
202
  }
239
203
 
240
204
  const rowKeys = Object.keys(row);
241
205
  if (rowKeys.length !== fields.length) {
242
- throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`, {
243
- cause: null,
244
- code: "validation"
245
- });
206
+ throw validationError(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`);
246
207
  }
247
208
 
248
209
  for (const key of rowKeys) {
249
210
  if (!fieldSet.has(key)) {
250
- throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`, {
251
- cause: null,
252
- code: "validation"
253
- });
211
+ throw validationError(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`);
254
212
  }
255
213
  if (row[key] === undefined) {
256
- throw new Error(`批量插入字段值不能为 undefined (table: ${options.table}, rowIndex: ${i}, field: ${key})`, {
257
- cause: null,
258
- code: "validation"
259
- });
214
+ throw validationError(`批量插入字段值不能为 undefined (table: ${options.table}, rowIndex: ${i}, field: ${key})`);
260
215
  }
261
216
  }
262
217
  }
@@ -269,10 +224,7 @@ class DbHelper {
269
224
  this.redis = options.redis;
270
225
 
271
226
  if (!isNonEmptyString(options.dbName)) {
272
- throw new Error("DbHelper 初始化失败:dbName 必须为非空字符串", {
273
- cause: null,
274
- code: "validation"
275
- });
227
+ throw validationError("DbHelper 初始化失败:dbName 必须为非空字符串");
276
228
  }
277
229
  this.dbName = options.dbName;
278
230
 
@@ -280,26 +232,22 @@ class DbHelper {
280
232
  this.isTransaction = options.isTransaction === true;
281
233
  this.beflyMode = options.beflyMode === "manual" ? "manual" : "auto";
282
234
  this.tables = isPlainObject(options.tables) ? options.tables : {};
235
+ // 查询超时(毫秒):防止 SQL 响应丢失/挂起导致启动或请求静默卡死
236
+ this.queryTimeout = (options.queryTimeout || 30) * 1000;
283
237
  }
284
238
 
285
239
  getTableDef(table) {
286
240
  const tableName = parseTableRef(table).table;
287
241
 
288
- if (!/^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/.test(tableName)) {
289
- throw new Error(`表名必须使用小驼峰写法 (table: ${tableName})`, {
290
- cause: null,
291
- code: "validation"
292
- });
242
+ if (!isLowerCamelCaseName(tableName)) {
243
+ throw validationError(`表名必须使用小驼峰写法 (table: ${tableName})`);
293
244
  }
294
245
 
295
246
  const tableInfo = this.tables[tableName];
296
247
 
297
248
  if (!isPlainObject(tableInfo)) {
298
249
  const sourceLabel = tableName.startsWith("befly") ? "packages/core/tables" : "app tables";
299
- throw new Error(`表 ${tableName} 缺少本地 tables 定义,请检查 ${sourceLabel}`, {
300
- cause: null,
301
- code: "runtime"
302
- });
250
+ throw createError(`表 ${tableName} 缺少本地 tables 定义,请检查 ${sourceLabel}`, { code: "runtime" });
303
251
  }
304
252
 
305
253
  return tableInfo;
@@ -321,36 +269,23 @@ class DbHelper {
321
269
 
322
270
  async execute(sql, params, retryOnConnectionClosed = true) {
323
271
  if (!this.sql) {
324
- throw new Error("数据库连接未初始化", {
325
- cause: null,
326
- code: "runtime"
327
- });
272
+ throw createError("数据库连接未初始化", { code: "runtime" });
328
273
  }
329
274
 
330
275
  if (!isString(sql)) {
331
- throw new Error(`execute 只接受字符串类型的 SQL,收到类型: ${typeof sql},值: ${JSON.stringify(sql)}`, {
332
- cause: null,
333
- code: "validation"
334
- });
276
+ throw validationError(`execute 只接受字符串类型的 SQL,收到类型: ${typeof sql},值: ${JSON.stringify(sql)}`);
335
277
  }
336
278
 
337
279
  const startTime = Date.now();
338
- let safeParams = [];
280
+ const safeParams = [];
339
281
  if (!isNullable(params)) {
340
282
  if (!Array.isArray(params)) {
341
- throw new Error(`SQL 参数必须是数组,当前类型: ${typeof params}`, {
342
- cause: null,
343
- code: "validation"
344
- });
283
+ throw validationError(`SQL 参数必须是数组,当前类型: ${typeof params}`);
345
284
  }
346
285
 
347
- safeParams = [];
348
286
  for (const value of params) {
349
287
  if (value === undefined) {
350
- throw new Error("SQL 参数不能为 undefined", {
351
- cause: null,
352
- code: "validation"
353
- });
288
+ throw validationError("SQL 参数不能为 undefined");
354
289
  }
355
290
 
356
291
  if (typeof value === "bigint") {
@@ -363,14 +298,16 @@ class DbHelper {
363
298
  }
364
299
 
365
300
  try {
366
- let queryResult;
367
- const timer = setTimeout(() => {}, 2147483647);
301
+ // 查询超时兜底:SQL 响应丢失/挂起时显式报错,避免进程静默卡死
368
302
  const queryPromise = safeParams.length > 0 ? this.sql.unsafe(sql, safeParams) : this.sql.unsafe(sql);
369
- try {
370
- queryResult = await queryPromise;
371
- } finally {
372
- clearTimeout(timer);
373
- }
303
+ let timeoutId;
304
+ const timeoutPromise = new Promise((_, reject) => {
305
+ timeoutId = setTimeout(() => {
306
+ reject(createError(`SQL 执行超时(${this.queryTimeout}ms): ${sql.slice(0, 100)}`, { code: "runtime", subsystem: "sql", operation: "execute", sqlInfo: { sql: sql, params: safeParams } }));
307
+ }, this.queryTimeout);
308
+ });
309
+ const queryResult = await Promise.race([queryPromise, timeoutPromise]);
310
+ clearTimeout(timeoutId);
374
311
 
375
312
  const duration = Date.now() - startTime;
376
313
 
@@ -395,26 +332,25 @@ class DbHelper {
395
332
  params: safeParams,
396
333
  duration: duration
397
334
  };
398
- const wrappedError = new Error(`SQL执行失败: ${msg}`, {
399
- cause: error
335
+ throw createError(`SQL执行失败: ${msg}`, {
336
+ cause: error,
337
+ code: "runtime",
338
+ subsystem: "sql",
339
+ operation: "execute",
340
+ params: safeParams,
341
+ duration: duration,
342
+ sqlInfo: sqlInfo
400
343
  });
401
-
402
- wrappedError.code = "runtime";
403
- wrappedError.subsystem = "sql";
404
- wrappedError.operation = "execute";
405
- wrappedError.params = safeParams;
406
- wrappedError.duration = duration;
407
- wrappedError.sqlInfo = sqlInfo;
408
-
409
- throw wrappedError;
410
344
  }
411
345
  }
412
346
 
413
347
  isConnectionClosedError(error, msg) {
414
- if (error?.code === "ERR_MYSQL_CONNECTION_CLOSED") {
348
+ // 沿 cause 链取根因判断,保证错误被包装后重试逻辑依然生效
349
+ const rootCause = getRootCause(error);
350
+ if (rootCause?.code === "ERR_MYSQL_CONNECTION_CLOSED") {
415
351
  return true;
416
352
  }
417
- if (error?.code === "ERR_POSTGRES_CONNECTION_CLOSED") {
353
+ if (rootCause?.code === "ERR_POSTGRES_CONNECTION_CLOSED") {
418
354
  return true;
419
355
  }
420
356
  if (typeof msg === "string" && msg.includes("Connection closed")) {
@@ -467,9 +403,6 @@ class DbHelper {
467
403
 
468
404
  const camelRow = this.normalizeDbFieldNames(row);
469
405
  const deserialized = deserializeArrayFields(camelRow);
470
- if (!deserialized) {
471
- return {};
472
- }
473
406
 
474
407
  const convertedList = normalizeBigIntValues([deserialized]);
475
408
  if (convertedList && convertedList.length > 0) {
@@ -534,10 +467,7 @@ class DbHelper {
534
467
  const fieldNames = Object.keys(tableInfo);
535
468
 
536
469
  if (fieldNames.length === 0) {
537
- throw new Error(`表 ${table} 的本地 tables 定义为空或没有字段`, {
538
- cause: null,
539
- code: "runtime"
540
- });
470
+ throw createError(`表 ${table} 的本地 tables 定义为空或没有字段`, { code: "runtime" });
541
471
  }
542
472
 
543
473
  return fieldNames.map((fieldName) => snakeCase(fieldName));
@@ -574,11 +504,11 @@ class DbHelper {
574
504
  }
575
505
  } catch (error) {
576
506
  if (dataList.length === 1) {
577
- throw new Error(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, {
507
+ throw createError(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, {
578
508
  cause: error,
579
509
  code: "runtime",
580
510
  subsystem: "db",
581
- operation: "genTimeId",
511
+ operation: "genTimeID",
582
512
  table: table
583
513
  });
584
514
  }
@@ -610,10 +540,7 @@ class DbHelper {
610
540
 
611
541
  if (this.beflyMode === "manual") {
612
542
  if (lastInsertRowidNum <= 0) {
613
- throw new Error(operation === "insBatch" ? `批量插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})` : `插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})`, {
614
- cause: null,
615
- code: "runtime"
616
- });
543
+ throw createError(operation === "insBatch" ? `批量插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})` : `插入失败:beflyMode=manual 时无法获取 lastInsertRowid (table: ${table})`, { code: "runtime" });
617
544
  }
618
545
 
619
546
  if (count === 1) {
@@ -848,13 +775,7 @@ class DbHelper {
848
775
  sql: executeRes.sql
849
776
  };
850
777
  } catch (error) {
851
- Logger.error("批量插入失败", error, {
852
- table: table,
853
- snakeTable: snakeTable,
854
- count: dataList.length,
855
- fields: insertFields
856
- });
857
- throw new Error("批量插入失败", {
778
+ throw createError("批量插入失败", {
858
779
  cause: error,
859
780
  code: "runtime",
860
781
  subsystem: "sql",
@@ -1053,17 +974,11 @@ class DbHelper {
1053
974
 
1054
975
  async trans(callback) {
1055
976
  if (this.isTransaction) {
1056
- throw new Error("不支持嵌套事务", {
1057
- cause: null,
1058
- code: "policy"
1059
- });
977
+ throw createError("不支持嵌套事务", { code: "policy" });
1060
978
  }
1061
979
 
1062
980
  if (typeof this.sql?.begin !== "function") {
1063
- throw new Error("不支持事务 begin() 方法", {
1064
- cause: null,
1065
- code: "runtime"
1066
- });
981
+ throw createError("不支持事务 begin() 方法", { code: "runtime" });
1067
982
  }
1068
983
 
1069
984
  return await this.sql.begin(async (tx) => {