befly 3.12.3 → 3.13.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/dist/befly.config.js +1 -1
- package/dist/befly.js +810 -896
- package/dist/befly.min.js +15 -18
- package/dist/checks/checkApi.js +14 -17
- package/dist/checks/checkHook.js +55 -24
- package/dist/checks/checkMenu.js +10 -10
- package/dist/checks/checkPlugin.js +55 -24
- package/dist/checks/checkTable.js +29 -28
- package/dist/hooks/auth.d.ts +3 -7
- package/dist/hooks/auth.js +2 -1
- package/dist/hooks/cors.d.ts +3 -7
- package/dist/hooks/cors.js +3 -2
- package/dist/hooks/parser.d.ts +3 -7
- package/dist/hooks/parser.js +2 -1
- package/dist/hooks/permission.d.ts +3 -7
- package/dist/hooks/permission.js +5 -3
- package/dist/hooks/validator.d.ts +3 -7
- package/dist/hooks/validator.js +2 -1
- package/dist/index.js +2 -2
- package/dist/lib/cacheHelper.js +8 -8
- package/dist/lib/connect.js +5 -5
- package/dist/lib/dbHelper.js +6 -5
- package/dist/lib/logger.d.ts +16 -17
- package/dist/lib/logger.js +335 -749
- package/dist/lib/redisHelper.js +27 -26
- package/dist/loader/loadApis.js +1 -1
- package/dist/loader/loadPlugins.js +1 -1
- package/dist/plugins/cache.d.ts +3 -9
- package/dist/plugins/cache.js +2 -1
- package/dist/plugins/cipher.d.ts +3 -8
- package/dist/plugins/cipher.js +2 -1
- package/dist/plugins/config.d.ts +3 -12
- package/dist/plugins/config.js +2 -1
- package/dist/plugins/db.d.ts +3 -9
- package/dist/plugins/db.js +3 -2
- package/dist/plugins/jwt.d.ts +3 -9
- package/dist/plugins/jwt.js +2 -1
- package/dist/plugins/logger.d.ts +3 -25
- package/dist/plugins/logger.js +2 -1
- package/dist/plugins/redis.d.ts +3 -9
- package/dist/plugins/redis.js +3 -2
- package/dist/plugins/tool.d.ts +3 -11
- package/dist/plugins/tool.js +2 -1
- package/dist/router/api.js +3 -2
- package/dist/router/static.js +1 -1
- package/dist/sync/syncApi.js +3 -3
- package/dist/sync/syncMenu.js +3 -2
- package/dist/sync/syncTable.js +2 -2
- package/dist/types/hook.d.ts +13 -0
- package/dist/types/hook.js +13 -0
- package/dist/types/logger.d.ts +20 -6
- package/dist/types/plugin.d.ts +12 -1
- package/dist/types/plugin.js +12 -1
- package/dist/utils/formatYmdHms.d.ts +1 -0
- package/dist/utils/formatYmdHms.js +20 -0
- package/dist/utils/importDefault.js +1 -1
- package/dist/utils/loadMenuConfigs.js +7 -6
- package/dist/utils/loggerUtils.d.ts +18 -0
- package/dist/utils/loggerUtils.js +167 -0
- package/dist/utils/response.js +6 -4
- package/dist/utils/scanCoreBuiltins.js +4 -1
- package/dist/utils/scanFiles.d.ts +2 -0
- package/dist/utils/scanFiles.js +5 -2
- package/dist/utils/sortModules.js +8 -7
- package/dist/utils/util.d.ts +2 -0
- package/dist/utils/util.js +16 -0
- package/package.json +2 -2
package/dist/checks/checkApi.js
CHANGED
|
@@ -5,70 +5,67 @@ export async function checkApi(apis) {
|
|
|
5
5
|
for (const api of apis) {
|
|
6
6
|
try {
|
|
7
7
|
if (typeof api?.name !== "string" || api.name.trim() === "") {
|
|
8
|
-
Logger.warn(omit(api, ["handler"]), "接口的 name 属性必须是非空字符串");
|
|
8
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 name 属性必须是非空字符串" }));
|
|
9
9
|
hasError = true;
|
|
10
10
|
continue;
|
|
11
11
|
}
|
|
12
12
|
if (typeof api?.handler !== "function") {
|
|
13
|
-
Logger.warn(omit(api, ["handler"]), "接口的 handler 属性必须是函数");
|
|
13
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 handler 属性必须是函数" }));
|
|
14
14
|
hasError = true;
|
|
15
15
|
continue;
|
|
16
16
|
}
|
|
17
17
|
// routePath / routePrefix 由 scanFiles 系统生成:必须是严格的 pathname
|
|
18
18
|
if (typeof api?.routePath !== "string" || api.routePath.trim() === "") {
|
|
19
|
-
Logger.warn(omit(api, ["handler"]), "接口的 routePath 属性必须是非空字符串(由系统生成)");
|
|
19
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 属性必须是非空字符串(由系统生成)" }));
|
|
20
20
|
hasError = true;
|
|
21
21
|
}
|
|
22
22
|
else {
|
|
23
23
|
const routePath = api.routePath.trim();
|
|
24
24
|
// 不允许出现 "POST/api/..." 等 method 前缀
|
|
25
25
|
if (/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/i.test(routePath)) {
|
|
26
|
-
Logger.warn(omit(api, ["handler"]), "接口的 routePath 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)");
|
|
26
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)" }));
|
|
27
27
|
hasError = true;
|
|
28
28
|
}
|
|
29
29
|
if (!routePath.startsWith("/api/")) {
|
|
30
|
-
Logger.warn(omit(api, ["handler"]), "接口的 routePath 必须以 /api/ 开头");
|
|
30
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 必须以 /api/ 开头" }));
|
|
31
31
|
hasError = true;
|
|
32
32
|
}
|
|
33
33
|
if (routePath.includes(" ")) {
|
|
34
|
-
Logger.warn(omit(api, ["handler"]), "接口的 routePath 不允许包含空格");
|
|
34
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许包含空格" }));
|
|
35
35
|
hasError = true;
|
|
36
36
|
}
|
|
37
37
|
if (routePath.includes("/api//")) {
|
|
38
|
-
Logger.warn(omit(api, ["handler"]), "接口的 routePath 不允许出现 /api//(重复斜杠)");
|
|
38
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePath 不允许出现 /api//(重复斜杠)" }));
|
|
39
39
|
hasError = true;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
if (typeof api?.routePrefix !== "string" || api.routePrefix.trim() === "") {
|
|
43
|
-
Logger.warn(omit(api, ["handler"]), "接口的 routePrefix 属性必须是非空字符串(由系统生成)");
|
|
43
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 routePrefix 属性必须是非空字符串(由系统生成)" }));
|
|
44
44
|
hasError = true;
|
|
45
45
|
}
|
|
46
46
|
if (api.method && !["GET", "POST", "GET,POST", "POST,GET"].includes(String(api.method).toUpperCase())) {
|
|
47
|
-
Logger.warn(omit(api, ["handler"]), "接口的 method 属性必须是有效的 HTTP 方法 (GET, POST, GET,POST, POST,GET)");
|
|
47
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 method 属性必须是有效的 HTTP 方法 (GET, POST, GET,POST, POST,GET)" }));
|
|
48
48
|
hasError = true;
|
|
49
49
|
}
|
|
50
50
|
if (api.auth !== undefined && typeof api.auth !== "boolean") {
|
|
51
|
-
Logger.warn(omit(api, ["handler"]), "接口的 auth 属性必须是布尔值 (true=需登录, false=公开)");
|
|
51
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 auth 属性必须是布尔值 (true=需登录, false=公开)" }));
|
|
52
52
|
hasError = true;
|
|
53
53
|
}
|
|
54
54
|
if (api.fields && !isPlainObject(api.fields)) {
|
|
55
|
-
Logger.warn(omit(api, ["handler"]), "接口的 fields 属性必须是对象");
|
|
55
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 fields 属性必须是对象" }));
|
|
56
56
|
hasError = true;
|
|
57
57
|
}
|
|
58
58
|
if (api.required && !Array.isArray(api.required)) {
|
|
59
|
-
Logger.warn(omit(api, ["handler"]), "接口的 required 属性必须是数组");
|
|
59
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 required 属性必须是数组" }));
|
|
60
60
|
hasError = true;
|
|
61
61
|
}
|
|
62
62
|
if (api.required && api.required.some((reqItem) => typeof reqItem !== "string")) {
|
|
63
|
-
Logger.warn(omit(api, ["handler"]), "接口的 required 属性必须是字符串数组");
|
|
63
|
+
Logger.warn(Object.assign({}, omit(api, ["handler"]), { msg: "接口的 required 属性必须是字符串数组" }));
|
|
64
64
|
hasError = true;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
catch (error) {
|
|
68
|
-
Logger.error({
|
|
69
|
-
err: error,
|
|
70
|
-
item: api
|
|
71
|
-
}, "接口解析失败");
|
|
68
|
+
Logger.error({ err: error, item: api, msg: "接口解析失败" });
|
|
72
69
|
hasError = true;
|
|
73
70
|
}
|
|
74
71
|
}
|
package/dist/checks/checkHook.js
CHANGED
|
@@ -1,82 +1,113 @@
|
|
|
1
1
|
import { Logger } from "../lib/logger";
|
|
2
2
|
import { isPlainObject, omit } from "../utils/util";
|
|
3
|
+
const exportKeys = ["name", "enable", "deps", "handler"];
|
|
3
4
|
export async function checkHook(hooks) {
|
|
4
5
|
let hasError = false;
|
|
6
|
+
// 说明:hooks 实际是 scanFiles/scanSources 的结果对象(包含元信息字段)。
|
|
7
|
+
// 这里不再对白名单枚举 metaKeys(因为它们是系统生成的),只校验“用户 default export 导出的字段”。
|
|
5
8
|
const coreBuiltinNameRegexp = /^[a-z]+(?:_[a-z]+)*$/;
|
|
6
9
|
for (const hook of hooks) {
|
|
7
10
|
try {
|
|
8
11
|
if (!isPlainObject(hook)) {
|
|
9
|
-
Logger.warn(omit(hook, ["handler"]), "钩子导出必须是对象(export default { deps, handler })");
|
|
12
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子导出必须是对象(export default { deps, handler })" }));
|
|
10
13
|
hasError = true;
|
|
11
14
|
continue;
|
|
12
15
|
}
|
|
13
16
|
// moduleName 必须存在(用于依赖排序与运行时挂载)。
|
|
14
17
|
if (typeof hook.moduleName !== "string" || hook.moduleName.trim() === "") {
|
|
15
|
-
Logger.warn(omit(hook, ["handler"]), "钩子的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)");
|
|
18
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)" }));
|
|
16
19
|
hasError = true;
|
|
17
20
|
continue;
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
if (!
|
|
21
|
-
Logger.warn(omit(hook, ["handler"]),
|
|
22
|
+
const customKeys = hook.customKeys;
|
|
23
|
+
if (!Array.isArray(customKeys)) {
|
|
24
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子扫描结果缺少 customKeys(无法判断用户导出的字段是否合法)" }));
|
|
22
25
|
hasError = true;
|
|
23
26
|
continue;
|
|
24
27
|
}
|
|
25
|
-
if (typeof
|
|
26
|
-
Logger.warn(omit(hook, ["handler"]), "钩子的
|
|
28
|
+
if (customKeys.some((k) => typeof k !== "string")) {
|
|
29
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 customKeys 必须是 string[](由系统生成)" }));
|
|
27
30
|
hasError = true;
|
|
28
31
|
continue;
|
|
29
32
|
}
|
|
33
|
+
// 严格字段校验:仅检查用户 default export 的字段集合,出现任何未支持字段都应视为错误。
|
|
34
|
+
const unknownCustomKeys = customKeys.filter((k) => !exportKeys.includes(k));
|
|
35
|
+
if (unknownCustomKeys.length > 0) {
|
|
36
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: `钩子导出存在不支持的属性:${unknownCustomKeys.join(", ")};仅允许:${exportKeys.join(", ")};当前 customKeys:${customKeys.join(", ")}` }));
|
|
37
|
+
hasError = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const hasCustomEnable = customKeys.includes("enable");
|
|
41
|
+
const hasCustomDeps = customKeys.includes("deps");
|
|
42
|
+
// enable 必须显式声明且只能为 boolean(true/false),不允许 0/1 等其他类型。
|
|
43
|
+
// - 允许缺省:由系统在此处补全默认值 true
|
|
44
|
+
// - 若用户显式导出 enable:必须是 boolean
|
|
45
|
+
if (hasCustomEnable) {
|
|
46
|
+
if (typeof hook.enable !== "boolean") {
|
|
47
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 enable 属性必须是 boolean(true/false),不允许 0/1 等其他类型" }));
|
|
48
|
+
hasError = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
hook.enable = true;
|
|
54
|
+
}
|
|
30
55
|
// core 内置钩子:必须来自静态注册(filePath 以 core:hook: 开头),且 name 必须显式指定并与 moduleName 一致。
|
|
31
56
|
if (hook.source === "core") {
|
|
32
57
|
const name = typeof hook.name === "string" ? hook.name : "";
|
|
33
58
|
if (name === "") {
|
|
34
|
-
Logger.warn(omit(hook, ["handler"]), "core 内置钩子必须显式设置 name(string),用于确定钩子名称");
|
|
59
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子必须显式设置 name(string),用于确定钩子名称" }));
|
|
35
60
|
hasError = true;
|
|
36
61
|
continue;
|
|
37
62
|
}
|
|
38
63
|
// name 必须满足:小写字母 + 下划线(不允许空格、驼峰、数字等)。
|
|
39
64
|
if (!coreBuiltinNameRegexp.test(name)) {
|
|
40
|
-
Logger.warn(omit(hook, ["handler"]), "core 内置钩子的 name 必须满足小写字母+下划线格式(例如 auth / rate_limit),不允许空格、驼峰或其他字符");
|
|
65
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子的 name 必须满足小写字母+下划线格式(例如 auth / rate_limit),不允许空格、驼峰或其他字符" }));
|
|
41
66
|
hasError = true;
|
|
42
67
|
continue;
|
|
43
68
|
}
|
|
44
69
|
if (!coreBuiltinNameRegexp.test(hook.moduleName)) {
|
|
45
|
-
Logger.warn(omit(hook, ["handler"]), "core 内置钩子的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)");
|
|
70
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)" }));
|
|
46
71
|
hasError = true;
|
|
47
72
|
continue;
|
|
48
73
|
}
|
|
49
74
|
if (name !== hook.moduleName) {
|
|
50
|
-
Logger.warn(omit(hook, ["handler"]), "core 内置钩子的 name 必须与 moduleName 完全一致");
|
|
75
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子的 name 必须与 moduleName 完全一致" }));
|
|
51
76
|
hasError = true;
|
|
52
77
|
continue;
|
|
53
78
|
}
|
|
54
79
|
if (typeof hook.filePath !== "string" || !hook.filePath.startsWith(`core:hook:${name}`)) {
|
|
55
|
-
Logger.warn(omit(hook, ["handler"]), "core 内置钩子必须来自静态注册(filePath 必须以 core:hook:<name> 开头),不允许通过扫描目录加载");
|
|
80
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "core 内置钩子必须来自静态注册(filePath 必须以 core:hook:<name> 开头),不允许通过扫描目录加载" }));
|
|
56
81
|
hasError = true;
|
|
57
82
|
continue;
|
|
58
83
|
}
|
|
59
84
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
// deps:允许缺省(补全为 []),但如果用户显式导出 deps,则必须是 string[]。
|
|
86
|
+
if (hasCustomDeps) {
|
|
87
|
+
if (!Array.isArray(hook.deps)) {
|
|
88
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 deps 属性必须是字符串数组" }));
|
|
89
|
+
hasError = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (hook.deps.some((depItem) => typeof depItem !== "string")) {
|
|
93
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 deps 属性必须是字符串数组" }));
|
|
94
|
+
hasError = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
64
97
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
else {
|
|
99
|
+
if (!Array.isArray(hook.deps)) {
|
|
100
|
+
hook.deps = [];
|
|
101
|
+
}
|
|
68
102
|
}
|
|
69
103
|
if (typeof hook.handler !== "function") {
|
|
70
|
-
Logger.warn(omit(hook, ["handler"]), "钩子的 handler 属性必须是函数");
|
|
104
|
+
Logger.warn(Object.assign({}, omit(hook, ["handler"]), { msg: "钩子的 handler 属性必须是函数" }));
|
|
71
105
|
hasError = true;
|
|
72
106
|
continue;
|
|
73
107
|
}
|
|
74
108
|
}
|
|
75
109
|
catch (error) {
|
|
76
|
-
Logger.error({
|
|
77
|
-
err: error,
|
|
78
|
-
item: hook
|
|
79
|
-
}, "钩子解析失败");
|
|
110
|
+
Logger.error({ err: error, item: hook, msg: "钩子解析失败" });
|
|
80
111
|
hasError = true;
|
|
81
112
|
}
|
|
82
113
|
}
|
package/dist/checks/checkMenu.js
CHANGED
|
@@ -59,24 +59,24 @@ export const checkMenu = async (addons, options = {}) => {
|
|
|
59
59
|
const depth = typeof current?.depth === "number" ? current.depth : 0;
|
|
60
60
|
if (menu === null || typeof menu !== "object") {
|
|
61
61
|
hasError = true;
|
|
62
|
-
Logger.warn({ menu: menu
|
|
62
|
+
Logger.warn({ menu: menu, msg: "菜单节点必须是对象" });
|
|
63
63
|
continue;
|
|
64
64
|
}
|
|
65
65
|
if (depth > 3) {
|
|
66
66
|
hasError = true;
|
|
67
|
-
Logger.warn({ path: menu?.path, depth: depth
|
|
67
|
+
Logger.warn({ path: menu?.path, depth: depth, msg: "菜单层级超过 3 级(最多三级)" });
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
70
|
const children = menu.children;
|
|
71
71
|
if (typeof children !== "undefined" && !Array.isArray(children)) {
|
|
72
72
|
hasError = true;
|
|
73
|
-
Logger.warn({ path: menu?.path, childrenType: typeof children
|
|
73
|
+
Logger.warn({ path: menu?.path, childrenType: typeof children, msg: "菜单 children 必须是数组" });
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
76
|
if (Array.isArray(children) && children.length > 0) {
|
|
77
77
|
if (depth >= 3) {
|
|
78
78
|
hasError = true;
|
|
79
|
-
Logger.warn({ path: menu?.path, depth: depth
|
|
79
|
+
Logger.warn({ path: menu?.path, depth: depth, msg: "菜单层级超过 3 级(最多三级)" });
|
|
80
80
|
}
|
|
81
81
|
else {
|
|
82
82
|
for (const child of children) {
|
|
@@ -99,29 +99,29 @@ export const checkMenu = async (addons, options = {}) => {
|
|
|
99
99
|
}
|
|
100
100
|
if (!path) {
|
|
101
101
|
hasError = true;
|
|
102
|
-
Logger.warn({ menu: menu
|
|
102
|
+
Logger.warn({ menu: menu, msg: "菜单缺少 path(必须是非空字符串)" });
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
105
|
const pathCheck = isValidMenuPath(path);
|
|
106
106
|
if (!pathCheck.ok) {
|
|
107
107
|
hasError = true;
|
|
108
|
-
Logger.warn({ path: path, reason: pathCheck.reason
|
|
108
|
+
Logger.warn({ path: path, reason: pathCheck.reason, msg: "菜单 path 不合法" });
|
|
109
109
|
}
|
|
110
110
|
if (!name) {
|
|
111
111
|
hasError = true;
|
|
112
|
-
Logger.warn({ path: path, menu: menu
|
|
112
|
+
Logger.warn({ path: path, menu: menu, msg: "菜单缺少 name(必须是非空字符串)" });
|
|
113
113
|
}
|
|
114
114
|
if (typeof menu.sort !== "undefined" && typeof menu.sort !== "number") {
|
|
115
115
|
hasError = true;
|
|
116
|
-
Logger.warn({ path: path, sort: menu.sort
|
|
116
|
+
Logger.warn({ path: path, sort: menu.sort, msg: "菜单 sort 必须是 number" });
|
|
117
117
|
}
|
|
118
118
|
if (typeof menu.sort === "number" && (!Number.isFinite(menu.sort) || menu.sort < 1)) {
|
|
119
119
|
hasError = true;
|
|
120
|
-
Logger.warn({ path: path, sort: menu.sort
|
|
120
|
+
Logger.warn({ path: path, sort: menu.sort, msg: "菜单 sort 最小值为 1" });
|
|
121
121
|
}
|
|
122
122
|
if (pathSet.has(path)) {
|
|
123
123
|
hasError = true;
|
|
124
|
-
Logger.warn({ path: path
|
|
124
|
+
Logger.warn({ path: path, msg: "菜单 path 重复(严格模式禁止重复 path)" });
|
|
125
125
|
continue;
|
|
126
126
|
}
|
|
127
127
|
pathSet.add(path);
|
|
@@ -1,82 +1,113 @@
|
|
|
1
1
|
import { Logger } from "../lib/logger";
|
|
2
2
|
import { isPlainObject, omit } from "../utils/util";
|
|
3
|
+
const exportKeys = ["name", "enable", "deps", "handler"];
|
|
3
4
|
export async function checkPlugin(plugins) {
|
|
4
5
|
let hasError = false;
|
|
6
|
+
// 说明:plugins 实际是 scanFiles/scanSources 的结果对象(包含元信息字段)。
|
|
7
|
+
// 这里不再对白名单枚举 metaKeys(因为它们是系统生成的),只校验“用户 default export 导出的字段”。
|
|
5
8
|
const coreBuiltinNameRegexp = /^[a-z]+(?:_[a-z]+)*$/;
|
|
6
9
|
for (const plugin of plugins) {
|
|
7
10
|
try {
|
|
8
11
|
if (!isPlainObject(plugin)) {
|
|
9
|
-
Logger.warn(omit(plugin, ["handler"]), "插件导出必须是对象(export default { deps, handler })");
|
|
12
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件导出必须是对象(export default { deps, handler })" }));
|
|
10
13
|
hasError = true;
|
|
11
14
|
continue;
|
|
12
15
|
}
|
|
13
16
|
// moduleName 必须存在(用于依赖排序与运行时挂载)。
|
|
14
17
|
if (typeof plugin.moduleName !== "string" || plugin.moduleName.trim() === "") {
|
|
15
|
-
Logger.warn(omit(plugin, ["handler"]), "插件的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)");
|
|
18
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 moduleName 必须是非空字符串(由系统生成,用于 deps 与运行时挂载)" }));
|
|
16
19
|
hasError = true;
|
|
17
20
|
continue;
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
if (!
|
|
21
|
-
Logger.warn(omit(plugin, ["handler"]),
|
|
22
|
+
const customKeys = plugin.customKeys;
|
|
23
|
+
if (!Array.isArray(customKeys)) {
|
|
24
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件扫描结果缺少 customKeys(无法判断用户导出的字段是否合法)" }));
|
|
22
25
|
hasError = true;
|
|
23
26
|
continue;
|
|
24
27
|
}
|
|
25
|
-
if (typeof
|
|
26
|
-
Logger.warn(omit(plugin, ["handler"]), "插件的
|
|
28
|
+
if (customKeys.some((k) => typeof k !== "string")) {
|
|
29
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 customKeys 必须是 string[](由系统生成)" }));
|
|
27
30
|
hasError = true;
|
|
28
31
|
continue;
|
|
29
32
|
}
|
|
33
|
+
// 严格字段校验:仅检查用户 default export 的字段集合,出现任何未支持字段都应视为错误。
|
|
34
|
+
const unknownCustomKeys = customKeys.filter((k) => !exportKeys.includes(k));
|
|
35
|
+
if (unknownCustomKeys.length > 0) {
|
|
36
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: `插件导出存在不支持的属性:${unknownCustomKeys.join(", ")};仅允许:${exportKeys.join(", ")};当前 customKeys:${customKeys.join(", ")}` }));
|
|
37
|
+
hasError = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const hasCustomEnable = customKeys.includes("enable");
|
|
41
|
+
const hasCustomDeps = customKeys.includes("deps");
|
|
42
|
+
// enable 必须显式声明且只能为 boolean(true/false),不允许 0/1 等其他类型。
|
|
43
|
+
// - 允许缺省:由系统在此处补全默认值 true
|
|
44
|
+
// - 若用户显式导出 enable:必须是 boolean
|
|
45
|
+
if (hasCustomEnable) {
|
|
46
|
+
if (typeof plugin.enable !== "boolean") {
|
|
47
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 enable 属性必须是 boolean(true/false),不允许 0/1 等其他类型" }));
|
|
48
|
+
hasError = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
plugin.enable = true;
|
|
54
|
+
}
|
|
30
55
|
// core 内置插件:必须来自静态注册(filePath 以 core:plugin: 开头),且 name 必须显式指定并与 moduleName 一致。
|
|
31
56
|
if (plugin.source === "core") {
|
|
32
57
|
const name = typeof plugin.name === "string" ? plugin.name : "";
|
|
33
58
|
if (name === "") {
|
|
34
|
-
Logger.warn(omit(plugin, ["handler"]), "core 内置插件必须显式设置 name(string),用于确定插件名称");
|
|
59
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件必须显式设置 name(string),用于确定插件名称" }));
|
|
35
60
|
hasError = true;
|
|
36
61
|
continue;
|
|
37
62
|
}
|
|
38
63
|
// name 必须满足:小写字母 + 下划线(不允许空格、驼峰、数字等)。
|
|
39
64
|
if (!coreBuiltinNameRegexp.test(name)) {
|
|
40
|
-
Logger.warn(omit(plugin, ["handler"]), "core 内置插件的 name 必须满足小写字母+下划线格式(例如 logger / redis_cache),不允许空格、驼峰或其他字符");
|
|
65
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件的 name 必须满足小写字母+下划线格式(例如 logger / redis_cache),不允许空格、驼峰或其他字符" }));
|
|
41
66
|
hasError = true;
|
|
42
67
|
continue;
|
|
43
68
|
}
|
|
44
69
|
if (!coreBuiltinNameRegexp.test(plugin.moduleName)) {
|
|
45
|
-
Logger.warn(omit(plugin, ["handler"]), "core 内置插件的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)");
|
|
70
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件的 moduleName 必须满足小写字母+下划线格式(由系统生成,且必须与 name 一致)" }));
|
|
46
71
|
hasError = true;
|
|
47
72
|
continue;
|
|
48
73
|
}
|
|
49
74
|
if (name !== plugin.moduleName) {
|
|
50
|
-
Logger.warn(omit(plugin, ["handler"]), "core 内置插件的 name 必须与 moduleName 完全一致");
|
|
75
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件的 name 必须与 moduleName 完全一致" }));
|
|
51
76
|
hasError = true;
|
|
52
77
|
continue;
|
|
53
78
|
}
|
|
54
79
|
if (typeof plugin.filePath !== "string" || !plugin.filePath.startsWith(`core:plugin:${name}`)) {
|
|
55
|
-
Logger.warn(omit(plugin, ["handler"]), "core 内置插件必须来自静态注册(filePath 必须以 core:plugin:<name> 开头),不允许通过扫描目录加载");
|
|
80
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "core 内置插件必须来自静态注册(filePath 必须以 core:plugin:<name> 开头),不允许通过扫描目录加载" }));
|
|
56
81
|
hasError = true;
|
|
57
82
|
continue;
|
|
58
83
|
}
|
|
59
84
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
// deps:允许缺省(补全为 []),但如果用户显式导出 deps,则必须是 string[]。
|
|
86
|
+
if (hasCustomDeps) {
|
|
87
|
+
if (!Array.isArray(plugin.deps)) {
|
|
88
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 deps 属性必须是字符串数组" }));
|
|
89
|
+
hasError = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (plugin.deps.some((depItem) => typeof depItem !== "string")) {
|
|
93
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 deps 属性必须是字符串数组" }));
|
|
94
|
+
hasError = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
64
97
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
else {
|
|
99
|
+
if (!Array.isArray(plugin.deps)) {
|
|
100
|
+
plugin.deps = [];
|
|
101
|
+
}
|
|
68
102
|
}
|
|
69
103
|
if (typeof plugin.handler !== "function") {
|
|
70
|
-
Logger.warn(omit(plugin, ["handler"]), "插件的 handler 属性必须是函数");
|
|
104
|
+
Logger.warn(Object.assign({}, omit(plugin, ["handler"]), { msg: "插件的 handler 属性必须是函数" }));
|
|
71
105
|
hasError = true;
|
|
72
106
|
continue;
|
|
73
107
|
}
|
|
74
108
|
}
|
|
75
109
|
catch (error) {
|
|
76
|
-
Logger.error({
|
|
77
|
-
err: error,
|
|
78
|
-
item: plugin
|
|
79
|
-
}, "插件解析失败");
|
|
110
|
+
Logger.error({ err: error, item: plugin, msg: "插件解析失败" });
|
|
80
111
|
hasError = true;
|
|
81
112
|
}
|
|
82
113
|
}
|
|
@@ -40,25 +40,26 @@ export async function checkTable(tables) {
|
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
42
42
|
const sourceName = typeof item.sourceName === "string" ? item.sourceName : "";
|
|
43
|
+
const tablePrefix = sourceName ? `${sourceName}表 ` : "表 ";
|
|
43
44
|
try {
|
|
44
45
|
const fileName = item.fileName;
|
|
45
46
|
const table = item.content || {};
|
|
46
47
|
// 1) 文件名小驼峰校验
|
|
47
48
|
if (!LOWER_CAMEL_CASE_REGEX.test(fileName)) {
|
|
48
|
-
Logger.warn(`${
|
|
49
|
+
Logger.warn(`${tablePrefix}${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
|
|
49
50
|
hasError = true;
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
53
|
// 检查 table 中的每个验证规则
|
|
53
54
|
for (const [colKey, fieldDef] of Object.entries(table)) {
|
|
54
55
|
if (typeof fieldDef !== "object" || fieldDef === null || Array.isArray(fieldDef)) {
|
|
55
|
-
Logger.warn(`${
|
|
56
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 规则必须为对象`);
|
|
56
57
|
hasError = true;
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
60
|
// 检查是否使用了保留字段
|
|
60
61
|
if (RESERVED_FIELDS.includes(colKey)) {
|
|
61
|
-
Logger.warn(`${
|
|
62
|
+
Logger.warn(`${tablePrefix}${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(", ")}`);
|
|
62
63
|
hasError = true;
|
|
63
64
|
}
|
|
64
65
|
// 直接使用字段对象
|
|
@@ -67,78 +68,78 @@ export async function checkTable(tables) {
|
|
|
67
68
|
const fieldKeys = Object.keys(field);
|
|
68
69
|
const illegalProps = fieldKeys.filter((key) => !ALLOWED_FIELD_PROPERTIES.includes(key));
|
|
69
70
|
if (illegalProps.length > 0) {
|
|
70
|
-
Logger.warn(`${
|
|
71
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 包含非法属性: ${illegalProps.join(", ")},` + `允许的属性为: ${ALLOWED_FIELD_PROPERTIES.join(", ")}`);
|
|
71
72
|
hasError = true;
|
|
72
73
|
}
|
|
73
74
|
// 检查必填字段:name, type
|
|
74
75
|
if (!field.name || typeof field.name !== "string") {
|
|
75
|
-
Logger.warn(`${
|
|
76
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
|
|
76
77
|
hasError = true;
|
|
77
78
|
continue;
|
|
78
79
|
}
|
|
79
80
|
if (!field.type || typeof field.type !== "string") {
|
|
80
|
-
Logger.warn(`${
|
|
81
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
|
|
81
82
|
hasError = true;
|
|
82
83
|
continue;
|
|
83
84
|
}
|
|
84
85
|
// 检查可选字段的类型
|
|
85
86
|
if (field.min !== undefined && !(field.min === null || typeof field.min === "number")) {
|
|
86
|
-
Logger.warn(`${
|
|
87
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 min 类型错误,必须为 null 或数字`);
|
|
87
88
|
hasError = true;
|
|
88
89
|
}
|
|
89
90
|
if (field.max !== undefined && !(field.max === null || typeof field.max === "number")) {
|
|
90
|
-
Logger.warn(`${
|
|
91
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 max 类型错误,必须为 null 或数字`);
|
|
91
92
|
hasError = true;
|
|
92
93
|
}
|
|
93
94
|
if (field.detail !== undefined && typeof field.detail !== "string") {
|
|
94
|
-
Logger.warn(`${
|
|
95
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
|
|
95
96
|
hasError = true;
|
|
96
97
|
}
|
|
97
98
|
if (field.index !== undefined && typeof field.index !== "boolean") {
|
|
98
|
-
Logger.warn(`${
|
|
99
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
|
|
99
100
|
hasError = true;
|
|
100
101
|
}
|
|
101
102
|
if (field.unique !== undefined && typeof field.unique !== "boolean") {
|
|
102
|
-
Logger.warn(`${
|
|
103
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
|
|
103
104
|
hasError = true;
|
|
104
105
|
}
|
|
105
106
|
if (field.nullable !== undefined && typeof field.nullable !== "boolean") {
|
|
106
|
-
Logger.warn(`${
|
|
107
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
|
|
107
108
|
hasError = true;
|
|
108
109
|
}
|
|
109
110
|
if (field.unsigned !== undefined && typeof field.unsigned !== "boolean") {
|
|
110
|
-
Logger.warn(`${
|
|
111
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
|
|
111
112
|
hasError = true;
|
|
112
113
|
}
|
|
113
114
|
if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== "string") {
|
|
114
|
-
Logger.warn(`${
|
|
115
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
|
|
115
116
|
hasError = true;
|
|
116
117
|
}
|
|
117
118
|
const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault } = field;
|
|
118
119
|
// 字段名称必须为中文、数字、字母、下划线、短横线、空格
|
|
119
120
|
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
120
|
-
Logger.warn(`${
|
|
121
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
121
122
|
hasError = true;
|
|
122
123
|
}
|
|
123
124
|
// 字段类型必须为string,number,text,array_string,array_text之一
|
|
124
125
|
if (!FIELD_TYPES.includes(fieldType)) {
|
|
125
|
-
Logger.warn(`${
|
|
126
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join("、")}之一`);
|
|
126
127
|
hasError = true;
|
|
127
128
|
}
|
|
128
129
|
// unsigned 仅对 number 类型有效(且仅 MySQL 语义上生效)
|
|
129
130
|
if (fieldType !== "number" && field.unsigned !== undefined) {
|
|
130
|
-
Logger.warn(`${
|
|
131
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 字段类型为 ${fieldType},不允许设置 unsigned(仅 number 类型有效)`);
|
|
131
132
|
hasError = true;
|
|
132
133
|
}
|
|
133
134
|
// 约束:unique 与 index 不能同时为 true(否则会重复索引),必须阻断启动。
|
|
134
135
|
if (field.unique === true && field.index === true) {
|
|
135
|
-
Logger.warn(`${
|
|
136
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 同时设置了 unique=true 和 index=true,` + `unique 和 index 不能同时设置,请删除其一(否则会创建重复索引)`);
|
|
136
137
|
hasError = true;
|
|
137
138
|
}
|
|
138
139
|
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
139
140
|
if (fieldMin !== undefined && fieldMax !== undefined && fieldMin !== null && fieldMax !== null) {
|
|
140
141
|
if (fieldMin > fieldMax) {
|
|
141
|
-
Logger.warn(`${
|
|
142
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
142
143
|
hasError = true;
|
|
143
144
|
}
|
|
144
145
|
}
|
|
@@ -146,47 +147,47 @@ export async function checkTable(tables) {
|
|
|
146
147
|
if (fieldType === "text" || fieldType === "array_text" || fieldType === "array_number_text") {
|
|
147
148
|
// text / array_text / array_number_text:min/max 必须为 null,默认值必须为 null,且不支持索引/唯一约束
|
|
148
149
|
if (fieldMin !== undefined && fieldMin !== null) {
|
|
149
|
-
Logger.warn(`${
|
|
150
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 的 ${fieldType} 类型最小值应为 null,当前为 "${fieldMin}"`);
|
|
150
151
|
hasError = true;
|
|
151
152
|
}
|
|
152
153
|
if (fieldMax !== undefined && fieldMax !== null) {
|
|
153
|
-
Logger.warn(`${
|
|
154
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 的 ${fieldType} 类型最大长度应为 null,当前为 "${fieldMax}"`);
|
|
154
155
|
hasError = true;
|
|
155
156
|
}
|
|
156
157
|
if (fieldDefault !== undefined && fieldDefault !== null) {
|
|
157
|
-
Logger.warn(`${
|
|
158
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
|
|
158
159
|
hasError = true;
|
|
159
160
|
}
|
|
160
161
|
if (field.index === true) {
|
|
161
|
-
Logger.warn(`${
|
|
162
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持创建索引(index=true 无效)`);
|
|
162
163
|
hasError = true;
|
|
163
164
|
}
|
|
164
165
|
if (field.unique === true) {
|
|
165
|
-
Logger.warn(`${
|
|
166
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,不支持唯一约束(unique=true 无效)`);
|
|
166
167
|
hasError = true;
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
else if (fieldType === "string" || fieldType === "array_string" || fieldType === "array_number_string") {
|
|
170
171
|
if (fieldMax !== undefined && (fieldMax === null || typeof fieldMax !== "number")) {
|
|
171
|
-
Logger.warn(`${
|
|
172
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
172
173
|
hasError = true;
|
|
173
174
|
}
|
|
174
175
|
else if (fieldMax !== undefined && fieldMax > MAX_VARCHAR_LENGTH) {
|
|
175
|
-
Logger.warn(`${
|
|
176
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
176
177
|
hasError = true;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
else if (fieldType === "number") {
|
|
180
181
|
// number 类型:default 如果存在,必须为 null 或 number
|
|
181
182
|
if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== "number") {
|
|
182
|
-
Logger.warn(`${
|
|
183
|
+
Logger.warn(`${tablePrefix}${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
|
|
183
184
|
hasError = true;
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
catch (error) {
|
|
189
|
-
Logger.error(`${
|
|
190
|
+
Logger.error({ msg: `${tablePrefix}${item.fileName} 解析失败`, err: error });
|
|
190
191
|
hasError = true;
|
|
191
192
|
}
|
|
192
193
|
}
|