befly 3.76.0 → 3.76.2
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/Befly.js +159 -0
- package/README.md +50 -11
- package/apis/admin/delete.js +1 -3
- package/apis/admin/insert.js +2 -1
- package/apis/admin/select.js +2 -7
- package/apis/admin/update.js +3 -8
- package/apis/api/select.js +2 -7
- package/apis/auth/login.js +16 -20
- package/apis/dict/delete.js +1 -1
- package/apis/dict/detail.js +1 -3
- package/apis/dict/select.js +1 -2
- package/apis/dict/update.js +1 -1
- package/apis/dictType/delete.js +1 -1
- package/apis/dictType/detail.js +1 -1
- package/apis/dictType/insert.js +1 -1
- package/apis/dictType/select.js +2 -7
- package/apis/dictType/update.js +2 -2
- package/apis/email/config.js +4 -7
- package/apis/email/logList.js +2 -7
- package/apis/email/send.js +1 -2
- package/apis/email/verify.js +1 -3
- package/apis/loginLog/select.js +2 -7
- package/apis/menu/select.js +2 -7
- package/apis/operateLog/select.js +2 -7
- package/apis/role/apiSave.js +2 -2
- package/apis/role/menuSave.js +2 -2
- package/apis/role/select.js +2 -7
- package/apis/source/imageList.js +2 -5
- package/apis/tongJi/dailyReport.js +12 -13
- package/apis/tongJi/dailyReportSelect.js +1 -2
- package/apis/tongJi/dailyStats.js +1 -1
- package/apis/tongJi/dailyStatsDistribution.js +2 -2
- package/apis/tongJi/errorList.js +1 -2
- package/apis/tongJi/errorReport.js +1 -2
- package/apis/tongJi/projectDelete.js +1 -3
- package/apis/tongJi/projectSelect.js +2 -4
- package/apis/tongJi/projectUpdate.js +2 -2
- package/apis/upload/file.js +7 -5
- package/checks/api.js +43 -66
- package/checks/config.js +17 -12
- package/checks/corns.js +28 -46
- package/checks/field.js +18 -0
- package/checks/items.js +16 -60
- package/checks/menu.js +28 -39
- package/checks/table.js +106 -26
- package/checks/validation.js +13 -0
- package/configs/beflyConfig.json +6 -6
- package/configs/constConfig.js +2 -63
- package/exports.js +2 -0
- package/hooks/auth.js +11 -7
- package/hooks/parser.js +55 -59
- package/hooks/permission.js +5 -2
- package/hooks/rateLimit.js +25 -62
- package/hooks/validator.js +43 -31
- package/index.js +43 -344
- package/{lib → libs}/cacheHelper.js +3 -8
- package/libs/cron.js +60 -0
- package/libs/logger/index.js +1 -0
- package/libs/logger/logger.js +245 -0
- package/libs/logger/sanitize.js +115 -0
- package/{lib → libs/mysql}/dbHelper.js +100 -73
- package/{lib → libs/mysql}/dbParse.js +16 -9
- package/{lib → libs/mysql}/dbUtil.js +15 -32
- package/libs/mysql/error.js +29 -0
- package/libs/mysql/index.js +6 -0
- package/libs/mysql/is.js +29 -0
- package/libs/mysql/mysql.js +43 -0
- package/libs/mysql/sql/identifier.js +85 -0
- package/libs/mysql/sql/index.js +2 -0
- package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
- package/libs/mysql/util.js +44 -0
- package/libs/redis/index.js +2 -0
- package/libs/redis/redis.js +204 -0
- package/libs/redis/redisError.js +8 -0
- package/libs/tableModel.js +72 -0
- package/libs/tool.js +33 -0
- package/libs/validator/compiler.js +248 -0
- package/libs/validator/index.js +10 -0
- package/libs/validator/parser.js +173 -0
- package/libs/validator/util.js +5 -0
- package/package.json +10 -8
- package/paths.js +2 -3
- package/plugins/cache.js +2 -1
- package/plugins/email.js +4 -4
- package/plugins/mysql.js +5 -15
- package/plugins/redis.js +4 -12
- package/router/api.js +14 -10
- package/router/root.js +14 -0
- package/router/static.js +3 -3
- package/schemas/api.json +99 -0
- package/schemas/config.json +255 -0
- package/schemas/corn.json +39 -0
- package/schemas/item.json +43 -0
- package/schemas/menu.json +44 -0
- package/schemas/table.json +80 -0
- package/scripts/syncDb/apply.js +87 -0
- package/scripts/syncDb/ddl.js +43 -0
- package/scripts/syncDb/index.js +175 -30
- package/scripts/syncDb/model.js +60 -0
- package/scripts/syncDb/plan.js +160 -0
- package/scripts/syncDb/preflight.js +98 -0
- package/scripts/syncDb/report.js +177 -162
- package/scripts/syncDb/snapshot.js +118 -0
- package/sync/syncUtil.js +7 -13
- package/tables/admin.json +51 -53
- package/tables/api.json +29 -40
- package/tables/dailyReport.json +85 -73
- package/tables/dict.json +27 -39
- package/tables/dictType.json +20 -34
- package/tables/emailLog.json +49 -47
- package/tables/errorReport.json +86 -60
- package/tables/file.json +47 -50
- package/tables/loginLog.json +68 -55
- package/tables/menu.json +20 -34
- package/tables/operateLog.json +53 -47
- package/tables/project.json +18 -30
- package/tables/role.json +33 -37
- package/utils/cors.js +37 -41
- package/utils/deepMerge.js +3 -1
- package/utils/error.js +1 -56
- package/utils/id.js +44 -0
- package/utils/is.js +6 -243
- package/utils/response.js +12 -1
- package/utils/scanFiles.js +13 -4
- package/utils/scanSources.js +58 -20
- package/utils/util.js +11 -163
- package/apis/_apis.js +0 -20
- package/apis/auth/sendSmsCode.js +0 -24
- package/apis/role/delete.js +0 -41
- package/apis/role/insert.js +0 -45
- package/apis/role/save.js +0 -36
- package/apis/role/update.js +0 -66
- package/configs/regexpAlias.json +0 -55
- package/lib/connect.js +0 -119
- package/lib/logger.js +0 -430
- package/lib/redisHelper.js +0 -399
- package/lib/validator.js +0 -274
- package/lib/xmlParse.js +0 -149
- package/plugins/config.js +0 -13
- package/plugins/cron.js +0 -115
- package/plugins/logger.js +0 -17
- package/plugins/tool.js +0 -131
- package/plugins/xmlParse.js +0 -10
- package/schemas/config.js +0 -67
- package/scripts/syncDb/context.js +0 -120
- package/scripts/syncDb/diff.js +0 -279
- package/scripts/syncDb/transform.js +0 -113
- package/sql/befly.sql +0 -271
- package/utils/email.js +0 -3
- package/utils/fieldClear.js +0 -22
- package/utils/formatValidationIssues.js +0 -28
- package/utils/getClientIp.js +0 -47
- package/utils/loggerUtils.js +0 -196
- package/utils/regexpUtil.js +0 -52
- /package/{lib → libs}/smtpText.js +0 -0
package/apis/role/menuSave.js
CHANGED
|
@@ -11,9 +11,9 @@ export default {
|
|
|
11
11
|
roleCode: roleTable.code,
|
|
12
12
|
menuPaths: roleTable.menus
|
|
13
13
|
},
|
|
14
|
-
required: ["roleCode"],
|
|
14
|
+
required: ["roleCode", "menuPaths"],
|
|
15
15
|
handler: async (befly, ctx) => {
|
|
16
|
-
const menuPaths = ctx.body.menuPaths
|
|
16
|
+
const menuPaths = ctx.body.menuPaths;
|
|
17
17
|
|
|
18
18
|
const role = await getRoleByCode(befly, ctx.body.roleCode);
|
|
19
19
|
|
package/apis/role/select.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { queryFields } from "#befly/apis/_apis.js";
|
|
2
|
-
import roleTable from "#befly/tables/role.json";
|
|
3
|
-
|
|
4
1
|
export default {
|
|
5
2
|
name: "获取角色列表",
|
|
3
|
+
category: "select",
|
|
6
4
|
method: "POST",
|
|
7
5
|
body: "none",
|
|
8
6
|
auth: true,
|
|
9
|
-
fields: {
|
|
10
|
-
...queryFields,
|
|
11
|
-
state: roleTable.state
|
|
12
|
-
},
|
|
7
|
+
fields: {},
|
|
13
8
|
required: [],
|
|
14
9
|
handler: async (befly, ctx) => {
|
|
15
10
|
const roles = await befly.mysql.getList({
|
package/apis/source/imageList.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { queryFields } from "#befly/apis/_apis.js";
|
|
2
|
-
|
|
3
1
|
export default {
|
|
4
2
|
name: "获取图库列表",
|
|
3
|
+
category: "select",
|
|
5
4
|
method: "POST",
|
|
6
5
|
body: "none",
|
|
7
6
|
auth: true,
|
|
8
|
-
fields: {
|
|
9
|
-
...queryFields
|
|
10
|
-
},
|
|
7
|
+
fields: {},
|
|
11
8
|
required: [],
|
|
12
9
|
handler: async (befly, ctx) => {
|
|
13
10
|
const result = await befly.mysql.getList({
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Logger } from "#befly/lib/logger.js";
|
|
2
1
|
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
3
2
|
|
|
4
3
|
import { getTongJiDateYmdNumber } from "./_tongJi.js";
|
|
@@ -11,17 +10,17 @@ export default {
|
|
|
11
10
|
body: "none",
|
|
12
11
|
auth: false,
|
|
13
12
|
fields: {
|
|
14
|
-
clientType: { name: "客户端类型",
|
|
15
|
-
clientId: { name: "客户端标识",
|
|
16
|
-
productName: { name: "产品名称",
|
|
17
|
-
productCode: { name: "产品代号",
|
|
18
|
-
productVersion: { name: "产品版本",
|
|
19
|
-
clientVersion: { name: "客户端版本",
|
|
20
|
-
os: { name: "操作系统",
|
|
21
|
-
deviceId: { name: "设备标识",
|
|
22
|
-
brand: { name: "设备品牌",
|
|
23
|
-
model: { name: "设备型号",
|
|
24
|
-
system: { name: "系统",
|
|
13
|
+
clientType: { name: "客户端类型", paramType: "string", enum: ["browser", "vscode", "desktop", "miniapp", "app"], minValue: 3, maxValue: 20 },
|
|
14
|
+
clientId: { name: "客户端标识", paramType: "string", minValue: 0, maxValue: 120 },
|
|
15
|
+
productName: { name: "产品名称", paramType: "string", minValue: 0, maxValue: 100 },
|
|
16
|
+
productCode: { name: "产品代号", paramType: "string", minValue: 0, maxValue: 100 },
|
|
17
|
+
productVersion: { name: "产品版本", paramType: "string", minValue: 0, maxValue: 50 },
|
|
18
|
+
clientVersion: { name: "客户端版本", paramType: "string", minValue: 0, maxValue: 100 },
|
|
19
|
+
os: { name: "操作系统", paramType: "string", minValue: 0, maxValue: 100 },
|
|
20
|
+
deviceId: { name: "设备标识", paramType: "string", minValue: 0, maxValue: 120 },
|
|
21
|
+
brand: { name: "设备品牌", paramType: "string", minValue: 0, maxValue: 100 },
|
|
22
|
+
model: { name: "设备型号", paramType: "string", minValue: 0, maxValue: 100 },
|
|
23
|
+
system: { name: "系统", paramType: "string", minValue: 0, maxValue: 100 }
|
|
25
24
|
},
|
|
26
25
|
required: ["clientType"],
|
|
27
26
|
handler: async (befly, ctx) => {
|
|
@@ -84,7 +83,7 @@ export default {
|
|
|
84
83
|
await befly.mysql.insData({ table: "beflyDailyReport", data: row });
|
|
85
84
|
stored = true;
|
|
86
85
|
} catch (error) {
|
|
87
|
-
|
|
86
|
+
befly.logger.error("daily_report 落盘失败", error);
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { queryFields } from "#befly/apis/_apis.js";
|
|
2
1
|
import dailyReportTable from "#befly/tables/dailyReport.json";
|
|
3
2
|
|
|
4
3
|
export default {
|
|
5
4
|
name: "获取日报记录列表",
|
|
5
|
+
category: "select",
|
|
6
6
|
method: "POST",
|
|
7
7
|
body: "none",
|
|
8
8
|
auth: true,
|
|
9
9
|
fields: {
|
|
10
|
-
...queryFields,
|
|
11
10
|
productCode: dailyReportTable.productCode,
|
|
12
11
|
clientType: dailyReportTable.clientType,
|
|
13
12
|
reportDate: dailyReportTable.reportDate,
|
|
@@ -137,7 +137,7 @@ export default {
|
|
|
137
137
|
body: "none",
|
|
138
138
|
auth: true,
|
|
139
139
|
fields: {
|
|
140
|
-
productCode: { name: "产品代号",
|
|
140
|
+
productCode: { name: "产品代号", paramType: "string", minValue: 0, maxValue: 100 }
|
|
141
141
|
},
|
|
142
142
|
required: [],
|
|
143
143
|
handler: async (befly, ctx) => {
|
|
@@ -62,8 +62,8 @@ export default {
|
|
|
62
62
|
body: "none",
|
|
63
63
|
auth: true,
|
|
64
64
|
fields: {
|
|
65
|
-
productCode: { name: "产品代号",
|
|
66
|
-
dateRange: { name: "日期范围",
|
|
65
|
+
productCode: { name: "产品代号", paramType: "string", minValue: 1, maxValue: 100 },
|
|
66
|
+
dateRange: { name: "日期范围", paramType: "string", enum: ["today", "yesterday", "dayBeforeYesterday", "recent30", "month"], minValue: 1, maxValue: 20 }
|
|
67
67
|
},
|
|
68
68
|
required: ["productCode", "dateRange"],
|
|
69
69
|
handler: async (befly, ctx) => {
|
package/apis/tongJi/errorList.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { queryFields } from "#befly/apis/_apis.js";
|
|
2
1
|
import errorReportTable from "#befly/tables/errorReport.json";
|
|
3
2
|
|
|
4
3
|
const NUMBER_FIELDS = ["reportTime", "firstReportTime", "bucketTime", "bucketDate"];
|
|
@@ -7,11 +6,11 @@ const KEYWORD_FIELDS = ["pagePath", "pageName", "message", "errorType", "product
|
|
|
7
6
|
|
|
8
7
|
export default {
|
|
9
8
|
name: "获取错误报告列表",
|
|
9
|
+
category: "select",
|
|
10
10
|
method: "POST",
|
|
11
11
|
body: "none",
|
|
12
12
|
auth: true,
|
|
13
13
|
fields: {
|
|
14
|
-
...queryFields,
|
|
15
14
|
errorType: errorReportTable.errorType,
|
|
16
15
|
productName: errorReportTable.productName,
|
|
17
16
|
productCode: errorReportTable.productCode,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Logger } from "#befly/lib/logger.js";
|
|
2
1
|
import errorReportTable from "#befly/tables/errorReport.json";
|
|
3
2
|
import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
|
|
4
3
|
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
@@ -111,7 +110,7 @@ export default {
|
|
|
111
110
|
await befly.mysql.insData({ table: "beflyErrorReport", data: data });
|
|
112
111
|
stored = true;
|
|
113
112
|
} catch (error) {
|
|
114
|
-
|
|
113
|
+
befly.logger.error("errorReport 插入失败", error);
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import projectTable from "#befly/tables/project.json";
|
|
2
|
-
|
|
3
1
|
export default {
|
|
4
2
|
name: "删除产品",
|
|
5
3
|
method: "POST",
|
|
6
4
|
body: "none",
|
|
7
5
|
auth: true,
|
|
8
6
|
fields: {
|
|
9
|
-
id:
|
|
7
|
+
id: { name: "ID", paramType: "integer", minValue: 1 }
|
|
10
8
|
},
|
|
11
9
|
required: ["id"],
|
|
12
10
|
handler: async (befly, ctx) => {
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { queryFields } from "#befly/apis/_apis.js";
|
|
2
1
|
import projectTable from "#befly/tables/project.json";
|
|
3
2
|
|
|
4
3
|
export default {
|
|
5
4
|
name: "获取产品列表",
|
|
5
|
+
category: "select",
|
|
6
6
|
method: "POST",
|
|
7
7
|
body: "none",
|
|
8
8
|
auth: true,
|
|
9
9
|
fields: {
|
|
10
|
-
|
|
11
|
-
code: projectTable.code,
|
|
12
|
-
state: projectTable.state
|
|
10
|
+
code: projectTable.code
|
|
13
11
|
},
|
|
14
12
|
required: [],
|
|
15
13
|
handler: async (befly, ctx) => {
|
|
@@ -6,11 +6,11 @@ export default {
|
|
|
6
6
|
body: "none",
|
|
7
7
|
auth: true,
|
|
8
8
|
fields: {
|
|
9
|
-
id:
|
|
9
|
+
id: { name: "ID", paramType: "integer", minValue: 1 },
|
|
10
10
|
code: projectTable.code,
|
|
11
11
|
name: projectTable.name,
|
|
12
12
|
sort: projectTable.sort,
|
|
13
|
-
state:
|
|
13
|
+
state: { name: "状态", paramType: "integer" }
|
|
14
14
|
},
|
|
15
15
|
required: ["id"],
|
|
16
16
|
handler: async (befly, ctx) => {
|
package/apis/upload/file.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { mkdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { extname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { Logger } from "#befly/lib/logger.js";
|
|
5
4
|
import { getAppPublicDir } from "#befly/paths.js";
|
|
6
5
|
import { getMonthDir } from "#befly/utils/datetime.js";
|
|
7
6
|
|
|
@@ -13,6 +12,7 @@ export default {
|
|
|
13
12
|
fields: {},
|
|
14
13
|
required: [],
|
|
15
14
|
handler: async (befly, ctx) => {
|
|
15
|
+
let savedFilePath;
|
|
16
16
|
try {
|
|
17
17
|
const maxFileSizeMb = Number(befly.config.upload.maxSize);
|
|
18
18
|
const maxFileSize = maxFileSizeMb * 1024 * 1024;
|
|
@@ -78,7 +78,8 @@ export default {
|
|
|
78
78
|
const isImage = fileType === "image" ? 1 : 0;
|
|
79
79
|
|
|
80
80
|
mkdirSync(uploadDir, { recursive: true });
|
|
81
|
-
|
|
81
|
+
savedFilePath = join(uploadDir, savedFileName);
|
|
82
|
+
await Bun.write(savedFilePath, rawBuffer);
|
|
82
83
|
|
|
83
84
|
const insertRes = await befly.mysql.insData({
|
|
84
85
|
table: "beflyFile",
|
|
@@ -96,7 +97,7 @@ export default {
|
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
if (!insertRes.data) {
|
|
99
|
-
|
|
100
|
+
throw new Error("资源记录写入失败");
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
return befly.tool.Yes("上传成功", {
|
|
@@ -112,7 +113,8 @@ export default {
|
|
|
112
113
|
url: absoluteFileUrl
|
|
113
114
|
});
|
|
114
115
|
} catch (error) {
|
|
115
|
-
|
|
116
|
+
if (savedFilePath && existsSync(savedFilePath)) unlinkSync(savedFilePath);
|
|
117
|
+
befly.logger.error("上传文件失败", error, {
|
|
116
118
|
apiPath: ctx.apiPath,
|
|
117
119
|
userId: ctx.userId,
|
|
118
120
|
roleType: ctx.roleType
|
package/checks/api.js
CHANGED
|
@@ -1,74 +1,51 @@
|
|
|
1
|
-
import { Logger } from "
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
for (const key of Object.keys(api)) {
|
|
27
|
-
if (!apiKeys.has(key)) issues.push({ path: [index, key], message: "不允许出现" });
|
|
28
|
-
}
|
|
29
|
-
for (const key of ["source", "type", "filePath", "relativePath"]) checkNoTrimString(issues, api[key], [index, key], 1);
|
|
30
|
-
checkNoTrimString(issues, api.name, [index, "name"], 1, 100);
|
|
31
|
-
if (!isString(api.fileName) || !isLowerCamelCaseName(api.fileName)) issues.push({ path: [index, "fileName"], message: "必须是 lowerCamelCase" });
|
|
32
|
-
if (!isFunction(api.handler)) issues.push({ path: [index, "handler"], message: "必须是函数" });
|
|
33
|
-
if (!isString(api.apiPath) || !apiPathRegex.test(api.apiPath)) issues.push({ path: [index, "apiPath"], message: "路径格式无效" });
|
|
34
|
-
if (!["GET", "POST", "ALL"].includes(api.method)) issues.push({ path: [index, "method"], message: "仅允许 GET|POST|ALL" });
|
|
35
|
-
if (!["none", "raw"].includes(api.body)) issues.push({ path: [index, "body"], message: "仅允许 none|raw" });
|
|
36
|
-
// body 为 raw 时不允许配置 fields/required:parser 跳过解析,参数校验由 handler 自行承担
|
|
37
|
-
if (api.body === "raw" && isPlainObject(api.fields) && Object.keys(api.fields).length > 0) {
|
|
38
|
-
issues.push({ path: [index, "body"], message: 'body 为 "raw" 时不允许配置 fields,由 handler 自行解析校验' });
|
|
39
|
-
}
|
|
40
|
-
if (api.body === "raw" && Array.isArray(api.required) && api.required.length > 0) {
|
|
41
|
-
issues.push({ path: [index, "body"], message: 'body 为 "raw" 时不允许配置 required,由 handler 自行解析校验' });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!isBoolean(api.auth) && (!Array.isArray(api.auth) || api.auth.length === 0 || api.auth.some((item) => !isString(item) || (item !== "" && item.trim() !== item)))) {
|
|
45
|
-
issues.push({ path: [index, "auth"], message: "必须是 boolean 或非空字符串数组" });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!isPlainObject(api.fields)) {
|
|
49
|
-
issues.push({ path: [index, "fields"], message: "必须是对象格式" });
|
|
50
|
-
} else {
|
|
51
|
-
for (const [fieldName, rule] of Object.entries(api.fields)) {
|
|
52
|
-
if (!isLowerCamelCaseName(fieldName)) issues.push({ path: [index, "fields", fieldName], message: "字段名必须为 lowerCamelCase" });
|
|
53
|
-
for (const issue of validateFieldRule(rule)) issues.push({ path: [index, "fields", fieldName, ...issue.path], message: issue.message });
|
|
1
|
+
import { Logger } from "#befly/libs/logger/index.js";
|
|
2
|
+
import { compile } from "#befly/libs/validator/index.js";
|
|
3
|
+
|
|
4
|
+
import apiSchema from "../schemas/api.json";
|
|
5
|
+
import { getFieldSchema } from "./field.js";
|
|
6
|
+
import { formatIssues } from "./validation.js";
|
|
7
|
+
|
|
8
|
+
const validate = compile(apiSchema);
|
|
9
|
+
|
|
10
|
+
function checkApiRules(issues, apis) {
|
|
11
|
+
apis.forEach((api, index) => {
|
|
12
|
+
if (!api || typeof api !== "object") return;
|
|
13
|
+
const fields = api.fields || {};
|
|
14
|
+
for (const [field, definition] of Object.entries(fields)) {
|
|
15
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition) || typeof definition.name !== "string" || definition.name.length === 0) {
|
|
16
|
+
issues.push({ path: [index, "fields", field], code: "rule", message: "必须提供非空 name" });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
compile(getFieldSchema(definition));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
issues.push({ path: [index, "fields", field], code: "rule", message: error.message });
|
|
23
|
+
}
|
|
54
24
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
25
|
+
try {
|
|
26
|
+
compile({
|
|
27
|
+
paramType: "object",
|
|
28
|
+
fields: Object.fromEntries(Object.entries(fields).map(([field, definition]) => [field, { ...getFieldSchema(definition), optional: !(api.required || []).includes(field) }])),
|
|
29
|
+
constraints: api.constraints
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
issues.push({ path: [index, "constraints"], code: "rule", message: error.message });
|
|
33
|
+
}
|
|
34
|
+
for (const field of api.required || []) {
|
|
35
|
+
if (!Object.hasOwn(fields, field)) issues.push({ path: [index, "required"], code: "rule", message: `必填字段未在 fields 中声明: ${field}` });
|
|
36
|
+
else if (fields[field] && typeof fields[field] === "object" && Object.hasOwn(fields[field], "paramDefault")) issues.push({ path: [index, "required"], code: "rule", message: `必填字段不能同时配置 paramDefault: ${field}` });
|
|
37
|
+
}
|
|
38
|
+
if (api.body === "raw" && Object.keys(fields).length > 0) issues.push({ path: [index, "fields"], code: "rule", message: "body 为 raw 时不允许配置 fields" });
|
|
39
|
+
if (api.body === "raw" && (api.required || []).length > 0) issues.push({ path: [index, "required"], code: "rule", message: "body 为 raw 时不允许配置 required" });
|
|
40
|
+
});
|
|
60
41
|
}
|
|
61
42
|
|
|
62
43
|
export async function checkApi(apis) {
|
|
63
|
-
const issues =
|
|
64
|
-
if (
|
|
65
|
-
issues.push({ path: [], message: "必须是数组" });
|
|
66
|
-
} else {
|
|
67
|
-
apis.forEach((api, index) => issues.push(...checkApiItem(api, index)));
|
|
68
|
-
}
|
|
69
|
-
|
|
44
|
+
const issues = validate(apis).issues;
|
|
45
|
+
if (Array.isArray(apis)) checkApiRules(issues, apis);
|
|
70
46
|
if (issues.length === 0) return [];
|
|
71
|
-
|
|
47
|
+
|
|
48
|
+
const errors = formatIssues(issues, { items: apis, itemLabel: "api" });
|
|
72
49
|
Logger.warn("接口校验失败", { errors: errors });
|
|
73
50
|
return errors;
|
|
74
51
|
}
|
package/checks/config.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { Logger } from "
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { Logger } from "#befly/libs/logger/index.js";
|
|
2
|
+
import { compile } from "#befly/libs/validator/index.js";
|
|
3
|
+
|
|
4
|
+
import configSchema from "../schemas/config.json";
|
|
5
|
+
import { formatIssues } from "./validation.js";
|
|
6
|
+
|
|
7
|
+
const validate = compile(configSchema);
|
|
4
8
|
|
|
5
9
|
export async function checkConfig(config) {
|
|
6
|
-
const issues =
|
|
10
|
+
const issues = validate(config).issues;
|
|
11
|
+
if (typeof config?.tz === "string") {
|
|
12
|
+
try {
|
|
13
|
+
new Intl.DateTimeFormat("en-US", { timeZone: config.tz }).format(0);
|
|
14
|
+
} catch {
|
|
15
|
+
issues.push({ path: ["tz"], code: "rule", message: "必须是有效 IANA 时区" });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
7
18
|
if (issues.length === 0) return [];
|
|
8
19
|
|
|
9
|
-
const errors =
|
|
10
|
-
Logger.warn(
|
|
11
|
-
"配置校验失败",
|
|
12
|
-
{
|
|
13
|
-
errors: errors
|
|
14
|
-
},
|
|
15
|
-
false
|
|
16
|
-
);
|
|
20
|
+
const errors = formatIssues(issues, { item: config, itemLabel: "config" });
|
|
21
|
+
Logger.warn("配置校验失败", { errors: errors }, false);
|
|
17
22
|
return errors;
|
|
18
23
|
}
|
package/checks/corns.js
CHANGED
|
@@ -1,54 +1,36 @@
|
|
|
1
|
-
import { Logger } from "
|
|
2
|
-
import {
|
|
3
|
-
import { isFunction, isPlainObject, isValidCronSchedule } from "../utils/is.js";
|
|
1
|
+
import { Logger } from "#befly/libs/logger/index.js";
|
|
2
|
+
import { compile } from "#befly/libs/validator/index.js";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import cornSchema from "../schemas/corn.json";
|
|
5
|
+
import { formatIssues } from "./validation.js";
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
if (!Array.isArray(corns)) {
|
|
10
|
-
const errors = formatValidationIssues([{ path: [], message: "必须是数组" }], { item: corns, itemLabel: "corn" });
|
|
11
|
-
Logger.warn(`定时器校验失败`, { errors: errors });
|
|
12
|
-
return errors;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const errors = [];
|
|
16
|
-
|
|
17
|
-
// 任务名唯一性:文件名即任务名(core + app 合并后全局唯一,防止同名任务被 register 静默去重)
|
|
18
|
-
const nameMap = new Map();
|
|
19
|
-
for (const corn of corns) {
|
|
20
|
-
if (!corn?.fileName) continue;
|
|
21
|
-
nameMap.set(corn.fileName, (nameMap.get(corn.fileName) || 0) + 1);
|
|
22
|
-
}
|
|
23
|
-
const duplicateNames = [...nameMap].filter(([, count]) => count > 1).map(([name]) => name);
|
|
24
|
-
if (duplicateNames.length > 0) {
|
|
25
|
-
const duplicateCorns = corns.filter((corn) => duplicateNames.includes(corn.fileName));
|
|
26
|
-
errors.push(
|
|
27
|
-
...formatValidationIssues(
|
|
28
|
-
duplicateCorns.map((corn, index) => ({ path: [index], message: `任务名 ${corn.fileName} 重复,请显式区分` })),
|
|
29
|
-
{ items: duplicateCorns, itemLabel: "corn" }
|
|
30
|
-
)
|
|
31
|
-
);
|
|
32
|
-
}
|
|
7
|
+
const validate = compile(cornSchema);
|
|
33
8
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
9
|
+
function checkCornNames(issues, corns) {
|
|
10
|
+
const counts = new Map();
|
|
11
|
+
for (const corn of corns) if (corn?.fileName) counts.set(corn.fileName, (counts.get(corn.fileName) || 0) + 1);
|
|
12
|
+
corns.forEach((corn, index) => {
|
|
13
|
+
if (corn?.fileName && counts.get(corn.fileName) > 1) issues.push({ path: [index], message: `任务名 ${corn.fileName} 重复,请显式区分` });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
39
16
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
17
|
+
export const checkCorn = (corns) => {
|
|
18
|
+
const issues = validate(corns).issues;
|
|
19
|
+
if (Array.isArray(corns)) {
|
|
20
|
+
checkCornNames(issues, corns);
|
|
21
|
+
corns.forEach((corn, index) => {
|
|
22
|
+
if (typeof corn?.schedule !== "string") return;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = Bun.cron.parse(corn.schedule);
|
|
25
|
+
if (parsed === null) throw new TypeError("invalid cron");
|
|
26
|
+
} catch {
|
|
27
|
+
issues.push({ path: [index, "schedule"], code: "rule", message: "必须是可注册的 cron 表达式" });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
48
30
|
}
|
|
31
|
+
if (issues.length === 0) return [];
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
33
|
+
const errors = formatIssues(issues, { items: corns, itemLabel: "corn" });
|
|
34
|
+
Logger.warn("定时器校验失败", { errors: errors });
|
|
53
35
|
return errors;
|
|
54
36
|
};
|
package/checks/field.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const SCHEMA_KEYS = ["paramType", "anyOf", "optional", "paramDefault", "enum", "minValue", "maxValue", "minItem", "maxItem", "precision", "scale", "pattern", "unique", "uniqueBy", "items", "fields", "extra", "keys", "values", "constraints"];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 从 Core 字段定义中提取通用验证器协议,避免领域元数据进入验证器。
|
|
5
|
+
*/
|
|
6
|
+
export function getFieldSchema(field) {
|
|
7
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) return {};
|
|
8
|
+
return Object.fromEntries(SCHEMA_KEYS.filter((key) => field[key] !== undefined).map((key) => [key, field[key]]));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getFieldErrors(issues, fields) {
|
|
12
|
+
return Object.fromEntries(
|
|
13
|
+
issues.map((issue) => {
|
|
14
|
+
const field = issue.path[0];
|
|
15
|
+
return [field, `${fields[field]?.name || field}${issue.message}`];
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
}
|
package/checks/items.js
CHANGED
|
@@ -1,67 +1,23 @@
|
|
|
1
|
-
import { Logger } from "
|
|
2
|
-
import {
|
|
3
|
-
import { isFunction, isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
|
|
1
|
+
import { Logger } from "#befly/libs/logger/index.js";
|
|
2
|
+
import { compile } from "#befly/libs/validator/index.js";
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
import itemSchema from "../schemas/item.json";
|
|
5
|
+
import { formatIssues } from "./validation.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const issues = [];
|
|
9
|
-
if (!isPlainObject(item)) {
|
|
10
|
-
issues.push({ path: [], message: "必须是对象格式" });
|
|
11
|
-
return issues;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
for (const key of Object.keys(item)) {
|
|
15
|
-
if (!itemKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
|
|
16
|
-
}
|
|
17
|
-
for (const key of ["source", "type", "filePath", "relativePath", "fileName", "apiPath"]) {
|
|
18
|
-
if (!isString(item[key]) || item[key].length === 0 || item[key].trim() !== item[key]) issues.push({ path: [key], message: "必须是非空且无首尾空格的字符串" });
|
|
19
|
-
}
|
|
20
|
-
if (!isFunction(item.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
|
|
21
|
-
if (!isFiniteNumber(item.order)) issues.push({ path: ["order"], message: "必须是有限数字" });
|
|
22
|
-
// 非核心(app)插件/钩子的 order 必须大于 1000,避免与核心区间(10~90)撞号
|
|
23
|
-
if (item.source === "app" && isFiniteNumber(item.order) && item.order <= 1000) issues.push({ path: ["order"], message: "非核心插件/钩子 order 必须大于 1000" });
|
|
24
|
-
return issues;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function checkItemList(items, label, labelEn) {
|
|
28
|
-
if (!Array.isArray(items)) {
|
|
29
|
-
const errors = formatValidationIssues([{ path: [], message: "必须是数组" }], { item: items, itemLabel: labelEn });
|
|
30
|
-
Logger.warn(`${label}校验失败`, { errors: errors });
|
|
31
|
-
return errors;
|
|
32
|
-
}
|
|
7
|
+
const validate = compile(itemSchema);
|
|
33
8
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
orderMap.set(item.order, (orderMap.get(item.order) || 0) + 1);
|
|
41
|
-
}
|
|
42
|
-
const duplicateOrders = [...orderMap].filter(([, count]) => count > 1).map(([order]) => order);
|
|
43
|
-
if (duplicateOrders.length > 0) {
|
|
44
|
-
const duplicateItems = items.filter((item) => duplicateOrders.includes(item.order));
|
|
45
|
-
errors.push(
|
|
46
|
-
...formatValidationIssues(
|
|
47
|
-
duplicateItems.map((item, index) => ({ path: [index], message: `order ${item.order} 重复,请显式区分` })),
|
|
48
|
-
{ items: duplicateItems, itemLabel: labelEn }
|
|
49
|
-
)
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
for (const item of items) {
|
|
54
|
-
const issues = checkItem(item);
|
|
55
|
-
if (issues.length === 0) continue;
|
|
56
|
-
errors.push(...formatValidationIssues(issues, { item: item, itemLabel: labelEn }));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (errors.length > 0) {
|
|
60
|
-
Logger.warn(`${label}校验失败`, { errors: errors });
|
|
9
|
+
function checkItems(items, label, labelEn) {
|
|
10
|
+
const issues = validate(items).issues;
|
|
11
|
+
if (Array.isArray(items)) {
|
|
12
|
+
items.forEach((item, index) => {
|
|
13
|
+
if (item?.source === "app" && typeof item.order === "number" && item.order <= 1000) issues.push({ path: [index, "order"], code: "rule", message: "非核心插件/钩子 order 必须大于 1000" });
|
|
14
|
+
});
|
|
61
15
|
}
|
|
16
|
+
if (issues.length === 0) return [];
|
|
17
|
+
const errors = formatIssues(issues, { items: items, itemLabel: labelEn });
|
|
18
|
+
Logger.warn(`${label}校验失败`, { errors: errors });
|
|
62
19
|
return errors;
|
|
63
20
|
}
|
|
64
21
|
|
|
65
|
-
export const checkHook = (hooks) =>
|
|
66
|
-
|
|
67
|
-
export const checkPlugin = (plugins) => checkItemList(plugins, "插件", "plugin");
|
|
22
|
+
export const checkHook = (hooks) => checkItems(hooks, "钩子", "hook");
|
|
23
|
+
export const checkPlugin = (plugins) => checkItems(plugins, "插件", "plugin");
|