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/sync/api.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const API_TABLE_NAME = "beflyApi";
|
|
1
|
+
import { BEFLY_API_TABLE } from "../configs/constConfig.js";
|
|
2
|
+
import { buildPathSyncLists, serializeAuth } from "./syncUtil.js";
|
|
4
3
|
|
|
5
4
|
const getApiParentPath = (apiPath) => {
|
|
6
5
|
const segments = apiPath
|
|
@@ -19,7 +18,7 @@ const getApiParentPath = (apiPath) => {
|
|
|
19
18
|
|
|
20
19
|
export async function syncApi(ctx, apis) {
|
|
21
20
|
const allDbApis = await ctx.mysql.getAll({
|
|
22
|
-
table:
|
|
21
|
+
table: BEFLY_API_TABLE,
|
|
23
22
|
fields: ["id", "path", "parentPath", "name", "auth", "state"],
|
|
24
23
|
where: { state$gte: 0 }
|
|
25
24
|
});
|
|
@@ -31,7 +30,7 @@ export async function syncApi(ctx, apis) {
|
|
|
31
30
|
path: api.apiPath,
|
|
32
31
|
method: api.method,
|
|
33
32
|
parentPath: getApiParentPath(api.apiPath),
|
|
34
|
-
auth:
|
|
33
|
+
auth: serializeAuth(api.auth)
|
|
35
34
|
});
|
|
36
35
|
}
|
|
37
36
|
|
|
@@ -55,14 +54,14 @@ export async function syncApi(ctx, apis) {
|
|
|
55
54
|
);
|
|
56
55
|
|
|
57
56
|
if (syncLists.updList.length > 0) {
|
|
58
|
-
await ctx.mysql.updBatch(
|
|
57
|
+
await ctx.mysql.updBatch(BEFLY_API_TABLE, syncLists.updList);
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
if (syncLists.insList.length > 0) {
|
|
62
|
-
await ctx.mysql.insBatch(
|
|
61
|
+
await ctx.mysql.insBatch(BEFLY_API_TABLE, syncLists.insList);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
if (syncLists.delIds.length > 0) {
|
|
66
|
-
await ctx.mysql.delForceBatch(
|
|
65
|
+
await ctx.mysql.delForceBatch(BEFLY_API_TABLE, syncLists.delIds);
|
|
67
66
|
}
|
|
68
67
|
}
|
package/sync/cache.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createError } from "../utils/error.js";
|
|
2
|
+
|
|
1
3
|
// 1
|
|
2
4
|
export async function syncCache(ctx) {
|
|
3
5
|
const tasks = [
|
|
@@ -22,7 +24,7 @@ export async function syncCache(ctx) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (failures.length > 0) {
|
|
25
|
-
throw
|
|
27
|
+
throw createError("缓存同步失败", {
|
|
26
28
|
cause: failures[0].error,
|
|
27
29
|
code: "runtime",
|
|
28
30
|
subsystem: "sync",
|
package/sync/dev.js
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
|
+
import { BEFLY_ADMIN_TABLE, BEFLY_API_TABLE, BEFLY_MENU_TABLE, BEFLY_ROLE_TABLE } from "../configs/constConfig.js";
|
|
1
2
|
import { hashPassword } from "../utils/crypto.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const API_TABLE_NAME = "beflyApi";
|
|
6
|
-
const ROLE_TABLE_NAME = "beflyRole";
|
|
7
|
-
const ADMIN_TABLE_NAME = "beflyAdmin";
|
|
3
|
+
import { createError } from "../utils/error.js";
|
|
4
|
+
import { isNonEmptyString, isNumber } from "../utils/is.js";
|
|
5
|
+
import { authAllowsRole } from "./syncUtil.js";
|
|
8
6
|
|
|
9
7
|
export async function syncDev(ctx) {
|
|
8
|
+
// devPassword 为必填项:dev 管理员拥有全部权限,未配置时阻断(不允许无密码/默认密码账号)。
|
|
9
|
+
// 校验放在 try 外,避免被下方统一包装掩盖具体原因。
|
|
10
|
+
if (!isNonEmptyString(ctx.config.devPassword)) {
|
|
11
|
+
throw createError("必须配置 devPassword(至少 6 位)以创建 dev 管理员账号", { code: "policy", subsystem: "sync", operation: "syncDev" });
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
try {
|
|
11
15
|
const allMenus = await ctx.mysql.getAll({
|
|
12
|
-
table:
|
|
16
|
+
table: BEFLY_MENU_TABLE,
|
|
13
17
|
fields: ["path"],
|
|
14
18
|
where: { state$gte: 0 },
|
|
15
19
|
orderBy: ["id#ASC"]
|
|
16
20
|
});
|
|
17
21
|
|
|
18
22
|
const allApis = await ctx.mysql.getAll({
|
|
19
|
-
table:
|
|
23
|
+
table: BEFLY_API_TABLE,
|
|
20
24
|
fields: ["path", "auth"],
|
|
21
25
|
where: { state$gte: 0 },
|
|
22
26
|
orderBy: ["id#ASC"]
|
|
23
27
|
});
|
|
24
28
|
|
|
25
29
|
const devRole = await ctx.mysql.getOne({
|
|
26
|
-
table:
|
|
30
|
+
table: BEFLY_ROLE_TABLE,
|
|
27
31
|
fields: ["id"],
|
|
28
32
|
where: { code: "dev", state$gte: 0 }
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
const devAdmin = await ctx.mysql.getOne({
|
|
32
|
-
table:
|
|
36
|
+
table: BEFLY_ADMIN_TABLE,
|
|
33
37
|
fields: ["id"],
|
|
34
38
|
where: { username: "dev", state$gte: 0 }
|
|
35
39
|
});
|
|
@@ -55,25 +59,20 @@ export async function syncDev(ctx) {
|
|
|
55
59
|
|
|
56
60
|
if (isNumber(devRole.data.id)) {
|
|
57
61
|
await ctx.mysql.updData({
|
|
58
|
-
table:
|
|
62
|
+
table: BEFLY_ROLE_TABLE,
|
|
59
63
|
where: { code: "dev", state$gte: 0 },
|
|
60
|
-
data: {
|
|
61
|
-
name: devRoleData.name,
|
|
62
|
-
description: devRoleData.description,
|
|
63
|
-
menus: devRoleData.menus,
|
|
64
|
-
apis: devRoleData.apis,
|
|
65
|
-
sort: devRoleData.sort,
|
|
66
|
-
state: 1
|
|
67
|
-
}
|
|
64
|
+
data: { ...devRoleData, state: 1 }
|
|
68
65
|
});
|
|
69
66
|
} else {
|
|
70
67
|
await ctx.mysql.insData({
|
|
71
|
-
table:
|
|
68
|
+
table: BEFLY_ROLE_TABLE,
|
|
72
69
|
data: devRoleData
|
|
73
70
|
});
|
|
74
71
|
}
|
|
75
72
|
|
|
73
|
+
// devPassword 为必填项(校验见函数开头),此处直接使用
|
|
76
74
|
const password = await hashPassword(ctx.config.devPassword);
|
|
75
|
+
// 【临时诊断】hashPassword 后连接预检
|
|
77
76
|
|
|
78
77
|
const devAdminData = {
|
|
79
78
|
nickname: "开发者",
|
|
@@ -86,29 +85,14 @@ export async function syncDev(ctx) {
|
|
|
86
85
|
|
|
87
86
|
if (isNumber(devAdmin.data.id)) {
|
|
88
87
|
await ctx.mysql.updData({
|
|
89
|
-
table:
|
|
88
|
+
table: BEFLY_ADMIN_TABLE,
|
|
90
89
|
where: { username: "dev" },
|
|
91
|
-
data: {
|
|
92
|
-
nickname: devAdminData.nickname,
|
|
93
|
-
email: devAdminData.email,
|
|
94
|
-
password: devAdminData.password,
|
|
95
|
-
roleCode: devAdminData.roleCode,
|
|
96
|
-
roleType: devAdminData.roleType,
|
|
97
|
-
state: 1
|
|
98
|
-
}
|
|
90
|
+
data: { ...devAdminData, state: 1 }
|
|
99
91
|
});
|
|
100
92
|
} else {
|
|
101
93
|
await ctx.mysql.insData({
|
|
102
|
-
table:
|
|
103
|
-
data: {
|
|
104
|
-
nickname: devAdminData.nickname,
|
|
105
|
-
email: devAdminData.email,
|
|
106
|
-
username: devAdminData.username,
|
|
107
|
-
password: devAdminData.password,
|
|
108
|
-
roleCode: devAdminData.roleCode,
|
|
109
|
-
roleType: devAdminData.roleType,
|
|
110
|
-
state: 1
|
|
111
|
-
}
|
|
94
|
+
table: BEFLY_ADMIN_TABLE,
|
|
95
|
+
data: { ...devAdminData, state: 1 }
|
|
112
96
|
});
|
|
113
97
|
}
|
|
114
98
|
|
|
@@ -135,7 +119,7 @@ export async function syncDev(ctx) {
|
|
|
135
119
|
|
|
136
120
|
for (const roleConfig of roles) {
|
|
137
121
|
const existingRole = await ctx.mysql.getOne({
|
|
138
|
-
table:
|
|
122
|
+
table: BEFLY_ROLE_TABLE,
|
|
139
123
|
fields: ["id", "apis"],
|
|
140
124
|
where: {
|
|
141
125
|
code: roleConfig.code,
|
|
@@ -150,24 +134,8 @@ export async function syncDev(ctx) {
|
|
|
150
134
|
continue;
|
|
151
135
|
}
|
|
152
136
|
|
|
153
|
-
|
|
154
|
-
if (auth === "否" || auth === false) {
|
|
137
|
+
if (authAllowsRole(api.auth, roleConfig.code)) {
|
|
155
138
|
defaultApis.push(apiPath);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 与 syncApi 的存储语义保持一致:
|
|
160
|
-
// - true -> "是"
|
|
161
|
-
// - false -> "否"
|
|
162
|
-
// - string[] -> "a,b,c"
|
|
163
|
-
if (typeof auth === "string" && auth !== "是") {
|
|
164
|
-
const roleCodes = auth.split(",");
|
|
165
|
-
for (const roleCode of roleCodes) {
|
|
166
|
-
if (roleCode.trim() === roleConfig.code) {
|
|
167
|
-
defaultApis.push(apiPath);
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
139
|
}
|
|
172
140
|
}
|
|
173
141
|
|
|
@@ -200,7 +168,7 @@ export async function syncDev(ctx) {
|
|
|
200
168
|
if (existingRole.data?.id) {
|
|
201
169
|
// 角色存在则强制更新(不做差异判断)
|
|
202
170
|
await ctx.mysql.updData({
|
|
203
|
-
table:
|
|
171
|
+
table: BEFLY_ROLE_TABLE,
|
|
204
172
|
where: { code: roleConfig.code, state$gte: 0 },
|
|
205
173
|
data: {
|
|
206
174
|
name: roleConfig.name,
|
|
@@ -212,7 +180,7 @@ export async function syncDev(ctx) {
|
|
|
212
180
|
});
|
|
213
181
|
} else {
|
|
214
182
|
await ctx.mysql.insData({
|
|
215
|
-
table:
|
|
183
|
+
table: BEFLY_ROLE_TABLE,
|
|
216
184
|
data: {
|
|
217
185
|
code: roleConfig.code,
|
|
218
186
|
name: roleConfig.name,
|
|
@@ -225,11 +193,6 @@ export async function syncDev(ctx) {
|
|
|
225
193
|
}
|
|
226
194
|
}
|
|
227
195
|
} catch (error) {
|
|
228
|
-
throw
|
|
229
|
-
cause: error,
|
|
230
|
-
code: "runtime",
|
|
231
|
-
subsystem: "sync",
|
|
232
|
-
operation: "syncDev"
|
|
233
|
-
});
|
|
196
|
+
throw createError("同步开发者数据失败", { cause: error, code: "runtime", subsystem: "sync", operation: "syncDev" });
|
|
234
197
|
}
|
|
235
198
|
}
|
package/sync/menu.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { BEFLY_MENU_TABLE } from "../configs/constConfig.js";
|
|
1
2
|
import { buildPathSyncLists } from "./syncUtil.js";
|
|
2
3
|
|
|
3
|
-
const MENU_TABLE_NAME = "beflyMenu";
|
|
4
|
-
|
|
5
4
|
export function flattenMenusToDefMap(menus) {
|
|
6
5
|
// 读取配置菜单:扁平化为 path => { name, sort, parentPath }
|
|
7
6
|
// - 以 path 为唯一键:后出现的覆盖先出现的(与旧逻辑“同 path 多次同步同一条记录”一致)
|
|
@@ -44,7 +43,7 @@ export async function syncMenu(ctx, menus) {
|
|
|
44
43
|
// 2) 批量同步(事务内):按 path diff 执行批量 insert/update/delete
|
|
45
44
|
// 读取全部菜单
|
|
46
45
|
const allExistingMenus = await ctx.mysql.getAll({
|
|
47
|
-
table:
|
|
46
|
+
table: BEFLY_MENU_TABLE,
|
|
48
47
|
fields: ["id", "name", "path", "parentPath", "sort", "state"],
|
|
49
48
|
where: { state$gte: 0 }
|
|
50
49
|
});
|
|
@@ -67,15 +66,15 @@ export async function syncMenu(ctx, menus) {
|
|
|
67
66
|
);
|
|
68
67
|
|
|
69
68
|
if (syncLists.updList.length > 0) {
|
|
70
|
-
await ctx.mysql.updBatch(
|
|
69
|
+
await ctx.mysql.updBatch(BEFLY_MENU_TABLE, syncLists.updList);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
if (syncLists.insList.length > 0) {
|
|
74
|
-
await ctx.mysql.insBatch(
|
|
73
|
+
await ctx.mysql.insBatch(BEFLY_MENU_TABLE, syncLists.insList);
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
// 3) 删除差集(DB - 配置)
|
|
78
77
|
if (syncLists.delIds.length > 0) {
|
|
79
|
-
await ctx.mysql.delForceBatch(
|
|
78
|
+
await ctx.mysql.delForceBatch(BEFLY_MENU_TABLE, syncLists.delIds);
|
|
80
79
|
}
|
|
81
80
|
}
|
package/sync/syncUtil.js
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
// 接口 auth 的存储协议(serializeAuth 与 authAllowsRole 配套使用):
|
|
2
|
+
// - true -> "是"
|
|
3
|
+
// - false -> "否"
|
|
4
|
+
// - string[] -> "a,b,c"
|
|
5
|
+
export const AUTH_YES = "是";
|
|
6
|
+
export const AUTH_NO = "否";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 序列化 api.auth 为存储字符串
|
|
10
|
+
*/
|
|
11
|
+
export function serializeAuth(auth) {
|
|
12
|
+
return auth === false ? AUTH_NO : Array.isArray(auth) ? auth.join(",") : AUTH_YES;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 判断 auth 存储串是否授权给指定角色
|
|
17
|
+
*/
|
|
18
|
+
export function authAllowsRole(auth, roleCode) {
|
|
19
|
+
if (auth === AUTH_NO) return true;
|
|
20
|
+
if (typeof auth === "string" && auth !== AUTH_YES) {
|
|
21
|
+
return auth.split(",").some((code) => code.trim() === roleCode);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
1
26
|
export function buildPathSyncLists(existingList, defList, buildUpdateData, buildInsertData) {
|
|
2
27
|
const existingMap = new Map();
|
|
3
28
|
const delIdSet = new Set();
|
package/utils/calcPerfTime.js
CHANGED
|
@@ -6,8 +6,7 @@ export const calcPerfTime = (startTime, endTime = Bun.nanoseconds()) => {
|
|
|
6
6
|
|
|
7
7
|
if (elapsedMs < 1000) {
|
|
8
8
|
return `${elapsedMs.toFixed(2)} 毫秒`;
|
|
9
|
-
} else {
|
|
10
|
-
const elapsedSeconds = elapsedMs / 1000;
|
|
11
|
-
return `${elapsedSeconds.toFixed(2)} 秒`;
|
|
12
9
|
}
|
|
10
|
+
const elapsedSeconds = elapsedMs / 1000;
|
|
11
|
+
return `${elapsedSeconds.toFixed(2)} 秒`;
|
|
13
12
|
};
|
package/utils/crypto.js
CHANGED
|
@@ -2,46 +2,6 @@
|
|
|
2
2
|
* 通用加密/哈希工具
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { isString } from "./is.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 计算 md5(hex)
|
|
9
|
-
* @param value 输入字符串
|
|
10
|
-
* @returns md5 hex
|
|
11
|
-
*/
|
|
12
|
-
export function md5(value) {
|
|
13
|
-
if (!isString(value) || value.length < 1) {
|
|
14
|
-
throw new Error("md5 输入必须是非空字符串", {
|
|
15
|
-
cause: null,
|
|
16
|
-
code: "validation",
|
|
17
|
-
subsystem: "crypto",
|
|
18
|
-
operation: "md5"
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
const hasher = new Bun.CryptoHasher("md5");
|
|
22
|
-
hasher.update(value);
|
|
23
|
-
return hasher.digest("hex");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 计算 sha256(hex)
|
|
28
|
-
* @param value 输入字符串
|
|
29
|
-
* @returns sha256 hex
|
|
30
|
-
*/
|
|
31
|
-
export function sha256(value) {
|
|
32
|
-
if (!isString(value) || value.length < 1) {
|
|
33
|
-
throw new Error("sha256 输入必须是非空字符串", {
|
|
34
|
-
cause: null,
|
|
35
|
-
code: "validation",
|
|
36
|
-
subsystem: "crypto",
|
|
37
|
-
operation: "sha256"
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
const hasher = new Bun.CryptoHasher("sha256");
|
|
41
|
-
hasher.update(value);
|
|
42
|
-
return hasher.digest("hex");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
5
|
/**
|
|
46
6
|
* 使用 bcrypt 对密码进行哈希
|
|
47
7
|
* @param password 原始密码
|
package/utils/deepMerge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createError } from "./error.js";
|
|
1
2
|
import { isPlainObject } from "./is.js";
|
|
2
3
|
|
|
3
4
|
function clone(value) {
|
|
@@ -69,10 +70,5 @@ export function deepMerge(source, target) {
|
|
|
69
70
|
return mergeInto(base, incoming);
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
throw
|
|
73
|
-
cause: null,
|
|
74
|
-
code: "validation",
|
|
75
|
-
subsystem: "utils",
|
|
76
|
-
operation: "deepMerge"
|
|
77
|
-
});
|
|
73
|
+
throw createError("deepMerge: source/target 必须同为数组或同为普通对象", { code: "validation", subsystem: "utils", operation: "deepMerge" });
|
|
78
74
|
}
|
package/utils/error.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一错误工厂
|
|
3
|
+
* 框架错误对象的唯一构造入口,统一字段约定:
|
|
4
|
+
* - code: "validation" | "runtime" | "policy"
|
|
5
|
+
* - subsystem: 子系统标识(sql/cache/redis/sync/config/start/...)
|
|
6
|
+
* - operation: 操作名
|
|
7
|
+
* - 其余字段原样透传(如 table、roleCode、runMode 等)
|
|
8
|
+
* - 不传 cause 时不设置(缺省为 undefined,避免 cause: null 噪音)
|
|
9
|
+
*/
|
|
10
|
+
export function createError(message, options = {}) {
|
|
11
|
+
const error = new Error(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
12
|
+
|
|
13
|
+
if (options.code) error.code = options.code;
|
|
14
|
+
if (options.subsystem) error.subsystem = options.subsystem;
|
|
15
|
+
if (options.operation) error.operation = options.operation;
|
|
16
|
+
|
|
17
|
+
for (const [key, value] of Object.entries(options)) {
|
|
18
|
+
if (key === "cause" || key === "code" || key === "subsystem" || key === "operation") {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
error[key] = value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 参数校验错误(code: "validation")
|
|
29
|
+
*/
|
|
30
|
+
export function validationError(message) {
|
|
31
|
+
return createError(message, { code: "validation" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 沿 cause 链取最底层错误(根因)
|
|
36
|
+
*/
|
|
37
|
+
export function getRootCause(error) {
|
|
38
|
+
let current = error;
|
|
39
|
+
while (current?.cause instanceof Error) {
|
|
40
|
+
current = current.cause;
|
|
41
|
+
}
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 错误链摘要:沿 cause 展开为单行文本,根因在后
|
|
47
|
+
* 每层格式:message [code/subsystem/operation](无元信息时仅 message)
|
|
48
|
+
* 例:SQL执行失败 [runtime/sql/execute] <- ER_DUP_ENTRY: 重复插入 [runtime]
|
|
49
|
+
*/
|
|
50
|
+
export function errorChainSummary(error) {
|
|
51
|
+
const parts = [];
|
|
52
|
+
const visited = new Set();
|
|
53
|
+
let current = error;
|
|
54
|
+
|
|
55
|
+
while (current instanceof Error && !visited.has(current)) {
|
|
56
|
+
visited.add(current);
|
|
57
|
+
const meta = [current.code, current.subsystem, current.operation].filter(Boolean).join("/");
|
|
58
|
+
const text = current.message || current.name || "Error";
|
|
59
|
+
parts.push(meta ? `${text} [${meta}]` : text);
|
|
60
|
+
current = current.cause;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return parts.join(" <- ");
|
|
64
|
+
}
|
package/utils/fieldClear.js
CHANGED
|
@@ -1,65 +1,22 @@
|
|
|
1
1
|
// fieldClear 工具函数实现
|
|
2
|
-
//
|
|
2
|
+
// 过滤对象中指定值(如 undefined)的字段,其余字段原样保留
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function isArray(val) {
|
|
9
|
-
return Array.isArray(val);
|
|
10
|
-
}
|
|
4
|
+
import { isPlainObject } from "./is.js";
|
|
11
5
|
|
|
12
6
|
export function fieldClear(data, options = {}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const result = {};
|
|
17
|
-
let keys = Object.keys(obj);
|
|
18
|
-
if (pickKeys && pickKeys.length) {
|
|
19
|
-
keys = keys.filter((k) => pickKeys.includes(k));
|
|
20
|
-
}
|
|
21
|
-
if (omitKeys && omitKeys.length) {
|
|
22
|
-
keys = keys.filter((k) => !omitKeys.includes(k));
|
|
23
|
-
}
|
|
24
|
-
for (const key of keys) {
|
|
25
|
-
const value = obj[key];
|
|
26
|
-
|
|
27
|
-
// 1. 优先检查 keepMap
|
|
28
|
-
if (keepMap && key in keepMap) {
|
|
29
|
-
if (Object.is(keepMap[key], value)) {
|
|
30
|
-
result[key] = value;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
7
|
+
if (!isPlainObject(data)) {
|
|
8
|
+
return data;
|
|
9
|
+
}
|
|
34
10
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
11
|
+
const { excludeValues } = options;
|
|
12
|
+
const result = {};
|
|
39
13
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
result[key] = value;
|
|
14
|
+
for (const [key, value] of Object.entries(data)) {
|
|
15
|
+
if (excludeValues && excludeValues.length && excludeValues.includes(value)) {
|
|
16
|
+
continue;
|
|
45
17
|
}
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
if (isArray(data)) {
|
|
50
|
-
return data
|
|
51
|
-
.map((item) => (isObject(item) ? filterObj(item) : item))
|
|
52
|
-
.filter((item) => {
|
|
53
|
-
if (isObject(item)) {
|
|
54
|
-
// 只保留有内容的对象
|
|
55
|
-
return Object.keys(item).length > 0;
|
|
56
|
-
}
|
|
57
|
-
// 原始值直接保留
|
|
58
|
-
return true;
|
|
59
|
-
});
|
|
18
|
+
result[key] = value;
|
|
60
19
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
return data;
|
|
20
|
+
|
|
21
|
+
return result;
|
|
65
22
|
}
|
package/utils/importDefault.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 动态导入模块并优先返回其 default 导出。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - default
|
|
4
|
+
* 仅限启动期使用(扫描 API/插件/钩子/表定义等静态资产),不要在运行期(请求处理中)调用:
|
|
5
|
+
* - import() 报错、default 导出类型与默认值不一致时,直接抛错中断启动(fail-fast),真实原因随 cause 带出
|
|
6
|
+
* - default 导出为空(null/undefined)时允许,回退默认值
|
|
6
7
|
*/
|
|
7
8
|
import { isAbsolute } from "node:path";
|
|
8
9
|
import { pathToFileURL } from "node:url";
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { createError } from "./error.js";
|
|
11
12
|
import { isNullable } from "./is.js";
|
|
12
13
|
import { getTypeTag } from "./util.js";
|
|
13
14
|
|
|
@@ -31,21 +32,19 @@ export async function importDefault(file, defaultValue) {
|
|
|
31
32
|
// 为保证一致性:json 一律走 import assertion。
|
|
32
33
|
const mod = isJson ? await import(toFileImportUrl(file), { with: { type: "json" } }) : await import(file);
|
|
33
34
|
const value = mod?.default;
|
|
35
|
+
|
|
36
|
+
// default 导出为空:允许,回退默认值
|
|
34
37
|
if (isNullable(value)) {
|
|
35
38
|
return defaultValue;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const actualType = getTypeTag(value);
|
|
42
|
-
if (expectedType !== actualType) {
|
|
43
|
-
Logger.warn("importDefault 导入类型与默认值不一致,已回退到默认值", { file: file, expectedType: expectedType, actualType: actualType });
|
|
44
|
-
return defaultValue;
|
|
41
|
+
// 类型与默认值不一致:视为导入异常,中断启动
|
|
42
|
+
if (getTypeTag(value) !== getTypeTag(defaultValue)) {
|
|
43
|
+
throw new Error("导入内容类型与默认值不一致");
|
|
45
44
|
}
|
|
45
|
+
|
|
46
46
|
return value;
|
|
47
47
|
} catch (err) {
|
|
48
|
-
|
|
49
|
-
return defaultValue;
|
|
48
|
+
throw createError(`导入失败:${file}`, { cause: err, code: "runtime", subsystem: "scan", operation: "importDefault" });
|
|
50
49
|
}
|
|
51
50
|
}
|