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/utils/is.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
import { join } from "pathe";
|
|
4
|
-
|
|
5
|
-
import { DECIMAL_KIND_TYPES, ENUM_KIND_TYPES, FLOAT_KIND_TYPES, INT_KIND_TYPES, JSON_KIND_TYPES, STRING_KIND_TYPES, TEXT_KIND_TYPES } from "../configs/constConfig.js";
|
|
6
|
-
|
|
7
1
|
/**
|
|
8
|
-
* 判断值是否为 plain object(原型为 Object.prototype 或 null
|
|
2
|
+
* 判断值是否为 plain object(原型为 Object.prototype 或 null)。
|
|
9
3
|
*/
|
|
10
4
|
export function isPlainObject(value) {
|
|
11
5
|
if (typeof value !== "object" || value === null) {
|
|
@@ -26,61 +20,6 @@ export function isJsonPrimitive(value) {
|
|
|
26
20
|
return typeof value === "string" || isNumber(value) || isBoolean(value);
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
/**
|
|
30
|
-
* 判断值是否为 JSON 对象(plain object,且非数组/非 Date)。
|
|
31
|
-
*/
|
|
32
|
-
export function isJsonObject(value) {
|
|
33
|
-
if (!value || typeof value !== "object") {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
if (value instanceof Date) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
if (Array.isArray(value)) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 判断值是否为 JSON 结构(数组或对象)。
|
|
47
|
-
*/
|
|
48
|
-
export function isJsonStructure(value) {
|
|
49
|
-
return Array.isArray(value) || isJsonObject(value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 判断值是否为可 JSON 序列化结构(递归)。
|
|
54
|
-
*/
|
|
55
|
-
export function isJsonValue(value) {
|
|
56
|
-
if (isJsonPrimitive(value)) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (Array.isArray(value)) {
|
|
61
|
-
for (const item of value) {
|
|
62
|
-
if (!isJsonValue(item)) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (isJsonObject(value)) {
|
|
70
|
-
for (const v of Object.values(value)) {
|
|
71
|
-
if (v === undefined) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (!isJsonValue(v)) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
23
|
/**
|
|
85
24
|
* 判断值是否为有限数值。
|
|
86
25
|
*/
|
|
@@ -173,12 +112,12 @@ export function isNoTrimStringAllowEmpty(value) {
|
|
|
173
112
|
return true;
|
|
174
113
|
}
|
|
175
114
|
|
|
176
|
-
const firstCharCode = value.
|
|
115
|
+
const firstCharCode = value.codePointAt(0);
|
|
177
116
|
if (isWhitespaceCharCode(firstCharCode)) {
|
|
178
117
|
return false;
|
|
179
118
|
}
|
|
180
119
|
|
|
181
|
-
const lastCharCode = value.
|
|
120
|
+
const lastCharCode = value.codePointAt(value.length - 1);
|
|
182
121
|
if (isWhitespaceCharCode(lastCharCode)) {
|
|
183
122
|
return false;
|
|
184
123
|
}
|
|
@@ -290,131 +229,15 @@ export function isRegexInput(input) {
|
|
|
290
229
|
return isRegexLiteral(trimmed) || trimmed.startsWith("@");
|
|
291
230
|
}
|
|
292
231
|
|
|
293
|
-
/**
|
|
294
|
-
* 判断输入是否为枚举规则(首字符非 / 且包含 |)。
|
|
295
|
-
*/
|
|
296
|
-
export function isEnumInput(input) {
|
|
297
|
-
const trimmed = String(input || "").trim();
|
|
298
|
-
if (trimmed.length === 0) {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
return !trimmed.startsWith("/") && trimmed.includes("|");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* 判断输入是否为规则别名(@ 开头)。
|
|
306
|
-
*/
|
|
307
|
-
export function isAliasInput(input) {
|
|
308
|
-
return String(input || "")
|
|
309
|
-
.trim()
|
|
310
|
-
.startsWith("@");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* 激进空值判断(项目约定)。
|
|
315
|
-
*/
|
|
316
|
-
export function isEmpty(value) {
|
|
317
|
-
if (isNullable(value)) {
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (typeof value === "string") {
|
|
322
|
-
return value.trim().length === 0;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (isNumber(value)) {
|
|
326
|
-
return value === 0 || Number.isNaN(value);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (isBoolean(value)) {
|
|
330
|
-
return value === false;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (Array.isArray(value)) {
|
|
334
|
-
return value.length === 0;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (value instanceof Map || value instanceof Set) {
|
|
338
|
-
return value.size === 0;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (isPlainObject(value)) {
|
|
342
|
-
return Object.keys(value).length === 0;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
232
|
/**
|
|
349
233
|
* 判断当前进程是否为主进程。
|
|
350
|
-
*
|
|
234
|
+
* bm2 的实例 0 是主进程;未由 bm2 启动时视为主进程。
|
|
351
235
|
*/
|
|
352
236
|
export function isPrimaryProcess() {
|
|
353
|
-
const instance = Bun.env.
|
|
237
|
+
const instance = Bun.env.BM2_APP_INSTANCE;
|
|
354
238
|
return instance === "0" || instance === 0 || instance === undefined;
|
|
355
239
|
}
|
|
356
240
|
|
|
357
|
-
export const isDirentDirectory = (parentDir, entry) => {
|
|
358
|
-
if (entry.isDirectory()) {
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 兼容 Windows 下的 junction / workspace link:Dirent.isDirectory() 可能为 false,但它实际指向目录。
|
|
363
|
-
if (!entry.isSymbolicLink()) {
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
const stats = statSync(join(parentDir, entry.name));
|
|
369
|
-
return stats.isDirectory();
|
|
370
|
-
} catch {
|
|
371
|
-
return false;
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* 数据库类型判定
|
|
377
|
-
*/
|
|
378
|
-
export function isStringDbType(dbType) {
|
|
379
|
-
return STRING_KIND_TYPES.includes(dbType);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export function isTextDbType(dbType) {
|
|
383
|
-
return TEXT_KIND_TYPES.includes(dbType);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function isIntDbType(dbType) {
|
|
387
|
-
return INT_KIND_TYPES.includes(dbType);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export function isDecimalDbType(dbType) {
|
|
391
|
-
return DECIMAL_KIND_TYPES.includes(dbType);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export function isFloatDbType(dbType) {
|
|
395
|
-
return FLOAT_KIND_TYPES.includes(dbType);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export function isJsonDbType(dbType) {
|
|
399
|
-
return JSON_KIND_TYPES.includes(dbType);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export function isEnumDbType(dbType) {
|
|
403
|
-
return ENUM_KIND_TYPES.includes(dbType);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export function isNumericDbType(dbType) {
|
|
407
|
-
return INT_KIND_TYPES.includes(dbType) || DECIMAL_KIND_TYPES.includes(dbType) || FLOAT_KIND_TYPES.includes(dbType);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* SQL Value 判定
|
|
412
|
-
*/
|
|
413
|
-
export function isSqlValue(value) {
|
|
414
|
-
if (value instanceof Date) return true;
|
|
415
|
-
return isJsonValue(value);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
241
|
/**
|
|
419
242
|
* 日志敏感键判定
|
|
420
243
|
*/
|
package/utils/loggerUtils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorChainSummary } from "./error.js";
|
|
1
2
|
import { isBoolean, isNullable, isNumber, isPlainObject, isSensitiveKey, isString } from "./is.js";
|
|
2
3
|
|
|
3
4
|
export function buildSensitiveKeyMatcher(options) {
|
|
@@ -34,7 +35,8 @@ export function buildSensitiveKeyMatcher(options) {
|
|
|
34
35
|
function sanitizeErrorValue(err, options) {
|
|
35
36
|
const out = {
|
|
36
37
|
name: err.name || "Error",
|
|
37
|
-
message: err.message || ""
|
|
38
|
+
message: err.message || "",
|
|
39
|
+
chain: errorChainSummary(err)
|
|
38
40
|
};
|
|
39
41
|
const state = { nodes: 0 };
|
|
40
42
|
const visited = new WeakSet();
|
package/utils/response.js
CHANGED
|
@@ -13,11 +13,16 @@ import { isString } from "./is.js";
|
|
|
13
13
|
* @returns Response 对象
|
|
14
14
|
*/
|
|
15
15
|
export function ErrorResponse(ctx, msg, code = 1, data = null, detail = null, reason = null) {
|
|
16
|
-
//
|
|
16
|
+
// 记录拦截日志(带请求上下文,便于排查被拦截请求)
|
|
17
17
|
if (ctx.requestId) {
|
|
18
|
-
// requestId/apiPath/user/duration 等字段由调用方显式写入日志上下文,避免在 msg 中重复拼接
|
|
19
18
|
Logger.info("请求已拦截", {
|
|
20
19
|
event: "request_blocked",
|
|
20
|
+
requestId: ctx.requestId,
|
|
21
|
+
apiPath: ctx.apiPath,
|
|
22
|
+
apiName: ctx.apiName,
|
|
23
|
+
ip: ctx.ip,
|
|
24
|
+
userId: ctx.userId,
|
|
25
|
+
roleCode: ctx.roleCode,
|
|
21
26
|
msg: msg,
|
|
22
27
|
reason: reason,
|
|
23
28
|
code: code,
|
|
@@ -62,8 +67,8 @@ export function FinalResponse(ctx) {
|
|
|
62
67
|
msg: result
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
|
-
// 如果是对象,自动补充 code: 0
|
|
66
|
-
else if (result && typeof result === "object") {
|
|
70
|
+
// 如果是对象,自动补充 code: 0(数组保持原样)
|
|
71
|
+
else if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
67
72
|
if (!("code" in result)) {
|
|
68
73
|
const merged = { code: 0 };
|
|
69
74
|
for (const key of Object.keys(result)) {
|
|
@@ -87,11 +92,10 @@ export function FinalResponse(ctx) {
|
|
|
87
92
|
return new Response(jsonString, {
|
|
88
93
|
headers: headers
|
|
89
94
|
});
|
|
90
|
-
} else {
|
|
91
|
-
return Response.json(result, {
|
|
92
|
-
headers: ctx.corsHeaders
|
|
93
|
-
});
|
|
94
95
|
}
|
|
96
|
+
return Response.json(result, {
|
|
97
|
+
headers: ctx.corsHeaders
|
|
98
|
+
});
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// 3. 默认响应:没有生成响应
|
package/utils/scanFiles.js
CHANGED
|
@@ -2,58 +2,10 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import { relative, normalize, parse, join } from "pathe";
|
|
4
4
|
|
|
5
|
+
import { createError } from "./error.js";
|
|
5
6
|
import { importDefault } from "./importDefault.js";
|
|
6
7
|
import { isPlainObject } from "./is.js";
|
|
7
8
|
|
|
8
|
-
// [ hooks ]-112 {
|
|
9
|
-
// source: "core",
|
|
10
|
-
// type: "hook",
|
|
11
|
-
// filePath: "D:/codes/befly/packages/core/hooks/auth.js",
|
|
12
|
-
// relativePath: "auth",
|
|
13
|
-
// fileName: "auth",
|
|
14
|
-
// apiPath: "/api/core/auth",
|
|
15
|
-
// name: "auth",
|
|
16
|
-
// order: 1,
|
|
17
|
-
// handler: [AsyncFunction: handler],
|
|
18
|
-
// }
|
|
19
|
-
// 🔥[ plugins ]-112 {
|
|
20
|
-
// source: "core",
|
|
21
|
-
// type: "plugin",
|
|
22
|
-
// filePath: "D:/codes/befly/packages/core/plugins/cache.js",
|
|
23
|
-
// relativePath: "cache",
|
|
24
|
-
// fileName: "cache",
|
|
25
|
-
// apiPath: "/api/core/cache",
|
|
26
|
-
// name: "cache",
|
|
27
|
-
// order: 1,
|
|
28
|
-
// handler: [AsyncFunction: handler],
|
|
29
|
-
// }
|
|
30
|
-
// 🔥[ tables ]-112 {
|
|
31
|
-
// source: "core",
|
|
32
|
-
// type: "table",
|
|
33
|
-
// filePath: "D:/codes/befly/packages/core/tables/admin.json",
|
|
34
|
-
// relativePath: "admin",
|
|
35
|
-
// fileName: "admin",
|
|
36
|
-
// apiPath: "/api/core/admin",
|
|
37
|
-
// "lastLoginTime", "lastLoginIp"
|
|
38
|
-
// ],
|
|
39
|
-
// name: "admin",
|
|
40
|
-
// }
|
|
41
|
-
// 🔥[ apis ]-112 {
|
|
42
|
-
// source: "core",
|
|
43
|
-
// type: "api",
|
|
44
|
-
// filePath: "D:/codes/befly/packages/core/apis/sysConfig/all.js",
|
|
45
|
-
// relativePath: "sysConfig/all",
|
|
46
|
-
// fileName: "all",
|
|
47
|
-
// apiPath: "/api/core/sysConfig/all",
|
|
48
|
-
// name: "all",
|
|
49
|
-
// method: "POST",
|
|
50
|
-
// body: "none",
|
|
51
|
-
// auth: true,
|
|
52
|
-
// fields: {},
|
|
53
|
-
// required: [],
|
|
54
|
-
// handler: [AsyncFunction: handler],
|
|
55
|
-
// }
|
|
56
|
-
|
|
57
9
|
/**
|
|
58
10
|
* 扫描指定目录下的文件
|
|
59
11
|
* @param dir 目录路径
|
|
@@ -116,9 +68,6 @@ export async function scanFiles(dir, source, type, pattern) {
|
|
|
116
68
|
}
|
|
117
69
|
return results;
|
|
118
70
|
} catch (error) {
|
|
119
|
-
throw
|
|
120
|
-
cause: error,
|
|
121
|
-
code: "runtime"
|
|
122
|
-
});
|
|
71
|
+
throw createError(`扫描失败: source=${source} type=${type} dir=${dir} pattern=${pattern}`, { cause: error, code: "runtime" });
|
|
123
72
|
}
|
|
124
73
|
}
|
package/utils/scanSources.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
coreApiDir,
|
|
10
10
|
appApiDir
|
|
11
11
|
} from "../paths.js";
|
|
12
|
+
import { createError } from "./error.js";
|
|
12
13
|
import { scanFiles } from "./scanFiles.js";
|
|
13
14
|
|
|
14
15
|
export const scanSources = async () => {
|
|
@@ -54,6 +55,14 @@ export const scanSources = async () => {
|
|
|
54
55
|
}
|
|
55
56
|
const allAppApis = await scanFiles(appApiDir, "app", "api", "**/*.js");
|
|
56
57
|
for (const item of allAppApis) {
|
|
58
|
+
// 禁止 app 接口目录下的 core 一级目录:会与框架内置 /api/core 命名空间冲突,从源头保证 apiPath 唯一
|
|
59
|
+
if (item.relativePath.split("/")[0] === "core") {
|
|
60
|
+
throw createError(`app 接口目录下禁止存在 core 一级目录:${item.filePath}`, {
|
|
61
|
+
code: "policy",
|
|
62
|
+
subsystem: "scan",
|
|
63
|
+
operation: "scanSources"
|
|
64
|
+
});
|
|
65
|
+
}
|
|
57
66
|
apis.push(item);
|
|
58
67
|
}
|
|
59
68
|
|
package/utils/util.js
CHANGED
|
@@ -4,7 +4,26 @@
|
|
|
4
4
|
* - 仅在 packages/core 内部使用;core 对外不承诺这些路径导出。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { createError } from "./error.js";
|
|
8
|
+
import { isPlainObject } from "./is.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 通用等待工具:轮询直到条件满足或超时(while + sleep 实现,无定时器句柄,无泄漏风险)
|
|
12
|
+
* @param {string} label - 等待说明(用于超时错误信息)
|
|
13
|
+
* @param {Function} check - 异步条件函数,返回 true 表示就绪
|
|
14
|
+
* @param {number} [intervalMs=200] - 轮询间隔(毫秒)
|
|
15
|
+
* @param {number} [timeoutMs=60000] - 超时时间(毫秒)
|
|
16
|
+
*/
|
|
17
|
+
export async function waitFor({ label, check, intervalMs = 200, timeoutMs = 60000 }) {
|
|
18
|
+
const deadline = Date.now() + timeoutMs;
|
|
19
|
+
while (Date.now() < deadline) {
|
|
20
|
+
if (await check()) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await Bun.sleep(intervalMs);
|
|
24
|
+
}
|
|
25
|
+
throw createError(`等待${label}超时`, { code: "runtime", subsystem: "start", operation: "waitFor" });
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
/**
|
|
10
29
|
* 判断 bigint 是否可安全转换为 number。
|
|
@@ -32,43 +51,6 @@ export function canConvertToNumber(value) {
|
|
|
32
51
|
return asNumber;
|
|
33
52
|
}
|
|
34
53
|
|
|
35
|
-
/**
|
|
36
|
-
* 安全格式化值用于日志预览(截断长文本,避免抛错)。
|
|
37
|
-
*/
|
|
38
|
-
export function formatValuePreview(value) {
|
|
39
|
-
if (value === null) {
|
|
40
|
-
return "null";
|
|
41
|
-
}
|
|
42
|
-
if (value === undefined) {
|
|
43
|
-
return "undefined";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (typeof value === "string") {
|
|
47
|
-
const s = value.length > 80 ? `${value.slice(0, 80)}...` : value;
|
|
48
|
-
return JSON.stringify(s);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (isNumber(value) || isBoolean(value) || typeof value === "bigint") {
|
|
52
|
-
return String(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (typeof value === "function") {
|
|
56
|
-
return "[Function]";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const s = JSON.stringify(value);
|
|
61
|
-
if (typeof s === "string") {
|
|
62
|
-
return s.length > 120 ? `${s.slice(0, 120)}...` : s;
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// ignore
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const s = String(value);
|
|
69
|
-
return s.length > 120 ? `${s.slice(0, 120)}...` : s;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
54
|
/**
|
|
73
55
|
* 获取值的类型标识(数组/对象等做细分)。
|
|
74
56
|
*/
|
|
@@ -88,16 +70,6 @@ export function getTypeTag(value) {
|
|
|
88
70
|
return "object";
|
|
89
71
|
}
|
|
90
72
|
|
|
91
|
-
// 正则字符转义列表
|
|
92
|
-
const REGEXP_SPECIAL = /[\^$.*+?()[\]{}|]/g;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 转义字符串中的正则特殊字符。
|
|
96
|
-
*/
|
|
97
|
-
export function escapeRegExp(input) {
|
|
98
|
-
return String(input).replace(REGEXP_SPECIAL, "\\$&");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
73
|
/**
|
|
102
74
|
* 归一化正整数:非法值回退,超出范围裁剪。
|
|
103
75
|
*/
|
|
@@ -109,24 +81,6 @@ export function normalizePositiveInt(value, fallback, min, max) {
|
|
|
109
81
|
return v;
|
|
110
82
|
}
|
|
111
83
|
|
|
112
|
-
/**
|
|
113
|
-
* 遍历 plain object 的自有字段。
|
|
114
|
-
*/
|
|
115
|
-
export function forOwn(obj, iteratee) {
|
|
116
|
-
if (typeof iteratee !== "function") {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!isPlainObject(obj)) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const record = obj;
|
|
125
|
-
for (const key of Object.keys(record)) {
|
|
126
|
-
iteratee(record[key], key);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
84
|
// 将字符串拆分为词段(用于 camelCase/snakeCase)
|
|
131
85
|
function toWordParts(input) {
|
|
132
86
|
const normalized = String(input)
|
|
@@ -170,19 +124,6 @@ export function camelCase(input) {
|
|
|
170
124
|
return [first].concat(rest).join("");
|
|
171
125
|
}
|
|
172
126
|
|
|
173
|
-
/**
|
|
174
|
-
* 字段名专用小驼峰转换。
|
|
175
|
-
* - 只要原始值包含下划线,就直接保留原字段名。
|
|
176
|
-
* - 其余场景继续走普通小驼峰转换。
|
|
177
|
-
*/
|
|
178
|
-
export function camelCaseKeepUnderscore(input) {
|
|
179
|
-
if (input.includes("_")) {
|
|
180
|
-
return input;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return camelCase(input);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
127
|
/**
|
|
187
128
|
* 把字符串转为 snake_case。
|
|
188
129
|
* - 主要用于表名/字段名(例如 userId -> user_id)。
|
|
@@ -196,22 +137,6 @@ export function snakeCase(input) {
|
|
|
196
137
|
return parts.map((p) => p.toLowerCase()).join("_");
|
|
197
138
|
}
|
|
198
139
|
|
|
199
|
-
/**
|
|
200
|
-
* 对象字段名转小驼峰
|
|
201
|
-
* @param obj - 源对象
|
|
202
|
-
* @returns 字段名转为小驼峰格式的新对象
|
|
203
|
-
*/
|
|
204
|
-
export const keysToCamel = (obj) => {
|
|
205
|
-
if (!obj || !isPlainObject(obj)) return obj;
|
|
206
|
-
|
|
207
|
-
const result = {};
|
|
208
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
209
|
-
const camelKey = camelCase(key);
|
|
210
|
-
result[camelKey] = value;
|
|
211
|
-
}
|
|
212
|
-
return result;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
140
|
/**
|
|
216
141
|
* 对象字段名转下划线
|
|
217
142
|
* @param obj - 源对象
|
|
@@ -229,134 +154,12 @@ export const keysToSnake = (obj) => {
|
|
|
229
154
|
};
|
|
230
155
|
|
|
231
156
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*/
|
|
236
|
-
export const arrayKeysToCamel = (arr) => {
|
|
237
|
-
if (!arr || !Array.isArray(arr)) return arr;
|
|
238
|
-
return arr.map((item) => keysToCamel(item));
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* 根据点路径读取对象字段(不存在则返回 undefined)。
|
|
243
|
-
*/
|
|
244
|
-
export function getByPath(obj, path) {
|
|
245
|
-
if (!path) {
|
|
246
|
-
return obj;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const parts = path.split(".");
|
|
250
|
-
let cur = obj;
|
|
251
|
-
|
|
252
|
-
for (const part of parts) {
|
|
253
|
-
if (isNullable(cur)) {
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
|
-
if (typeof cur !== "object") {
|
|
257
|
-
return undefined;
|
|
258
|
-
}
|
|
259
|
-
const record = cur;
|
|
260
|
-
cur = record[part];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return cur;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* 根据点路径写入对象字段(中间层自动补空对象)。
|
|
268
|
-
*/
|
|
269
|
-
export function setByPath(target, path, value) {
|
|
270
|
-
const parts = path.split(".");
|
|
271
|
-
// 避免无效 path(如 a..b)导致部分写入
|
|
272
|
-
for (const part of parts) {
|
|
273
|
-
if (!part) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
let cur = target;
|
|
278
|
-
|
|
279
|
-
for (let i = 0; i < parts.length; i++) {
|
|
280
|
-
const key = parts[i];
|
|
281
|
-
if (!key) {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const isLast = i === parts.length - 1;
|
|
286
|
-
if (isLast) {
|
|
287
|
-
cur[key] = value;
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const nextVal = cur[key];
|
|
292
|
-
if (!isPlainObject(nextVal)) {
|
|
293
|
-
cur[key] = {};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
cur = cur[key];
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* 生成移除指定字段后的浅拷贝。
|
|
302
|
-
*/
|
|
303
|
-
export function omit(obj, keys) {
|
|
304
|
-
if (!isPlainObject(obj)) {
|
|
305
|
-
return {};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const keySet = new Set(Array.isArray(keys) ? keys : []);
|
|
309
|
-
const out = {};
|
|
310
|
-
|
|
311
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
312
|
-
if (keySet.has(k)) {
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
out[k] = v;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return out;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* 从对象中挑选指定字段。
|
|
323
|
-
*/
|
|
324
|
-
export const pickFields = (obj, keys) => {
|
|
325
|
-
if (!obj || (!isPlainObject(obj) && !Array.isArray(obj))) {
|
|
326
|
-
return {};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const record = obj;
|
|
330
|
-
const result = {};
|
|
331
|
-
|
|
332
|
-
for (const key of keys) {
|
|
333
|
-
if (key in record) {
|
|
334
|
-
result[key] = record[key];
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return result;
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* 根据 key 生成对象映射。
|
|
157
|
+
* 获取运行模式(开发/正式的统一判断,仅以 NODE_ENV 为准)
|
|
158
|
+
* - 线上部署(bm2 等)注入 NODE_ENV=production
|
|
159
|
+
* - 本地开发未设置 NODE_ENV,默认 "development"
|
|
343
160
|
*/
|
|
344
|
-
export function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (!Array.isArray(items) || typeof getKey !== "function") {
|
|
348
|
-
return out;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
for (const item of items) {
|
|
352
|
-
const key = getKey(item);
|
|
353
|
-
if (typeof key !== "string" || key === "") {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
out[key] = item;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return out;
|
|
161
|
+
export function getRunMode() {
|
|
162
|
+
return Bun.env.NODE_ENV || "development";
|
|
360
163
|
}
|
|
361
164
|
|
|
362
165
|
/**
|
package/checks/hook.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Logger } from "../lib/logger.js";
|
|
2
|
-
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
3
|
-
import { isFiniteNumber, isFunction, isPlainObject, isString } from "../utils/is.js";
|
|
4
|
-
|
|
5
|
-
const hookKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
|
|
6
|
-
|
|
7
|
-
function checkHookItem(hook) {
|
|
8
|
-
const issues = [];
|
|
9
|
-
if (!isPlainObject(hook)) {
|
|
10
|
-
issues.push({ path: [], message: "必须是对象格式" });
|
|
11
|
-
return issues;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
for (const key of Object.keys(hook)) {
|
|
15
|
-
if (!hookKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
|
|
16
|
-
}
|
|
17
|
-
for (const key of ["source", "type", "filePath", "relativePath", "fileName", "apiPath"]) {
|
|
18
|
-
if (!isString(hook[key]) || hook[key].length === 0 || hook[key].trim() !== hook[key]) issues.push({ path: [key], message: "必须是非空且无首尾空格的字符串" });
|
|
19
|
-
}
|
|
20
|
-
if (!isFunction(hook.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
|
|
21
|
-
if (!isFiniteNumber(hook.order)) issues.push({ path: ["order"], message: "必须是有限数字" });
|
|
22
|
-
return issues;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function checkHook(hooks) {
|
|
26
|
-
if (!Array.isArray(hooks)) {
|
|
27
|
-
Logger.warn("钩子校验失败", { errors: formatValidationIssues([{ path: [], message: "必须是数组" }], { item: hooks, itemLabel: "hook" }) }, false);
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let hasError = false;
|
|
32
|
-
for (const hook of hooks) {
|
|
33
|
-
const issues = checkHookItem(hook);
|
|
34
|
-
if (issues.length === 0) continue;
|
|
35
|
-
Logger.warn("钩子校验失败", { errors: formatValidationIssues(issues, { item: hook, itemLabel: "hook" }) }, false);
|
|
36
|
-
hasError = true;
|
|
37
|
-
}
|
|
38
|
-
return hasError;
|
|
39
|
-
}
|