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.
- package/apis/dashboard/systemResources.js +3 -1
- package/apis/dict/_dict.js +1 -1
- package/apis/role/_role.js +2 -2
- package/apis/tongJi/dailyStats.js +2 -2
- package/apis/tongJi/dailyStatsDistribution.js +3 -3
- package/apis/tongJi/errorList.js +2 -2
- package/apis/tongJi/errorStats.js +6 -6
- package/checks/api.js +11 -6
- package/checks/items.js +56 -0
- package/checks/menu.js +1 -1
- package/checks/table.js +4 -5
- package/configs/beflyConfig.json +5 -10
- package/configs/constConfig.js +44 -3
- package/hooks/auth.js +3 -2
- package/hooks/parser.js +15 -4
- package/hooks/permission.js +3 -2
- package/hooks/rateLimit.js +16 -3
- package/hooks/validator.js +2 -1
- package/index.js +53 -24
- package/lib/cacheHelper.js +83 -229
- package/lib/connect.js +3 -13
- package/lib/dbHelper.js +57 -142
- package/lib/dbParse.js +65 -248
- package/lib/dbUtil.js +19 -63
- package/lib/logger.js +45 -22
- package/lib/redisHelper.js +186 -420
- package/lib/smtpText.js +7 -18
- package/lib/sqlBuilder.js +22 -51
- package/lib/xmlParse.js +15 -59
- package/package.json +1 -1
- package/plugins/cache.js +2 -3
- package/plugins/config.js +3 -1
- package/plugins/cron.js +2 -2
- package/plugins/email.js +3 -1
- package/plugins/logger.js +2 -2
- package/plugins/mysql.js +5 -10
- package/plugins/redis.js +4 -11
- package/plugins/tool.js +3 -3
- package/plugins/xmlParse.js +3 -1
- package/router/api.js +2 -6
- package/router/static.js +9 -10
- package/schemas/config.js +4 -4
- package/scripts/syncDb/context.js +4 -7
- package/scripts/syncDb/diff.js +2 -1
- package/scripts/syncDb/index.js +2 -6
- package/sync/api.js +7 -8
- package/sync/cache.js +3 -1
- package/sync/dev.js +28 -65
- package/sync/menu.js +5 -6
- package/sync/syncUtil.js +25 -0
- package/utils/calcPerfTime.js +2 -3
- package/utils/crypto.js +0 -40
- package/utils/deepMerge.js +2 -6
- package/utils/error.js +64 -0
- package/utils/fieldClear.js +13 -56
- package/utils/importDefault.js +11 -12
- package/utils/is.js +5 -182
- package/utils/loggerUtils.js +3 -1
- package/utils/response.js +12 -8
- package/utils/scanFiles.js +2 -53
- package/utils/scanSources.js +9 -0
- package/utils/util.js +25 -222
- package/checks/hook.js +0 -39
- package/checks/plugin.js +0 -39
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
203
|
-
cause: null,
|
|
204
|
-
code: "validation"
|
|
205
|
-
});
|
|
181
|
+
throw validationError("批量插入 rows 必须是数组");
|
|
206
182
|
}
|
|
207
183
|
if (rows.length === 0) {
|
|
208
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
289
|
-
throw
|
|
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
|
|
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
|
|
325
|
-
cause: null,
|
|
326
|
-
code: "runtime"
|
|
327
|
-
});
|
|
272
|
+
throw createError("数据库连接未初始化", { code: "runtime" });
|
|
328
273
|
}
|
|
329
274
|
|
|
330
275
|
if (!isString(sql)) {
|
|
331
|
-
throw
|
|
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
|
-
|
|
280
|
+
const safeParams = [];
|
|
339
281
|
if (!isNullable(params)) {
|
|
340
282
|
if (!Array.isArray(params)) {
|
|
341
|
-
throw
|
|
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
|
|
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
|
-
|
|
367
|
-
const timer = setTimeout(() => {}, 2147483647);
|
|
301
|
+
// 查询超时兜底:SQL 响应丢失/挂起时显式报错,避免进程静默卡死
|
|
368
302
|
const queryPromise = safeParams.length > 0 ? this.sql.unsafe(sql, safeParams) : this.sql.unsafe(sql);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
+
// 沿 cause 链取根因判断,保证错误被包装后重试逻辑依然生效
|
|
349
|
+
const rootCause = getRootCause(error);
|
|
350
|
+
if (rootCause?.code === "ERR_MYSQL_CONNECTION_CLOSED") {
|
|
415
351
|
return true;
|
|
416
352
|
}
|
|
417
|
-
if (
|
|
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
|
|
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
|
|
507
|
+
throw createError(`生成 ID 失败,Redis 可能不可用 (table: ${table})`, {
|
|
578
508
|
cause: error,
|
|
579
509
|
code: "runtime",
|
|
580
510
|
subsystem: "db",
|
|
581
|
-
operation: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) => {
|