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/dbUtil.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validationError } from "../utils/error.js";
|
|
1
2
|
import { isFiniteNumber, isNonEmptyString, isNullable, isString } from "../utils/is.js";
|
|
2
3
|
import { snakeCase } from "../utils/util.js";
|
|
3
4
|
|
|
@@ -11,57 +12,36 @@ export function assertNoExprField(field) {
|
|
|
11
12
|
|
|
12
13
|
const trimmed = field.trim();
|
|
13
14
|
if (trimmed.includes("(") || trimmed.includes(")")) {
|
|
14
|
-
throw
|
|
15
|
-
cause: null,
|
|
16
|
-
code: "validation"
|
|
17
|
-
});
|
|
15
|
+
throw validationError(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`);
|
|
18
16
|
}
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export function parseTableRef(tableRef) {
|
|
22
20
|
if (!isString(tableRef)) {
|
|
23
|
-
throw
|
|
24
|
-
cause: null,
|
|
25
|
-
code: "validation"
|
|
26
|
-
});
|
|
21
|
+
throw validationError(`tableRef 必须是字符串 (tableRef: ${String(tableRef)})`);
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
const trimmed = tableRef.trim();
|
|
30
25
|
if (!trimmed) {
|
|
31
|
-
throw
|
|
32
|
-
cause: null,
|
|
33
|
-
code: "validation"
|
|
34
|
-
});
|
|
26
|
+
throw validationError("tableRef 不能为空");
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
const parts = trimmed.split(/\s+/);
|
|
38
30
|
if (parts.length > 2) {
|
|
39
|
-
throw
|
|
40
|
-
cause: null,
|
|
41
|
-
code: "validation"
|
|
42
|
-
});
|
|
31
|
+
throw validationError(`不支持的表引用格式(包含过多片段)。请使用最简形式:table 或 table alias 或 schema.table 或 schema.table alias (tableRef: ${trimmed})`);
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
const nameSegments = parts[0].split(".");
|
|
46
35
|
if (nameSegments.length > 2) {
|
|
47
|
-
throw
|
|
48
|
-
cause: null,
|
|
49
|
-
code: "validation"
|
|
50
|
-
});
|
|
36
|
+
throw validationError(`不支持的表引用格式(schema 层级过深) (tableRef: ${trimmed})`);
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
if (nameSegments.length === 2) {
|
|
54
40
|
if (!nameSegments[0]) {
|
|
55
|
-
throw
|
|
56
|
-
cause: null,
|
|
57
|
-
code: "validation"
|
|
58
|
-
});
|
|
41
|
+
throw validationError(`tableRef 解析失败:schema 为空 (tableRef: ${trimmed})`);
|
|
59
42
|
}
|
|
60
43
|
if (!nameSegments[1]) {
|
|
61
|
-
throw
|
|
62
|
-
cause: null,
|
|
63
|
-
code: "validation"
|
|
64
|
-
});
|
|
44
|
+
throw validationError(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`);
|
|
65
45
|
}
|
|
66
46
|
|
|
67
47
|
return {
|
|
@@ -72,10 +52,7 @@ export function parseTableRef(tableRef) {
|
|
|
72
52
|
}
|
|
73
53
|
|
|
74
54
|
if (!nameSegments[0]) {
|
|
75
|
-
throw
|
|
76
|
-
cause: null,
|
|
77
|
-
code: "validation"
|
|
78
|
-
});
|
|
55
|
+
throw validationError(`tableRef 解析失败:table 为空 (tableRef: ${trimmed})`);
|
|
79
56
|
}
|
|
80
57
|
|
|
81
58
|
return {
|
|
@@ -92,7 +69,7 @@ function isQuotedIdentPaired(value) {
|
|
|
92
69
|
}
|
|
93
70
|
|
|
94
71
|
const first = trimmed[0];
|
|
95
|
-
const last = trimmed
|
|
72
|
+
const last = trimmed.at(-1);
|
|
96
73
|
return (first === "`" && last === "`") || (first === '"' && last === '"');
|
|
97
74
|
}
|
|
98
75
|
|
|
@@ -124,10 +101,7 @@ export function parseFieldAliasParts(field) {
|
|
|
124
101
|
}
|
|
125
102
|
|
|
126
103
|
if (/\s+AS\s+/i.test(trimmed)) {
|
|
127
|
-
throw
|
|
128
|
-
cause: null,
|
|
129
|
-
code: "validation"
|
|
130
|
-
});
|
|
104
|
+
throw validationError(`字段别名不支持 AS,请使用单个空格分隔字段与别名 (field: ${trimmed})`);
|
|
131
105
|
}
|
|
132
106
|
|
|
133
107
|
if (!/\s/.test(trimmed)) {
|
|
@@ -135,10 +109,7 @@ export function parseFieldAliasParts(field) {
|
|
|
135
109
|
}
|
|
136
110
|
|
|
137
111
|
if (!/^[^\s]+ [^\s]+$/.test(trimmed)) {
|
|
138
|
-
throw
|
|
139
|
-
cause: null,
|
|
140
|
-
code: "validation"
|
|
141
|
-
});
|
|
112
|
+
throw validationError(`字段别名必须使用单个空格分隔字段与别名 (field: ${trimmed})`);
|
|
142
113
|
}
|
|
143
114
|
|
|
144
115
|
const parts = trimmed.split(" ");
|
|
@@ -399,10 +370,7 @@ export async function fieldsToSnake(table, classified, getTableColumns) {
|
|
|
399
370
|
const excludeSnakeFields = classified.fields.map((field) => snakeCase(field));
|
|
400
371
|
const resultFields = allColumns.filter((column) => !excludeSnakeFields.includes(column));
|
|
401
372
|
if (resultFields.length === 0) {
|
|
402
|
-
throw
|
|
403
|
-
cause: null,
|
|
404
|
-
code: "validation"
|
|
405
|
-
});
|
|
373
|
+
throw validationError(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`);
|
|
406
374
|
}
|
|
407
375
|
return resultFields;
|
|
408
376
|
}
|
|
@@ -415,7 +383,7 @@ export function normalizeQualifierField(field) {
|
|
|
415
383
|
return snakeCase(field);
|
|
416
384
|
}
|
|
417
385
|
|
|
418
|
-
const fieldName = parts
|
|
386
|
+
const fieldName = parts.at(-1).trim();
|
|
419
387
|
const qualifier = parts
|
|
420
388
|
.slice(0, parts.length - 1)
|
|
421
389
|
.map((item) => snakeCase(item.trim()))
|
|
@@ -469,10 +437,7 @@ function buildFlatWhereKey(field, operator, orGroup) {
|
|
|
469
437
|
|
|
470
438
|
export function parseFlatWhereKey(key) {
|
|
471
439
|
if (!isNonEmptyString(key)) {
|
|
472
|
-
throw
|
|
473
|
-
cause: null,
|
|
474
|
-
code: "validation"
|
|
475
|
-
});
|
|
440
|
+
throw validationError("where 字段名不能为空");
|
|
476
441
|
}
|
|
477
442
|
|
|
478
443
|
assertNoExprField(key);
|
|
@@ -481,24 +446,18 @@ export function parseFlatWhereKey(key) {
|
|
|
481
446
|
const field = rawParts[0];
|
|
482
447
|
|
|
483
448
|
if (!isNonEmptyString(field)) {
|
|
484
|
-
throw
|
|
485
|
-
cause: null,
|
|
486
|
-
code: "validation"
|
|
487
|
-
});
|
|
449
|
+
throw validationError(`where 字段名不能为空 (field: ${key})`);
|
|
488
450
|
}
|
|
489
451
|
|
|
490
452
|
const suffixParts = rawParts.slice(1);
|
|
491
453
|
let orGroup = null;
|
|
492
454
|
|
|
493
|
-
if (suffixParts.length > 0 && isFlatWhereOrGroupToken(suffixParts
|
|
455
|
+
if (suffixParts.length > 0 && isFlatWhereOrGroupToken(suffixParts.at(-1))) {
|
|
494
456
|
orGroup = suffixParts.pop();
|
|
495
457
|
}
|
|
496
458
|
|
|
497
459
|
if (suffixParts.length > 1) {
|
|
498
|
-
throw
|
|
499
|
-
cause: null,
|
|
500
|
-
code: "validation"
|
|
501
|
-
});
|
|
460
|
+
throw validationError(`where 字段后缀格式无效,请使用 field、field$op、field$or、field$op$orN (field: ${key})`);
|
|
502
461
|
}
|
|
503
462
|
|
|
504
463
|
if (suffixParts.length === 0) {
|
|
@@ -512,10 +471,7 @@ export function parseFlatWhereKey(key) {
|
|
|
512
471
|
const operatorToken = suffixParts[0];
|
|
513
472
|
|
|
514
473
|
if (!flatWhereOperatorTokens.has(operatorToken)) {
|
|
515
|
-
throw
|
|
516
|
-
cause: null,
|
|
517
|
-
code: "validation"
|
|
518
|
-
});
|
|
474
|
+
throw validationError(`where 操作符无效,请使用受支持的扁平操作符 (field: ${key}, operator: ${operatorToken})`);
|
|
519
475
|
}
|
|
520
476
|
|
|
521
477
|
return {
|
package/lib/logger.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createWriteStream, existsSync, mkdirSync, rmSync, statSync } from "node
|
|
|
6
6
|
import { join as nodePathJoin, resolve as nodePathResolve } from "node:path";
|
|
7
7
|
|
|
8
8
|
import { formatYmdHms } from "../utils/datetime.js";
|
|
9
|
+
import { createError } from "../utils/error.js";
|
|
9
10
|
import { isPlainObject } from "../utils/is.js";
|
|
10
11
|
import { buildSensitiveKeyMatcher, sanitizeLogObject } from "../utils/loggerUtils.js";
|
|
11
12
|
import { normalizePositiveInt } from "../utils/util.js";
|
|
@@ -32,7 +33,9 @@ let errorFileSink = null;
|
|
|
32
33
|
let config = {
|
|
33
34
|
debug: 0,
|
|
34
35
|
dir: "",
|
|
35
|
-
runtimeEnv: "production"
|
|
36
|
+
runtimeEnv: "production",
|
|
37
|
+
// bm2 多实例标识(BM2_INSTANCE_ID 优先,BM2_APP_INSTANCE 兜底);无 bm2 环境为空串,文件名不带后缀
|
|
38
|
+
instanceId: ""
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
function safeWriteStderr(msg) {
|
|
@@ -48,6 +51,8 @@ class LogFileSink {
|
|
|
48
51
|
this.streamIndex = 0;
|
|
49
52
|
this.streamSizeBytes = 0;
|
|
50
53
|
this.disabled = false;
|
|
54
|
+
// 最近一次 write 的完成回调(write 回调按提交顺序触发,等待它即等待全部已提交数据排空)
|
|
55
|
+
this.lastWriteDone = null;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
write(line) {
|
|
@@ -55,13 +60,20 @@ class LogFileSink {
|
|
|
55
60
|
|
|
56
61
|
this.ensureStreamReady(line.length);
|
|
57
62
|
if (this.stream) {
|
|
58
|
-
this.stream.write(line);
|
|
59
63
|
this.streamSizeBytes += line.length;
|
|
64
|
+
this.lastWriteDone = new Promise((resolve) => {
|
|
65
|
+
this.stream.write(line, resolve);
|
|
66
|
+
});
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 等待已提交数据排空(不关闭流)
|
|
72
|
+
*/
|
|
63
73
|
async flushNow() {
|
|
64
|
-
|
|
74
|
+
if (this.lastWriteDone) {
|
|
75
|
+
await this.lastWriteDone;
|
|
76
|
+
}
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
async shutdown() {
|
|
@@ -73,8 +85,10 @@ class LogFileSink {
|
|
|
73
85
|
return nodePathJoin(config.dir, "dev.log");
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
// bm2 多实例时按实例分文件,避免多进程写同一文件(例:app.2026-08-01.0.1.log)
|
|
76
89
|
const suffix = index > 0 ? `.${index}` : "";
|
|
77
|
-
const
|
|
90
|
+
const instanceSuffix = config.instanceId ? `.${config.instanceId}` : "";
|
|
91
|
+
const filename = this.prefix === "app" ? `${date}${instanceSuffix}${suffix}.log` : `${this.prefix}.${date}${instanceSuffix}${suffix}.log`;
|
|
78
92
|
return nodePathJoin(config.dir, filename);
|
|
79
93
|
}
|
|
80
94
|
|
|
@@ -155,7 +169,7 @@ class LogFileSink {
|
|
|
155
169
|
}
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
|
|
172
|
+
async function flush() {
|
|
159
173
|
if (mockInstance) return;
|
|
160
174
|
|
|
161
175
|
for (const sink of [appFileSink, errorFileSink]) {
|
|
@@ -165,7 +179,7 @@ export async function flush() {
|
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
|
|
168
|
-
|
|
182
|
+
async function shutdown() {
|
|
169
183
|
if (mockInstance) return;
|
|
170
184
|
|
|
171
185
|
const currentAppFileSink = appFileSink;
|
|
@@ -190,7 +204,7 @@ function getRuntimeLogDir() {
|
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
function prepareLogDir(runtimeEnv, shouldClearDev = false) {
|
|
193
|
-
const logDir = getRuntimeLogDir(
|
|
207
|
+
const logDir = getRuntimeLogDir();
|
|
194
208
|
const devLogPath = nodePathResolve(logDir, "dev.log");
|
|
195
209
|
|
|
196
210
|
try {
|
|
@@ -201,10 +215,7 @@ function prepareLogDir(runtimeEnv, shouldClearDev = false) {
|
|
|
201
215
|
rmSync(devLogPath, { force: true });
|
|
202
216
|
}
|
|
203
217
|
} catch (error) {
|
|
204
|
-
throw
|
|
205
|
-
cause: error,
|
|
206
|
-
code: "runtime"
|
|
207
|
-
});
|
|
218
|
+
throw createError(`创建日志目录失败: ${logDir}. ${error?.message || error}`, { cause: error, code: "runtime" });
|
|
208
219
|
}
|
|
209
220
|
|
|
210
221
|
return logDir;
|
|
@@ -217,19 +228,21 @@ function ensureLogDirExists() {
|
|
|
217
228
|
/**
|
|
218
229
|
* 配置日志
|
|
219
230
|
*/
|
|
220
|
-
|
|
231
|
+
function configure(cfg) {
|
|
221
232
|
shutdown();
|
|
222
233
|
|
|
223
|
-
config =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
config = {
|
|
235
|
+
debug: 0,
|
|
236
|
+
dir: "",
|
|
237
|
+
runtimeEnv: "production",
|
|
238
|
+
instanceId: "",
|
|
239
|
+
...cfg
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// 实例 ID 来自运行环境(bm2 注入),不允许配置覆盖
|
|
243
|
+
config.instanceId = Bun.env.BM2_INSTANCE_ID || Bun.env.BM2_APP_INSTANCE || "";
|
|
231
244
|
|
|
232
|
-
config.dir = getRuntimeLogDir(
|
|
245
|
+
config.dir = getRuntimeLogDir();
|
|
233
246
|
prepareLogDir(config.runtimeEnv, true);
|
|
234
247
|
|
|
235
248
|
appFileSink = null;
|
|
@@ -247,7 +260,7 @@ export function configure(cfg) {
|
|
|
247
260
|
* 设置 Mock Logger(用于测试)
|
|
248
261
|
* @param mock - Mock Logger 实例(形如 {info/warn/error/debug}),传 null 清除 mock
|
|
249
262
|
*/
|
|
250
|
-
|
|
263
|
+
function setMockLogger(mock) {
|
|
251
264
|
mockInstance = mock;
|
|
252
265
|
}
|
|
253
266
|
|
|
@@ -379,10 +392,20 @@ function buildLogMethod(level) {
|
|
|
379
392
|
|
|
380
393
|
/**
|
|
381
394
|
* 日志实例(延迟初始化)
|
|
395
|
+
* 方法签名约定:
|
|
396
|
+
* - info/warn/debug: (msg, data) —— msg 为字符串或直接传对象(对象时作为完整 record)
|
|
397
|
+
* - error: (msg, err, data) —— err 为 Error 或任意附加信息,data 为可选附加数据
|
|
398
|
+
* - 输出统一为 JSONL 单行:{ level, time, pid, msg, ...record }
|
|
382
399
|
*/
|
|
383
400
|
export const Logger = {
|
|
384
401
|
info: buildLogMethod("info"),
|
|
385
402
|
warn: buildLogMethod("warn"),
|
|
403
|
+
/**
|
|
404
|
+
* 错误日志
|
|
405
|
+
* @param msg - 错误说明(字符串;传对象时作为完整 record)
|
|
406
|
+
* @param err - 错误对象(Error 实例会被序列化并附带 chain 摘要)或任意附加信息
|
|
407
|
+
* @param data - 可选附加数据
|
|
408
|
+
*/
|
|
386
409
|
error: function (msg, err, data) {
|
|
387
410
|
if (isPlainObject(msg)) {
|
|
388
411
|
logWrite("error", msg);
|