befly 3.72.0 → 3.74.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/checks/api.js +50 -107
- package/checks/config.js +5 -133
- package/checks/hook.js +28 -31
- package/checks/menu.js +36 -50
- package/checks/plugin.js +28 -31
- package/checks/table.js +25 -109
- package/configs/beflyConfig.json +1 -3
- package/hooks/rateLimit.js +25 -186
- package/hooks/validator.js +2 -2
- package/index.js +48 -2
- package/lib/connect.js +0 -6
- package/lib/dbHelper.js +14 -40
- package/lib/validator.js +208 -304
- package/package.json +3 -3
- package/plugins/tool.js +16 -0
- package/router/api.js +4 -0
- package/schemas/config.js +67 -0
- package/utils/cors.js +2 -2
- package/utils/formatValidationIssues.js +28 -0
- package/utils/is.js +1 -1
- package/utils/formatZodIssues.js +0 -98
package/checks/api.js
CHANGED
|
@@ -1,125 +1,68 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
|
-
import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
|
|
4
1
|
import { Logger } from "../lib/logger.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
z.config(z.locales.zhCN());
|
|
2
|
+
import { validateFieldRule } from "../lib/validator.js";
|
|
3
|
+
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
4
|
+
import { isBoolean, isFunction, isPlainObject, isString } from "../utils/is.js";
|
|
9
5
|
|
|
10
|
-
const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
|
|
11
6
|
const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
12
7
|
const apiPathRegex = /^\/api\/[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*$/;
|
|
8
|
+
const apiKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "name", "handler", "apiPath", "method", "body", "auth", "fields", "required"]);
|
|
13
9
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
path: path,
|
|
17
|
-
|
|
18
|
-
}
|
|
10
|
+
function checkNoTrimString(issues, value, path, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
11
|
+
if (!isString(value)) {
|
|
12
|
+
issues.push({ path: path, message: "必须是字符串" });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (value !== "" && value.trim() !== value) issues.push({ path: path, message: "不允许首尾空格" });
|
|
16
|
+
if (value.length < min) issues.push({ path: path, message: `长度不能少于${min}个字符` });
|
|
17
|
+
if (value.length > max) issues.push({ path: path, message: `长度不能超过${max}个字符` });
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
max: z.number().nullable().optional(),
|
|
28
|
-
detail: z.string().optional()
|
|
29
|
-
})
|
|
30
|
-
.strict()
|
|
31
|
-
.superRefine((value, context) => {
|
|
32
|
-
const min = isNullable(value.min) ? FIELD_RULE_DEFAULT_MIN : value.min;
|
|
33
|
-
const max = isNullable(value.max) ? FIELD_RULE_DEFAULT_MAX : value.max;
|
|
34
|
-
if (min > max) {
|
|
35
|
-
addIssue(context, ["min"], "min 不能大于 max");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const checkValue = value.check;
|
|
39
|
-
const input = value.input;
|
|
40
|
-
|
|
41
|
-
if ((input === "enum" || input === "enumNumber" || input === "enumInteger" || input === "regexp") && (!checkValue || checkValue.length === 0)) {
|
|
42
|
-
addIssue(context, ["check"], `${input} 必须提供 check`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (input === "regexp" && checkValue && !isRegexInput(checkValue)) {
|
|
47
|
-
addIssue(context, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if ((input === "enum" || input === "enumNumber" || input === "enumInteger") && checkValue) {
|
|
52
|
-
const items = String(checkValue)
|
|
53
|
-
.split("|")
|
|
54
|
-
.map((item) => item.trim())
|
|
55
|
-
.filter((item) => item.length > 0);
|
|
56
|
-
|
|
57
|
-
if (items.length === 0) {
|
|
58
|
-
addIssue(context, ["check"], `${input}.check 必须是非空枚举值列表`);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (input === "enumNumber" || input === "enumInteger") {
|
|
63
|
-
for (const item of items) {
|
|
64
|
-
const num = Number(item);
|
|
65
|
-
if (!Number.isFinite(num)) {
|
|
66
|
-
addIssue(context, ["check"], `${input}.check 存在非法数字值: ${item}`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (input === "enumInteger" && !Number.isInteger(num)) {
|
|
70
|
-
addIssue(context, ["check"], `${input}.check 存在非法整数值: ${item}`);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const fieldsSchema = z.record(z.string().regex(lowerCamelRegex), fieldSchema);
|
|
20
|
+
function checkApiItem(api, index) {
|
|
21
|
+
const issues = [];
|
|
22
|
+
if (!isPlainObject(api)) {
|
|
23
|
+
issues.push({ path: [index], message: "必须是对象格式" });
|
|
24
|
+
return issues;
|
|
25
|
+
}
|
|
79
26
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
required: z.array(noTrimString)
|
|
95
|
-
})
|
|
96
|
-
.strict()
|
|
97
|
-
.superRefine((value, context) => {
|
|
98
|
-
if (value.source !== "app") {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
27
|
+
for (const key of Object.keys(api)) {
|
|
28
|
+
if (!apiKeys.has(key)) issues.push({ path: [index, key], message: "不允许出现" });
|
|
29
|
+
}
|
|
30
|
+
for (const key of ["source", "type", "filePath", "relativePath"]) checkNoTrimString(issues, api[key], [index, key], 1);
|
|
31
|
+
checkNoTrimString(issues, api.name, [index, "name"], 1, 100);
|
|
32
|
+
if (!isString(api.fileName) || !lowerCamelRegex.test(api.fileName)) issues.push({ path: [index, "fileName"], message: "必须是 lowerCamelCase" });
|
|
33
|
+
if (!isFunction(api.handler)) issues.push({ path: [index, "handler"], message: "必须是函数" });
|
|
34
|
+
if (!isString(api.apiPath) || !apiPathRegex.test(api.apiPath)) issues.push({ path: [index, "apiPath"], message: "路径格式无效" });
|
|
35
|
+
if (!["GET", "POST", "ALL"].includes(api.method)) issues.push({ path: [index, "method"], message: "仅允许 GET|POST|ALL" });
|
|
36
|
+
if (!["none", "raw"].includes(api.body)) issues.push({ path: [index, "body"], message: "仅允许 none|raw" });
|
|
37
|
+
|
|
38
|
+
if (!isBoolean(api.auth) && (!Array.isArray(api.auth) || api.auth.length === 0 || api.auth.some((item) => !isString(item) || (item !== "" && item.trim() !== item)))) {
|
|
39
|
+
issues.push({ path: [index, "auth"], message: "必须是 boolean 或非空字符串数组" });
|
|
40
|
+
}
|
|
101
41
|
|
|
102
|
-
|
|
103
|
-
|
|
42
|
+
if (!isPlainObject(api.fields)) {
|
|
43
|
+
issues.push({ path: [index, "fields"], message: "必须是对象格式" });
|
|
44
|
+
} else {
|
|
45
|
+
for (const [fieldName, rule] of Object.entries(api.fields)) {
|
|
46
|
+
if (!lowerCamelRegex.test(fieldName)) issues.push({ path: [index, "fields", fieldName], message: "字段名必须为 lowerCamelCase" });
|
|
47
|
+
for (const issue of validateFieldRule(rule)) issues.push({ path: [index, "fields", fieldName, ...issue.path], message: issue.message });
|
|
104
48
|
}
|
|
49
|
+
}
|
|
105
50
|
|
|
106
|
-
|
|
107
|
-
});
|
|
51
|
+
if (!Array.isArray(api.required) || api.required.some((item) => !isString(item) || (item !== "" && item.trim() !== item))) issues.push({ path: [index, "required"], message: "必须是字符串数组" });
|
|
52
|
+
if (api.source === "app" && isString(api.relativePath) && api.relativePath.split("/")[0] === "core") issues.push({ path: [index, "relativePath"], message: "app API 不能使用 core 作为一级目录,避免占用 /api/core 命名空间" });
|
|
108
53
|
|
|
109
|
-
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
110
56
|
|
|
111
57
|
export async function checkApi(apis) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
|
|
58
|
+
const issues = [];
|
|
59
|
+
if (!Array.isArray(apis)) {
|
|
60
|
+
issues.push({ path: [], message: "必须是数组" });
|
|
61
|
+
} else {
|
|
62
|
+
apis.forEach((api, index) => issues.push(...checkApiItem(api, index)));
|
|
115
63
|
}
|
|
116
64
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
errors: formatZodIssues(result.error.issues, { items: apis, itemLabel: "api" })
|
|
121
|
-
},
|
|
122
|
-
false
|
|
123
|
-
);
|
|
65
|
+
if (issues.length === 0) return false;
|
|
66
|
+
Logger.warn("接口校验失败", { errors: formatValidationIssues(issues, { items: apis, itemLabel: "api" }) }, false);
|
|
124
67
|
return true;
|
|
125
68
|
}
|
package/checks/config.js
CHANGED
|
@@ -1,143 +1,15 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
|
-
import { RUN_MODE_VALUES } from "../configs/constConfig.js";
|
|
4
1
|
import { Logger } from "../lib/logger.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
z.config(z.locales.zhCN());
|
|
9
|
-
|
|
10
|
-
const boolIntSchema = z.union([z.literal(0), z.literal(1), z.literal(true), z.literal(false)]);
|
|
11
|
-
const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
|
|
12
|
-
const beflyModeSchema = z.union([z.literal("manual"), z.literal("auto")]);
|
|
13
|
-
const uploadExtensionListSchema = noTrimString.regex(/^\.[a-z0-9]+(?:,\.[a-z0-9]+)*$/);
|
|
14
|
-
const uploadMimeTypeListSchema = noTrimString.regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*(?:,[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*)*$/);
|
|
15
|
-
const configSchema = z
|
|
16
|
-
.object({
|
|
17
|
-
runMode: z.enum(RUN_MODE_VALUES),
|
|
18
|
-
appName: noTrimString,
|
|
19
|
-
appPort: z.int().min(1).max(65535),
|
|
20
|
-
appHost: noTrimString,
|
|
21
|
-
apiHost: noTrimString,
|
|
22
|
-
devEmail: z.union([z.literal(""), z.email()]),
|
|
23
|
-
devPassword: z.string().min(6),
|
|
24
|
-
bodyLimit: z.int().min(1),
|
|
25
|
-
upload: z
|
|
26
|
-
.object({
|
|
27
|
-
maxSize: z.int().min(1),
|
|
28
|
-
publicDir: noTrimString.min(1),
|
|
29
|
-
publicHost: noTrimString,
|
|
30
|
-
allowedExtensions: uploadExtensionListSchema,
|
|
31
|
-
allowedMimeTypes: uploadMimeTypeListSchema,
|
|
32
|
-
forceDownloadExtensions: z.union([z.literal(""), uploadExtensionListSchema])
|
|
33
|
-
})
|
|
34
|
-
.strict(),
|
|
35
|
-
tz: z.string().refine((value) => isValidTimeZone(value), "无效的时区"),
|
|
36
|
-
excludeApisLog: z.array(noTrimString),
|
|
37
|
-
|
|
38
|
-
logger: z
|
|
39
|
-
.object({
|
|
40
|
-
debug: boolIntSchema,
|
|
41
|
-
excludeFields: z.array(noTrimString)
|
|
42
|
-
})
|
|
43
|
-
.strict(),
|
|
44
|
-
|
|
45
|
-
mysql: z
|
|
46
|
-
.object({
|
|
47
|
-
hostname: noTrimString,
|
|
48
|
-
port: z.int().min(1).max(65535),
|
|
49
|
-
username: noTrimString,
|
|
50
|
-
password: noTrimString,
|
|
51
|
-
database: noTrimString,
|
|
52
|
-
max: z.number().min(1),
|
|
53
|
-
beflyMode: beflyModeSchema.optional(),
|
|
54
|
-
idleTimeout: z.number().min(1).optional(),
|
|
55
|
-
maxLifetime: z.number().min(0).optional(),
|
|
56
|
-
connectionTimeout: z.number().min(1).optional()
|
|
57
|
-
})
|
|
58
|
-
.strict(),
|
|
59
|
-
|
|
60
|
-
redis: z
|
|
61
|
-
.object({
|
|
62
|
-
hostname: noTrimString,
|
|
63
|
-
port: z.int().min(1).max(65535),
|
|
64
|
-
username: noTrimString,
|
|
65
|
-
password: noTrimString,
|
|
66
|
-
db: z.int().min(0).max(16),
|
|
67
|
-
prefix: noTrimString.regex(/^[^:]*$/)
|
|
68
|
-
})
|
|
69
|
-
.strict(),
|
|
70
|
-
|
|
71
|
-
session: z
|
|
72
|
-
.object({
|
|
73
|
-
expireDays: z.int().min(1)
|
|
74
|
-
})
|
|
75
|
-
.strict(),
|
|
76
|
-
|
|
77
|
-
email: z
|
|
78
|
-
.object({
|
|
79
|
-
host: noTrimString,
|
|
80
|
-
port: z.int().min(1).max(65535),
|
|
81
|
-
secure: z.union([z.literal(0), z.literal(1), z.literal(true), z.literal(false)]),
|
|
82
|
-
user: noTrimString,
|
|
83
|
-
pass: noTrimString,
|
|
84
|
-
label: noTrimString.optional()
|
|
85
|
-
})
|
|
86
|
-
.strict()
|
|
87
|
-
.optional(),
|
|
88
|
-
|
|
89
|
-
cors: z
|
|
90
|
-
.object({
|
|
91
|
-
origin: noTrimString,
|
|
92
|
-
methods: noTrimString,
|
|
93
|
-
allowedHeaders: noTrimString,
|
|
94
|
-
exposedHeaders: noTrimString,
|
|
95
|
-
maxAge: z.int().min(0),
|
|
96
|
-
credentials: z.union([z.literal("true"), z.literal("false"), z.literal(true), z.literal(false)])
|
|
97
|
-
})
|
|
98
|
-
.strict()
|
|
99
|
-
.superRefine((cors, ctx) => {
|
|
100
|
-
if (cors.origin !== "*" || (cors.credentials !== true && cors.credentials !== "true")) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
ctx.addIssue({
|
|
105
|
-
code: "custom",
|
|
106
|
-
message: "cors.credentials 为 true 时 cors.origin 不能为 *",
|
|
107
|
-
path: ["credentials"]
|
|
108
|
-
});
|
|
109
|
-
}),
|
|
110
|
-
|
|
111
|
-
rateLimit: z
|
|
112
|
-
.object({
|
|
113
|
-
enable: boolIntSchema,
|
|
114
|
-
defaultLimit: z.int().min(1),
|
|
115
|
-
defaultWindow: z.int().min(1),
|
|
116
|
-
key: noTrimString,
|
|
117
|
-
skipRoutes: z.array(noTrimString),
|
|
118
|
-
rules: z.array(z.object({}).passthrough())
|
|
119
|
-
})
|
|
120
|
-
.strict()
|
|
121
|
-
})
|
|
122
|
-
.strict();
|
|
2
|
+
import { validateConfig } from "../lib/validator.js";
|
|
3
|
+
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
123
4
|
|
|
124
|
-
/**
|
|
125
|
-
* 配置检查(启动期强校验)
|
|
126
|
-
*
|
|
127
|
-
* 说明:
|
|
128
|
-
* - checkTable/checkApi 等校验的是“扫描到的源码/表定义”。
|
|
129
|
-
* - checkConfig 校验的是“最终合并后的运行时配置对象”,用于阻断错误配置带病启动。
|
|
130
|
-
*/
|
|
131
5
|
export async function checkConfig(config) {
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
6
|
+
const issues = validateConfig(config);
|
|
7
|
+
if (issues.length === 0) return false;
|
|
136
8
|
|
|
137
9
|
Logger.warn(
|
|
138
10
|
"配置校验失败",
|
|
139
11
|
{
|
|
140
|
-
errors:
|
|
12
|
+
errors: formatValidationIssues(issues, { item: config, itemLabel: "config" })
|
|
141
13
|
},
|
|
142
14
|
false
|
|
143
15
|
);
|
package/checks/hook.js
CHANGED
|
@@ -1,42 +1,39 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
1
|
import { Logger } from "../lib/logger.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
3
|
+
import { isFiniteNumber, isFunction, isPlainObject, isString } from "../utils/is.js";
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
const hookKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
function checkHookItem(hook) {
|
|
8
|
+
const issues = [];
|
|
9
|
+
if (!isPlainObject(hook)) {
|
|
10
|
+
issues.push({ path: [], message: "必须是对象格式" });
|
|
11
|
+
return issues;
|
|
12
|
+
}
|
|
10
13
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
22
|
-
.strict();
|
|
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
|
+
}
|
|
23
24
|
|
|
24
25
|
export async function checkHook(hooks) {
|
|
25
|
-
|
|
26
|
+
if (!Array.isArray(hooks)) {
|
|
27
|
+
Logger.warn("钩子校验失败", { errors: formatValidationIssues([{ path: [], message: "必须是数组" }], { item: hooks, itemLabel: "hook" }) }, false);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
26
30
|
|
|
31
|
+
let hasError = false;
|
|
27
32
|
for (const hook of hooks) {
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
errors: formatZodIssues(result.error.issues, { item: hook, itemLabel: "hook" })
|
|
34
|
-
},
|
|
35
|
-
false
|
|
36
|
-
);
|
|
37
|
-
hasError = true;
|
|
38
|
-
}
|
|
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;
|
|
39
37
|
}
|
|
40
|
-
|
|
41
38
|
return hasError;
|
|
42
39
|
}
|
package/checks/menu.js
CHANGED
|
@@ -1,62 +1,48 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
1
|
import { Logger } from "../lib/logger.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
z.config(z.locales.zhCN());
|
|
2
|
+
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
3
|
+
import { isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
|
|
7
4
|
|
|
8
5
|
const menuPathSegmentRegexSource = "[a-z][a-z0-9]*(?:-[a-z0-9]+)*";
|
|
9
6
|
const fullMenuPathRegex = new RegExp(`^/(?:core(?:/${menuPathSegmentRegexSource})*|(?:${menuPathSegmentRegexSource}(?:/${menuPathSegmentRegexSource})*)?)$`);
|
|
10
7
|
const childMenuPathRegex = new RegExp(`^/${menuPathSegmentRegexSource}$`);
|
|
8
|
+
const menuKeys = new Set(["path", "name", "sort", "children"]);
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
path:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
children
|
|
27
|
-
|
|
28
|
-
|
|
10
|
+
function checkMenuItem(issues, menu, path, child = false) {
|
|
11
|
+
if (!isPlainObject(menu)) {
|
|
12
|
+
issues.push({ path: path, message: "必须是对象格式" });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const key of Object.keys(menu)) {
|
|
16
|
+
if (!menuKeys.has(key)) issues.push({ path: [...path, key], message: "不允许出现" });
|
|
17
|
+
}
|
|
18
|
+
const pathRegex = child ? childMenuPathRegex : fullMenuPathRegex;
|
|
19
|
+
if (!isString(menu.path) || !pathRegex.test(menu.path)) issues.push({ path: [...path, "path"], message: "路径格式无效" });
|
|
20
|
+
if (!isString(menu.name) || menu.name.trim() !== menu.name || menu.name.length === 0) issues.push({ path: [...path, "name"], message: "必须是非空且无首尾空格的字符串" });
|
|
21
|
+
if (!isFiniteNumber(menu.sort)) issues.push({ path: [...path, "sort"], message: "必须是有限数字" });
|
|
22
|
+
|
|
23
|
+
if (child) {
|
|
24
|
+
if (menu.children !== undefined && (!Array.isArray(menu.children) || menu.children.length > 0)) issues.push({ path: [...path, "children"], message: "只能为空数组" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
refineCtx.addIssue({
|
|
39
|
-
code: "custom",
|
|
40
|
-
message: "菜单完整路径必须是 /core/xxx 或无前缀路径",
|
|
41
|
-
path: [menuIndex, "children", childIndex, "path"]
|
|
42
|
-
});
|
|
43
|
-
}
|
|
28
|
+
if (menu.children !== undefined && !Array.isArray(menu.children)) {
|
|
29
|
+
issues.push({ path: [...path, "children"], message: "必须是数组" });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const [index, item] of (menu.children || []).entries()) {
|
|
33
|
+
checkMenuItem(issues, item, [...path, "children", index], true);
|
|
34
|
+
if (isPlainObject(item) && isString(menu.path) && isString(item.path) && !fullMenuPathRegex.test(`${menu.path}${item.path}`)) {
|
|
35
|
+
issues.push({ path: [...path, "children", index, "path"], message: "菜单完整路径必须是 /core/xxx 或无前缀路径" });
|
|
44
36
|
}
|
|
45
37
|
}
|
|
46
|
-
}
|
|
38
|
+
}
|
|
47
39
|
|
|
48
|
-
export
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
}
|
|
40
|
+
export async function checkMenu(menus) {
|
|
41
|
+
const issues = [];
|
|
42
|
+
if (!Array.isArray(menus)) issues.push({ path: [], message: "必须是数组" });
|
|
43
|
+
else menus.forEach((menu, index) => checkMenuItem(issues, menu, [index]));
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
errors: formatZodIssues(result.error.issues, { items: menus, itemLabel: "menus" })
|
|
58
|
-
},
|
|
59
|
-
false
|
|
60
|
-
);
|
|
45
|
+
if (issues.length === 0) return false;
|
|
46
|
+
Logger.warn("菜单校验失败", { errors: formatValidationIssues(issues, { items: menus, itemLabel: "menus" }) }, false);
|
|
61
47
|
return true;
|
|
62
|
-
}
|
|
48
|
+
}
|
package/checks/plugin.js
CHANGED
|
@@ -1,42 +1,39 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
1
|
import { Logger } from "../lib/logger.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
3
|
+
import { isFunction, isFiniteNumber, isPlainObject, isString } from "../utils/is.js";
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
const pluginKeys = new Set(["source", "type", "filePath", "relativePath", "fileName", "apiPath", "handler", "order"]);
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
function checkPluginItem(plugin) {
|
|
8
|
+
const issues = [];
|
|
9
|
+
if (!isPlainObject(plugin)) {
|
|
10
|
+
issues.push({ path: [], message: "必须是对象格式" });
|
|
11
|
+
return issues;
|
|
12
|
+
}
|
|
10
13
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
22
|
-
.strict();
|
|
14
|
+
for (const key of Object.keys(plugin)) {
|
|
15
|
+
if (!pluginKeys.has(key)) issues.push({ path: [key], message: "不允许出现" });
|
|
16
|
+
}
|
|
17
|
+
for (const key of ["source", "type", "filePath", "relativePath", "fileName", "apiPath"]) {
|
|
18
|
+
if (!isString(plugin[key]) || plugin[key].length === 0 || plugin[key].trim() !== plugin[key]) issues.push({ path: [key], message: "必须是非空且无首尾空格的字符串" });
|
|
19
|
+
}
|
|
20
|
+
if (!isFunction(plugin.handler)) issues.push({ path: ["handler"], message: "必须是函数" });
|
|
21
|
+
if (!isFiniteNumber(plugin.order)) issues.push({ path: ["order"], message: "必须是有限数字" });
|
|
22
|
+
return issues;
|
|
23
|
+
}
|
|
23
24
|
|
|
24
25
|
export async function checkPlugin(plugins) {
|
|
25
|
-
|
|
26
|
+
if (!Array.isArray(plugins)) {
|
|
27
|
+
Logger.warn("插件校验失败", { errors: formatValidationIssues([{ path: [], message: "必须是数组" }], { item: plugins, itemLabel: "plugin" }) }, false);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
26
30
|
|
|
31
|
+
let hasError = false;
|
|
27
32
|
for (const plugin of plugins) {
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
errors: formatZodIssues(result.error.issues, { item: plugin, itemLabel: "plugin" })
|
|
34
|
-
},
|
|
35
|
-
false
|
|
36
|
-
);
|
|
37
|
-
hasError = true;
|
|
38
|
-
}
|
|
33
|
+
const issues = checkPluginItem(plugin);
|
|
34
|
+
if (issues.length === 0) continue;
|
|
35
|
+
Logger.warn("插件校验失败", { errors: formatValidationIssues(issues, { item: plugin, itemLabel: "plugin" }) }, false);
|
|
36
|
+
hasError = true;
|
|
39
37
|
}
|
|
40
|
-
|
|
41
38
|
return hasError;
|
|
42
39
|
}
|