befly 3.9.39 → 3.10.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/README.md +39 -8
- package/befly.config.ts +19 -2
- package/checks/checkApi.ts +79 -77
- package/checks/checkHook.ts +48 -0
- package/checks/checkMenu.ts +168 -0
- package/checks/checkPlugin.ts +48 -0
- package/checks/checkTable.ts +137 -183
- package/docs/README.md +1 -1
- package/docs/api/api.md +1 -1
- package/docs/guide/quickstart.md +16 -9
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +7 -7
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +16 -9
- package/docs/reference/addon.md +12 -1
- package/docs/reference/config.md +13 -30
- package/docs/reference/sync.md +62 -193
- package/docs/reference/table.md +27 -29
- package/hooks/auth.ts +3 -4
- package/hooks/cors.ts +4 -6
- package/hooks/parser.ts +3 -4
- package/hooks/permission.ts +3 -4
- package/hooks/validator.ts +4 -5
- package/lib/cacheHelper.ts +89 -153
- package/lib/cacheKeys.ts +1 -1
- package/lib/connect.ts +9 -13
- package/lib/dbDialect.ts +285 -0
- package/lib/dbHelper.ts +179 -507
- package/lib/dbUtils.ts +450 -0
- package/lib/logger.ts +41 -5
- package/lib/redisHelper.ts +1 -0
- package/lib/sqlBuilder.ts +358 -58
- package/lib/sqlCheck.ts +136 -0
- package/lib/validator.ts +1 -1
- package/loader/loadApis.ts +23 -126
- package/loader/loadHooks.ts +31 -46
- package/loader/loadPlugins.ts +37 -52
- package/main.ts +58 -19
- package/package.json +24 -25
- package/paths.ts +14 -14
- package/plugins/cache.ts +12 -6
- package/plugins/cipher.ts +2 -2
- package/plugins/config.ts +6 -8
- package/plugins/db.ts +14 -19
- package/plugins/jwt.ts +6 -7
- package/plugins/logger.ts +7 -9
- package/plugins/redis.ts +8 -10
- package/plugins/tool.ts +3 -4
- package/router/api.ts +3 -2
- package/router/static.ts +7 -5
- package/sync/syncApi.ts +80 -235
- package/sync/syncCache.ts +16 -0
- package/sync/syncDev.ts +167 -202
- package/sync/syncMenu.ts +230 -444
- package/sync/syncTable.ts +1247 -0
- package/tests/_mocks/mockSqliteDb.ts +204 -0
- package/tests/addonHelper-cache.test.ts +32 -0
- package/tests/apiHandler-routePath-only.test.ts +32 -0
- package/tests/cacheHelper.test.ts +16 -51
- package/tests/checkApi-routePath-strict.test.ts +166 -0
- package/tests/checkMenu.test.ts +346 -0
- package/tests/checkTable-smoke.test.ts +157 -0
- package/tests/dbDialect-cache.test.ts +23 -0
- package/tests/dbDialect.test.ts +46 -0
- package/tests/dbHelper-advanced.test.ts +1 -1
- package/tests/dbHelper-all-array-types.test.ts +15 -15
- package/tests/dbHelper-batch-write.test.ts +90 -0
- package/tests/dbHelper-columns.test.ts +36 -54
- package/tests/dbHelper-execute.test.ts +26 -26
- package/tests/dbHelper-joins.test.ts +85 -176
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
- package/tests/fixtures/scanFilesApis/a.ts +3 -0
- package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
- package/tests/loadPlugins-order-smoke.test.ts +75 -0
- package/tests/logger.test.ts +6 -6
- package/tests/redisHelper.test.ts +6 -1
- package/tests/scanFiles-routePath.test.ts +46 -0
- package/tests/smoke-sql.test.ts +24 -0
- package/tests/sqlBuilder-advanced.test.ts +18 -5
- package/tests/sqlBuilder.test.ts +24 -0
- package/tests/sync-init-guard.test.ts +105 -0
- package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
- package/tests/syncApi-obsolete-records.test.ts +69 -0
- package/tests/syncApi-type-compat.test.ts +72 -0
- package/tests/syncDev-permissions.test.ts +81 -0
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
- package/tests/syncMenu-duplicate-path.test.ts +122 -0
- package/tests/syncMenu-obsolete-records.test.ts +161 -0
- package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
- package/tests/syncMenu-paths.test.ts +0 -9
- package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
- package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
- package/tests/syncTable-constants.test.ts +101 -0
- package/tests/syncTable-db-integration.test.ts +237 -0
- package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
- package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
- package/tests/syncTable-schema.test.ts +99 -0
- package/tests/syncTable-testkit.test.ts +25 -0
- package/tests/syncTable-types.test.ts +122 -0
- package/tests/tableRef-and-deserialize.test.ts +67 -0
- package/tsconfig.json +1 -1
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +13 -12
- package/types/cache.d.ts +2 -2
- package/types/context.d.ts +1 -1
- package/types/database.d.ts +0 -5
- package/types/hook.d.ts +1 -10
- package/types/plugin.d.ts +2 -96
- package/types/sync.d.ts +19 -25
- package/utils/convertBigIntFields.ts +38 -0
- package/utils/disableMenusGlob.ts +85 -0
- package/utils/importDefault.ts +21 -0
- package/utils/isDirentDirectory.ts +23 -0
- package/utils/loadMenuConfigs.ts +145 -0
- package/utils/processFields.ts +25 -0
- package/utils/scanAddons.ts +72 -0
- package/utils/scanFiles.ts +129 -21
- package/utils/scanSources.ts +64 -0
- package/utils/sortModules.ts +137 -0
- package/checks/checkApp.ts +0 -55
- package/hooks/rateLimit.ts +0 -276
- package/sync/syncAll.ts +0 -35
- package/sync/syncDb/apply.ts +0 -192
- package/sync/syncDb/constants.ts +0 -119
- package/sync/syncDb/ddl.ts +0 -251
- package/sync/syncDb/helpers.ts +0 -84
- package/sync/syncDb/schema.ts +0 -202
- package/sync/syncDb/sqlite.ts +0 -48
- package/sync/syncDb/table.ts +0 -207
- package/sync/syncDb/tableCreate.ts +0 -163
- package/sync/syncDb/types.ts +0 -132
- package/sync/syncDb/version.ts +0 -69
- package/sync/syncDb.ts +0 -168
- package/tests/rateLimit-hook.test.ts +0 -477
- package/tests/syncDb-constants.test.ts +0 -130
- package/tests/syncDb-schema.test.ts +0 -179
- package/tests/syncDb-types.test.ts +0 -139
- package/utils/addonHelper.ts +0 -90
- package/utils/modules.ts +0 -98
- package/utils/route.ts +0 -23
package/lib/sqlCheck.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL 入参校验工具(静态类)
|
|
3
|
+
*
|
|
4
|
+
* 目标:把“参数合法性/一致性/安全性”判断从 SqlBuilder 等拼接逻辑中拆出来,便于复用与维护。
|
|
5
|
+
*
|
|
6
|
+
* 说明:这里的校验仅关注“字符串/标识符/批量数据结构”层面的正确性;
|
|
7
|
+
* 具体 SQL 语义(如字段是否存在)不在此处校验。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class SqlCheck {
|
|
11
|
+
private static readonly SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
12
|
+
|
|
13
|
+
static assertNonEmptyString(value: unknown, label: string): asserts value is string {
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
throw new Error(`${label} 必须是字符串 (value: ${String(value)})`);
|
|
16
|
+
}
|
|
17
|
+
if (!value.trim()) {
|
|
18
|
+
throw new Error(`${label} 不能为空`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static assertNoUndefinedParam(value: unknown, label: string): void {
|
|
23
|
+
if (value === undefined) {
|
|
24
|
+
throw new Error(`${label} 不能为 undefined`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static startsWithQuote(value: string): boolean {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
return trimmed.startsWith("`") || trimmed.startsWith('"');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static isQuotedIdentPaired(value: string): boolean {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
if (trimmed.length < 2) return false;
|
|
36
|
+
|
|
37
|
+
const first = trimmed[0];
|
|
38
|
+
const last = trimmed[trimmed.length - 1];
|
|
39
|
+
|
|
40
|
+
if (first === "`" && last === "`") return true;
|
|
41
|
+
if (first === '"' && last === '"') return true;
|
|
42
|
+
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static assertPairedQuotedIdentIfStartsWithQuote(value: string, label: string): void {
|
|
47
|
+
if (SqlCheck.startsWithQuote(value) && !SqlCheck.isQuotedIdentPaired(value)) {
|
|
48
|
+
throw new Error(`${label} 引用不完整,请使用成对的 \`...\` 或 "..." (value: ${value})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static assertSafeIdentifierPart(part: string, kind: "table" | "schema" | "alias" | "field"): void {
|
|
53
|
+
// 这里仅允许常规标识符(字母/数字/下划线),避免把复杂表达式混进“自动转义”路径。
|
|
54
|
+
if (!SqlCheck.SAFE_IDENTIFIER_RE.test(part)) {
|
|
55
|
+
throw new Error(`无效的 ${kind} 标识符: ${part}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static assertSafeAlias(aliasPart: string): void {
|
|
60
|
+
// alias 允许两种:
|
|
61
|
+
// 1) 已经被引用(`alias` 或 "alias")
|
|
62
|
+
// 2) 普通标识符(不允许带空格/符号),避免注入
|
|
63
|
+
if (SqlCheck.isQuotedIdentPaired(aliasPart)) return;
|
|
64
|
+
if (!SqlCheck.SAFE_IDENTIFIER_RE.test(aliasPart)) {
|
|
65
|
+
throw new Error(`无效的字段别名: ${aliasPart}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static assertNoExprField(field: string): void {
|
|
70
|
+
if (typeof field !== "string") return;
|
|
71
|
+
const trimmed = field.trim();
|
|
72
|
+
if (!trimmed) return;
|
|
73
|
+
|
|
74
|
+
// 收紧:包含函数/表达式(括号)不允许走自动转义路径
|
|
75
|
+
// 这类表达式应显式使用 selectRaw/whereRaw 以避免误拼接和注入风险
|
|
76
|
+
if (trimmed.includes("(") || trimmed.includes(")")) {
|
|
77
|
+
throw new Error(`字段包含函数/表达式,请使用 selectRaw/whereRaw (field: ${trimmed})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static assertNoUndefinedInRecord(row: Record<string, unknown>, label: string): void {
|
|
82
|
+
for (const [key, value] of Object.entries(row)) {
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
throw new Error(`${label} 存在 undefined 字段值 (field: ${key})`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static assertBatchInsertRowsConsistent(rows: Array<Record<string, unknown>>, options: { table: string }): string[] {
|
|
90
|
+
if (!Array.isArray(rows)) {
|
|
91
|
+
throw new Error("批量插入 rows 必须是数组");
|
|
92
|
+
}
|
|
93
|
+
if (rows.length === 0) {
|
|
94
|
+
throw new Error(`插入数据不能为空 (table: ${options.table})`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const first = rows[0];
|
|
98
|
+
if (!first || typeof first !== "object" || Array.isArray(first)) {
|
|
99
|
+
throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: 0)`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const fields = Object.keys(first);
|
|
103
|
+
if (fields.length === 0) {
|
|
104
|
+
throw new Error(`插入数据必须至少有一个字段 (table: ${options.table})`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fieldSet = new Set(fields);
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < rows.length; i++) {
|
|
110
|
+
const row = rows[i];
|
|
111
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
112
|
+
throw new Error(`批量插入的每一行必须是对象 (table: ${options.table}, rowIndex: ${i})`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rowKeys = Object.keys(row);
|
|
116
|
+
if (rowKeys.length !== fields.length) {
|
|
117
|
+
throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i})`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const key of rowKeys) {
|
|
121
|
+
if (!fieldSet.has(key)) {
|
|
122
|
+
throw new Error(`批量插入每行字段必须一致 (table: ${options.table}, rowIndex: ${i}, extraField: ${key})`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const field of fields) {
|
|
127
|
+
if (!(field in row)) {
|
|
128
|
+
throw new Error(`批量插入缺少字段 (table: ${options.table}, rowIndex: ${i}, field: ${field})`);
|
|
129
|
+
}
|
|
130
|
+
SqlCheck.assertNoUndefinedParam((row as any)[field], `批量插入字段值 (table: ${options.table}, rowIndex: ${i}, field: ${field})`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return fields;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/lib/validator.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { TableDefinition, FieldDefinition, ValidateResult, SingleResult } from "../types/validate.js";
|
|
7
7
|
|
|
8
|
-
import { RegexAliases, getCompiledRegex } from "../
|
|
8
|
+
import { RegexAliases, getCompiledRegex } from "../configs/presetRegexp.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 验证器类
|
package/loader/loadApis.ts
CHANGED
|
@@ -5,142 +5,39 @@
|
|
|
5
5
|
|
|
6
6
|
// 类型导入
|
|
7
7
|
import type { ApiRoute } from "../types/api.js";
|
|
8
|
+
import type { ScanFileResult } from "../utils/scanFiles.js";
|
|
8
9
|
|
|
9
10
|
import { Logger } from "../lib/logger.js";
|
|
10
|
-
import {
|
|
11
|
-
import { scanAddons, getAddonDir, addonDirExists } from "../utils/addonHelper.js";
|
|
12
|
-
import { makeRouteKey } from "../utils/route.js";
|
|
13
|
-
// 相对导入
|
|
14
|
-
import { scanFiles } from "../utils/scanFiles.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 预定义的默认字段
|
|
18
|
-
*/
|
|
19
|
-
const PRESET_FIELDS: Record<string, any> = {
|
|
20
|
-
"@id": { name: "ID", type: "number", min: 1, max: null },
|
|
21
|
-
"@page": { name: "页码", type: "number", min: 1, max: 9999, default: 1 },
|
|
22
|
-
"@limit": { name: "每页数量", type: "number", min: 1, max: 100, default: 30 },
|
|
23
|
-
"@keyword": { name: "关键词", type: "string", min: 0, max: 50 },
|
|
24
|
-
"@state": { name: "状态", type: "number", regex: "^[0-2]$" }
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 处理字段定义:将 @ 符号引用替换为实际字段定义
|
|
29
|
-
*/
|
|
30
|
-
function processFields(fields: Record<string, any>, apiName: string, routePath: string): Record<string, any> {
|
|
31
|
-
if (!fields || typeof fields !== "object") return fields;
|
|
32
|
-
|
|
33
|
-
const processed: Record<string, any> = {};
|
|
34
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
35
|
-
if (typeof value === "string" && value.startsWith("@")) {
|
|
36
|
-
if (PRESET_FIELDS[value]) {
|
|
37
|
-
processed[key] = PRESET_FIELDS[value];
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const validKeys = Object.keys(PRESET_FIELDS).join(", ");
|
|
42
|
-
throw new Error(`API [${apiName}] (${routePath}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
processed[key] = value;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return processed;
|
|
49
|
-
}
|
|
11
|
+
import { processFields } from "../utils/processFields.js";
|
|
50
12
|
|
|
51
13
|
/**
|
|
52
14
|
* 加载所有 API 路由
|
|
53
|
-
* @param
|
|
15
|
+
* @param apiItems - scanSources/scanFiles 扫描到的 API 条目数组
|
|
16
|
+
* @returns API 路由映射表
|
|
54
17
|
*/
|
|
55
|
-
export async function loadApis(apis: Map<string, ApiRoute
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
typeName: "项目"
|
|
65
|
-
}));
|
|
66
|
-
|
|
67
|
-
// 2. 扫描组件 API
|
|
68
|
-
const addonApiList: Array<{
|
|
69
|
-
filePath: string;
|
|
70
|
-
relativePath: string;
|
|
71
|
-
type: "addon";
|
|
72
|
-
routePrefix: string;
|
|
73
|
-
typeName: string;
|
|
74
|
-
}> = [];
|
|
75
|
-
const addons = scanAddons();
|
|
76
|
-
for (const addon of addons) {
|
|
77
|
-
if (!addonDirExists(addon, "apis")) continue;
|
|
78
|
-
|
|
79
|
-
const addonApiDir = getAddonDir(addon, "apis");
|
|
80
|
-
const addonApiFiles = await scanFiles(addonApiDir);
|
|
81
|
-
|
|
82
|
-
for (const file of addonApiFiles) {
|
|
83
|
-
addonApiList.push({
|
|
84
|
-
filePath: file.filePath,
|
|
85
|
-
relativePath: file.relativePath,
|
|
86
|
-
type: "addon" as const,
|
|
87
|
-
routePrefix: `/addon/${addon}/`, // 组件 API 默认带斜杠
|
|
88
|
-
typeName: `组件${addon}`
|
|
89
|
-
});
|
|
90
|
-
}
|
|
18
|
+
export async function loadApis(apis: ScanFileResult[]): Promise<Map<string, ApiRoute>> {
|
|
19
|
+
const apisMap = new Map<string, ApiRoute>();
|
|
20
|
+
|
|
21
|
+
for (const api of apis) {
|
|
22
|
+
const apiType = (api as any).type;
|
|
23
|
+
// 兼容:scanFiles 的结果或测试构造数据可能缺少 type 字段;缺少时默认按 API 处理。
|
|
24
|
+
// 仅在 type 显式存在且不等于 "api" 时跳过,避免错误过滤。
|
|
25
|
+
if (apiType && apiType !== "api") {
|
|
26
|
+
continue;
|
|
91
27
|
}
|
|
92
28
|
|
|
93
|
-
|
|
94
|
-
|
|
29
|
+
try {
|
|
30
|
+
const apiRoute = api as any;
|
|
95
31
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
// Windows 下路径需要转换为正斜杠格式
|
|
100
|
-
const normalizedFilePath = apiFile.filePath.replace(/\\/g, "/");
|
|
101
|
-
const apiImport = await import(normalizedFilePath);
|
|
102
|
-
const api = apiImport.default;
|
|
32
|
+
// 处理字段定义,将 @ 引用替换为实际字段定义
|
|
33
|
+
apiRoute.fields = processFields(apiRoute.fields || {}, apiRoute.name, apiRoute.routePath);
|
|
103
34
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
api.auth = api.auth !== undefined ? api.auth : true;
|
|
109
|
-
|
|
110
|
-
// 构建路由路径(用于错误提示)
|
|
111
|
-
const routePath = `/api${apiFile.routePrefix}${apiFile.relativePath}`;
|
|
112
|
-
|
|
113
|
-
// 处理字段定义,将 @ 引用替换为实际字段定义
|
|
114
|
-
api.fields = processFields(api.fields || {}, api.name, routePath);
|
|
115
|
-
api.required = api.required || [];
|
|
116
|
-
|
|
117
|
-
// 支持逗号分隔的多方法,拆分后分别注册
|
|
118
|
-
const methods = methodStr
|
|
119
|
-
.split(",")
|
|
120
|
-
.map((m: string) => m.trim())
|
|
121
|
-
.filter((m: string) => m);
|
|
122
|
-
for (const method of methods) {
|
|
123
|
-
const route = makeRouteKey(method, routePath);
|
|
124
|
-
// 为每个方法创建独立的路由对象
|
|
125
|
-
const routeApi: ApiRoute = {
|
|
126
|
-
name: api.name,
|
|
127
|
-
handler: api.handler,
|
|
128
|
-
method: method,
|
|
129
|
-
auth: api.auth,
|
|
130
|
-
fields: api.fields,
|
|
131
|
-
required: api.required,
|
|
132
|
-
rawBody: api.rawBody,
|
|
133
|
-
route: route
|
|
134
|
-
};
|
|
135
|
-
apis.set(route, routeApi);
|
|
136
|
-
}
|
|
137
|
-
} catch (error: any) {
|
|
138
|
-
Logger.error({ err: error, api: apiFile.relativePath, type: apiFile.typeName }, "接口加载失败");
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
35
|
+
apisMap.set(apiRoute.routePath, apiRoute as ApiRoute);
|
|
36
|
+
} catch (error: any) {
|
|
37
|
+
Logger.error({ err: error, api: api.relativePath, file: api.filePath }, "接口加载失败");
|
|
38
|
+
throw error;
|
|
141
39
|
}
|
|
142
|
-
} catch (error: any) {
|
|
143
|
-
Logger.error({ err: error }, "加载 API 时发生错误");
|
|
144
|
-
process.exit(1);
|
|
145
40
|
}
|
|
41
|
+
|
|
42
|
+
return apisMap;
|
|
146
43
|
}
|
package/loader/loadHooks.ts
CHANGED
|
@@ -1,66 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 钩子加载器
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 默认加载所有来源钩子(core/addon/app)
|
|
4
|
+
* 可通过 disableHooks 禁用指定钩子
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// 类型导入
|
|
8
8
|
import type { Hook } from "../types/hook.js";
|
|
9
|
+
import type { ScanFileResult } from "../utils/scanFiles.js";
|
|
9
10
|
|
|
10
|
-
import { beflyConfig } from "../befly.config.js";
|
|
11
11
|
import { Logger } from "../lib/logger.js";
|
|
12
|
-
import {
|
|
13
|
-
// 相对导入
|
|
14
|
-
import { scanAddons, getAddonDir, addonDirExists } from "../utils/addonHelper.js";
|
|
15
|
-
import { scanModules } from "../utils/modules.js";
|
|
12
|
+
import { sortModules } from "../utils/sortModules.js";
|
|
16
13
|
|
|
17
|
-
export async function loadHooks(hooks:
|
|
18
|
-
|
|
19
|
-
const allHooks: Hook[] = [];
|
|
14
|
+
export async function loadHooks(hooks: ScanFileResult[], disableHooks: string[] = []): Promise<Hook[]> {
|
|
15
|
+
const hooksMap: Hook[] = [];
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
if (disableHooks.length > 0) {
|
|
18
|
+
Logger.info({ hooks: disableHooks }, "禁用钩子");
|
|
19
|
+
}
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
const addons = scanAddons();
|
|
30
|
-
for (const addon of addons) {
|
|
31
|
-
if (!addonDirExists(addon, "hooks")) continue;
|
|
32
|
-
const dir = getAddonDir(addon, "hooks");
|
|
33
|
-
const items = await scanModules<Hook>(dir, "addon", "钩子", addon);
|
|
34
|
-
addonHooks.push(...items);
|
|
35
|
-
}
|
|
36
|
-
allHooks.push(...addonHooks);
|
|
21
|
+
const enabledHooks = hooks.filter((item: any) => {
|
|
22
|
+
const moduleName = item?.moduleName;
|
|
23
|
+
if (typeof moduleName !== "string" || moduleName.trim() === "") {
|
|
24
|
+
return false;
|
|
37
25
|
}
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (enableAppHooks) {
|
|
42
|
-
const appHooks = await scanModules<Hook>(projectHookDir, "app", "钩子");
|
|
43
|
-
allHooks.push(...appHooks);
|
|
27
|
+
if (disableHooks.includes(moduleName)) {
|
|
28
|
+
return false;
|
|
44
29
|
}
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const enabledHooks = allHooks.filter((hook) => hook.name && !disableHooks.includes(hook.name));
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
const sortedHooks = sortModules(enabledHooks, { moduleLabel: "钩子" });
|
|
35
|
+
if (sortedHooks === false) {
|
|
36
|
+
throw new Error("钩子依赖关系错误");
|
|
37
|
+
}
|
|
53
38
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const orderB = b.order ?? 999;
|
|
58
|
-
return orderA - orderB;
|
|
59
|
-
});
|
|
39
|
+
for (const item of sortedHooks) {
|
|
40
|
+
const hookName = (item as any).moduleName as string;
|
|
41
|
+
const hook = item as any as Hook;
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
43
|
+
hooksMap.push({
|
|
44
|
+
name: hookName,
|
|
45
|
+
deps: hook.deps,
|
|
46
|
+
handler: hook.handler
|
|
47
|
+
});
|
|
65
48
|
}
|
|
49
|
+
|
|
50
|
+
return hooksMap;
|
|
66
51
|
}
|
package/loader/loadPlugins.ts
CHANGED
|
@@ -5,67 +5,52 @@
|
|
|
5
5
|
|
|
6
6
|
import type { BeflyContext } from "../types/befly.js";
|
|
7
7
|
import type { Plugin } from "../types/plugin.js";
|
|
8
|
+
import type { ScanFileResult } from "../utils/scanFiles.js";
|
|
8
9
|
|
|
9
|
-
import { beflyConfig } from "../befly.config.js";
|
|
10
10
|
import { Logger } from "../lib/logger.js";
|
|
11
|
-
import {
|
|
12
|
-
import { scanAddons, getAddonDir } from "../utils/addonHelper.js";
|
|
13
|
-
import { sortModules, scanModules } from "../utils/modules.js";
|
|
11
|
+
import { sortModules } from "../utils/sortModules.js";
|
|
14
12
|
|
|
15
|
-
export async function loadPlugins(plugins:
|
|
16
|
-
|
|
17
|
-
const allPlugins: Plugin[] = [];
|
|
13
|
+
export async function loadPlugins(plugins: ScanFileResult[], context: BeflyContext, disablePlugins: string[] = []): Promise<Plugin[]> {
|
|
14
|
+
const pluginsMap: Plugin[] = [];
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// 2. 扫描组件插件
|
|
23
|
-
const addonPlugins: Plugin[] = [];
|
|
24
|
-
const addons = scanAddons();
|
|
25
|
-
for (const addon of addons) {
|
|
26
|
-
const dir = getAddonDir(addon, "plugins");
|
|
27
|
-
const plugins = await scanModules<Plugin>(dir, "addon", "插件", addon);
|
|
28
|
-
addonPlugins.push(...plugins);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 3. 扫描项目插件
|
|
32
|
-
const appPlugins = await scanModules<Plugin>(projectPluginDir, "app", "插件");
|
|
33
|
-
|
|
34
|
-
// 4. 合并所有插件
|
|
35
|
-
allPlugins.push(...corePlugins);
|
|
36
|
-
allPlugins.push(...addonPlugins);
|
|
37
|
-
allPlugins.push(...appPlugins);
|
|
38
|
-
|
|
39
|
-
// 5. 过滤禁用的插件
|
|
40
|
-
const disablePlugins = beflyConfig.disablePlugins || [];
|
|
41
|
-
const enabledPlugins = allPlugins.filter((plugin) => plugin.name && !disablePlugins.includes(plugin.name));
|
|
16
|
+
if (disablePlugins.length > 0) {
|
|
17
|
+
Logger.info({ plugins: disablePlugins }, "禁用插件");
|
|
18
|
+
}
|
|
42
19
|
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
const enabledPlugins = plugins.filter((item: any) => {
|
|
21
|
+
const moduleName = item?.moduleName;
|
|
22
|
+
if (typeof moduleName !== "string" || moduleName.trim() === "") {
|
|
23
|
+
return false;
|
|
45
24
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const sortedPlugins = sortModules(enabledPlugins);
|
|
49
|
-
if (sortedPlugins === false) {
|
|
50
|
-
Logger.error("插件依赖关系错误,请检查 after 属性");
|
|
51
|
-
process.exit(1);
|
|
25
|
+
if (disablePlugins.includes(moduleName)) {
|
|
26
|
+
return false;
|
|
52
27
|
}
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
53
30
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const pluginInstance = typeof plugin.handler === "function" ? await plugin.handler(context) : {};
|
|
31
|
+
const sortedPlugins = sortModules(enabledPlugins, { moduleLabel: "插件" });
|
|
32
|
+
if (sortedPlugins === false) {
|
|
33
|
+
throw new Error("插件依赖关系错误");
|
|
34
|
+
}
|
|
59
35
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
36
|
+
for (const item of sortedPlugins) {
|
|
37
|
+
const pluginName = (item as any).moduleName as string;
|
|
38
|
+
const plugin = item as any as Plugin;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const pluginInstance = typeof plugin.handler === "function" ? await plugin.handler(context) : {};
|
|
42
|
+
(context as any)[pluginName] = pluginInstance;
|
|
43
|
+
|
|
44
|
+
pluginsMap.push({
|
|
45
|
+
name: pluginName,
|
|
46
|
+
deps: plugin.deps,
|
|
47
|
+
handler: plugin.handler
|
|
48
|
+
});
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
Logger.error({ err: error, plugin: pluginName }, "插件初始化失败");
|
|
51
|
+
throw error;
|
|
66
52
|
}
|
|
67
|
-
} catch (error: any) {
|
|
68
|
-
Logger.error({ err: error }, "加载插件时发生错误");
|
|
69
|
-
process.exit(1);
|
|
70
53
|
}
|
|
54
|
+
|
|
55
|
+
return pluginsMap;
|
|
71
56
|
}
|
package/main.ts
CHANGED
|
@@ -9,11 +9,12 @@ import type { BeflyContext, BeflyOptions } from "./types/befly.js";
|
|
|
9
9
|
import type { Hook } from "./types/hook.js";
|
|
10
10
|
import type { Plugin } from "./types/plugin.js";
|
|
11
11
|
|
|
12
|
-
// ========== 相对导入(项目内部文件) ==========
|
|
13
|
-
// 启动检查
|
|
14
12
|
import { checkApi } from "./checks/checkApi.js";
|
|
15
|
-
import {
|
|
13
|
+
import { checkHook } from "./checks/checkHook.js";
|
|
14
|
+
import { checkMenu } from "./checks/checkMenu.js";
|
|
15
|
+
import { checkPlugin } from "./checks/checkPlugin.js";
|
|
16
16
|
import { checkTable } from "./checks/checkTable.js";
|
|
17
|
+
// ========== 相对导入(项目内部文件) ==========
|
|
17
18
|
// 基础设施
|
|
18
19
|
import { Connect } from "./lib/connect.js";
|
|
19
20
|
import { Logger } from "./lib/logger.js";
|
|
@@ -25,10 +26,15 @@ import { loadPlugins } from "./loader/loadPlugins.js";
|
|
|
25
26
|
import { apiHandler } from "./router/api.js";
|
|
26
27
|
import { staticHandler } from "./router/static.js";
|
|
27
28
|
// 同步
|
|
28
|
-
import {
|
|
29
|
+
import { syncApi } from "./sync/syncApi.js";
|
|
30
|
+
import { syncCache } from "./sync/syncCache.js";
|
|
31
|
+
import { syncDev } from "./sync/syncDev.js";
|
|
32
|
+
import { syncMenu } from "./sync/syncMenu.js";
|
|
33
|
+
import { syncTable } from "./sync/syncTable.js";
|
|
29
34
|
// 工具
|
|
30
35
|
import { calcPerfTime } from "./utils/calcPerfTime.js";
|
|
31
|
-
import {
|
|
36
|
+
import { getProcessRole } from "./utils/process.js";
|
|
37
|
+
import { scanSources } from "./utils/scanSources.js";
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* Befly 框架核心类
|
|
@@ -62,28 +68,61 @@ export class Befly {
|
|
|
62
68
|
const { beflyConfig } = await import("./befly.config.js");
|
|
63
69
|
this.config = beflyConfig;
|
|
64
70
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await
|
|
71
|
+
// 将配置注入到 ctx,供插件/Hook/sync 等按需读取
|
|
72
|
+
this.context.config = this.config;
|
|
73
|
+
|
|
74
|
+
const { apis, tables, plugins, hooks, addons } = await scanSources();
|
|
75
|
+
|
|
76
|
+
// 让后续 syncMenu 能拿到 addon 的 views 路径等信息
|
|
77
|
+
this.context.addons = addons;
|
|
78
|
+
|
|
79
|
+
await checkApi(apis);
|
|
80
|
+
await checkTable(tables);
|
|
81
|
+
await checkPlugin(plugins);
|
|
82
|
+
await checkHook(hooks);
|
|
83
|
+
const checkedMenus = await checkMenu(addons, { disableMenus: this.config.disableMenus || [] });
|
|
84
|
+
|
|
85
|
+
// 1. 启动期建立基础连接(SQL + Redis)
|
|
86
|
+
// 说明:连接职责收敛到启动期单点;插件只消费已连接实例(Connect.getSql/getRedis)。
|
|
87
|
+
await Connect.connect({
|
|
88
|
+
db: this.config.db || {},
|
|
89
|
+
redis: this.config.redis || {}
|
|
90
|
+
});
|
|
69
91
|
|
|
70
92
|
// 2. 加载插件
|
|
71
|
-
await loadPlugins(
|
|
93
|
+
this.plugins = await loadPlugins(plugins as any, this.context as BeflyContext, this.config!.disablePlugins || []);
|
|
94
|
+
|
|
95
|
+
// 启动期依赖完整性检查:避免 sync 阶段出现 undefined 调用
|
|
96
|
+
// 注意:这里不做兼容别名(例如 dbHelper=db),要求上下文必须注入标准字段。
|
|
97
|
+
if (!(this.context as any).redis) {
|
|
98
|
+
throw new Error("启动失败:ctx.redis 未初始化(Redis 插件未加载或注入失败)");
|
|
99
|
+
}
|
|
100
|
+
if (!(this.context as any).db) {
|
|
101
|
+
throw new Error("启动失败:ctx.db 未初始化(Db 插件未加载或注入失败)");
|
|
102
|
+
}
|
|
103
|
+
if (!(this.context as any).cache) {
|
|
104
|
+
throw new Error("启动失败:ctx.cache 未初始化(cache 插件未加载或注入失败)");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 5. 自动同步 (仅主进程执行,避免集群模式下重复执行)
|
|
108
|
+
await syncTable(this.context as BeflyContext, tables);
|
|
109
|
+
await syncApi(this.context as BeflyContext, apis as any);
|
|
110
|
+
|
|
111
|
+
await syncMenu(this.context as BeflyContext, checkedMenus);
|
|
112
|
+
await syncDev(this.context as BeflyContext, { devEmail: this.config.devEmail, devPassword: this.config.devPassword });
|
|
113
|
+
|
|
114
|
+
// 缓存同步:统一在所有同步完成后执行(cacheApis + cacheMenus + rebuildRoleApiPermissions)
|
|
115
|
+
await syncCache(this.context as BeflyContext);
|
|
72
116
|
|
|
73
117
|
// 3. 加载钩子
|
|
74
|
-
await loadHooks(this.
|
|
118
|
+
this.hooks = await loadHooks(hooks as any, this.config!.disableHooks || []);
|
|
75
119
|
|
|
76
120
|
// 4. 加载所有 API
|
|
77
|
-
await loadApis(
|
|
78
|
-
|
|
79
|
-
// 5. 自动同步 (仅主进程执行,避免集群模式下重复执行)
|
|
80
|
-
if (isPrimaryProcess()) {
|
|
81
|
-
await syncAllCommand();
|
|
82
|
-
}
|
|
121
|
+
this.apis = await loadApis(apis as any);
|
|
83
122
|
|
|
84
|
-
// 6. 启动 HTTP
|
|
123
|
+
// 6. 启动 HTTP服务器
|
|
85
124
|
const apiFetch = apiHandler(this.apis, this.hooks, this.context as BeflyContext);
|
|
86
|
-
const staticFetch = staticHandler();
|
|
125
|
+
const staticFetch = staticHandler(this.config!.cors);
|
|
87
126
|
|
|
88
127
|
const server = Bun.serve({
|
|
89
128
|
port: this.config!.appPort,
|