befly 3.9.40 → 3.10.1
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 +47 -19
- 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 +17 -11
- package/docs/api/api.md +16 -2
- package/docs/guide/quickstart.md +31 -10
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +26 -14
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +5 -328
- package/docs/reference/addon.md +0 -4
- package/docs/reference/config.md +14 -31
- package/docs/reference/logger.md +3 -3
- package/docs/reference/sync.md +132 -237
- package/docs/reference/table.md +28 -30
- 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 +3 -4
- 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/docs/cipher.md +0 -582
- package/docs/database.md +0 -1176
- 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/hooks/cors.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { CorsConfig } from "../types/befly.js";
|
|
|
2
2
|
// 类型导入
|
|
3
3
|
import type { Hook } from "../types/hook.js";
|
|
4
4
|
|
|
5
|
-
import { beflyConfig } from "../befly.config.js";
|
|
6
5
|
// 相对导入
|
|
7
6
|
import { setCorsOptions } from "../utils/cors.js";
|
|
8
7
|
|
|
@@ -10,8 +9,8 @@ import { setCorsOptions } from "../utils/cors.js";
|
|
|
10
9
|
* CORS 跨域处理钩子
|
|
11
10
|
* 设置跨域响应头并处理 OPTIONS 预检请求
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export default {
|
|
13
|
+
deps: [],
|
|
15
14
|
handler: async (befly, ctx) => {
|
|
16
15
|
const req = ctx.req;
|
|
17
16
|
|
|
@@ -25,7 +24,7 @@ const hook: Hook = {
|
|
|
25
24
|
credentials: "true"
|
|
26
25
|
};
|
|
27
26
|
|
|
28
|
-
const corsConfig = {
|
|
27
|
+
const corsConfig = Object.assign({}, defaultConfig, befly.config && befly.config.cors ? befly.config.cors : {});
|
|
29
28
|
|
|
30
29
|
// 设置 CORS 响应头
|
|
31
30
|
const headers = setCorsOptions(req, corsConfig);
|
|
@@ -41,5 +40,4 @@ const hook: Hook = {
|
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
|
-
};
|
|
45
|
-
export default hook;
|
|
43
|
+
} satisfies Hook;
|
package/hooks/parser.ts
CHANGED
|
@@ -18,8 +18,8 @@ const xmlParser = new XMLParser();
|
|
|
18
18
|
* - 根据 API 定义的 fields 过滤字段
|
|
19
19
|
* - rawBody: true 时跳过解析,由 handler 自行处理原始请求
|
|
20
20
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
export default {
|
|
22
|
+
deps: ["auth"],
|
|
23
23
|
handler: async (befly, ctx) => {
|
|
24
24
|
if (!ctx.api) return;
|
|
25
25
|
|
|
@@ -103,5 +103,4 @@ const hook: Hook = {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
};
|
|
107
|
-
export default hook;
|
|
106
|
+
} satisfies Hook;
|
package/hooks/permission.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { ErrorResponse } from "../utils/response.js";
|
|
|
13
13
|
* - 开发者角色(dev):最高权限,直接通过
|
|
14
14
|
* - 其他角色:检查 Redis 中的角色权限集合
|
|
15
15
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
export default {
|
|
17
|
+
deps: ["validator"],
|
|
18
18
|
handler: async (befly, ctx) => {
|
|
19
19
|
if (!ctx.api) return;
|
|
20
20
|
|
|
@@ -65,5 +65,4 @@ const hook: Hook = {
|
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
};
|
|
69
|
-
export default hook;
|
|
68
|
+
} satisfies Hook;
|
package/hooks/validator.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { ErrorResponse } from "../utils/response.js";
|
|
|
9
9
|
* 参数验证钩子
|
|
10
10
|
* 根据 API 定义的 fields 和 required 验证请求参数
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export default {
|
|
13
|
+
deps: ["parser"],
|
|
14
14
|
handler: async (befly, ctx) => {
|
|
15
15
|
if (!ctx.api) return;
|
|
16
16
|
|
|
@@ -35,5 +35,4 @@ const hook: Hook = {
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
};
|
|
39
|
-
export default hook;
|
|
38
|
+
} satisfies Hook;
|
package/lib/cacheHelper.ts
CHANGED
|
@@ -3,122 +3,79 @@
|
|
|
3
3
|
* 负责在服务器启动时缓存接口、菜单和角色权限到 Redis
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { BeflyContext } from "../types/befly.js";
|
|
7
|
-
|
|
8
|
-
import { makeRouteKey } from "../utils/route.js";
|
|
9
6
|
import { CacheKeys } from "./cacheKeys.js";
|
|
10
7
|
import { Logger } from "./logger.js";
|
|
11
8
|
|
|
9
|
+
type CacheHelperDb = {
|
|
10
|
+
tableExists(table: string): Promise<boolean>;
|
|
11
|
+
getAll(options: any): Promise<{ lists: any[] }>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type CacheHelperRedis = {
|
|
15
|
+
setObject<T = any>(key: string, obj: T, ttl?: number | null): Promise<string | null>;
|
|
16
|
+
getObject<T = any>(key: string): Promise<T | null>;
|
|
17
|
+
del(key: string): Promise<number>;
|
|
18
|
+
delBatch(keys: string[]): Promise<number>;
|
|
19
|
+
sadd(key: string, members: string[]): Promise<number>;
|
|
20
|
+
saddBatch(items: Array<{ key: string; members: string[] }>): Promise<number>;
|
|
21
|
+
smembers(key: string): Promise<string[]>;
|
|
22
|
+
sismember(key: string, member: string): Promise<boolean>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type CacheHelperDeps = {
|
|
26
|
+
db: CacheHelperDb;
|
|
27
|
+
redis: CacheHelperRedis;
|
|
28
|
+
};
|
|
29
|
+
|
|
12
30
|
/**
|
|
13
31
|
* 缓存助手类
|
|
14
32
|
*/
|
|
15
33
|
export class CacheHelper {
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
private static readonly API_ID_IN_CHUNK_SIZE = 1000;
|
|
34
|
+
private db: CacheHelperDb;
|
|
35
|
+
private redis: CacheHelperRedis;
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
constructor(deps: CacheHelperDeps) {
|
|
38
|
+
this.db = deps.db;
|
|
39
|
+
this.redis = deps.redis;
|
|
40
|
+
}
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const intValue = Math.trunc(item);
|
|
27
|
-
return Number.isSafeInteger(intValue) ? intValue : null;
|
|
28
|
-
}
|
|
29
|
-
if (typeof item === "bigint") {
|
|
30
|
-
const intValue = Number(item);
|
|
31
|
-
return Number.isSafeInteger(intValue) ? intValue : null;
|
|
32
|
-
}
|
|
33
|
-
if (typeof item === "string") {
|
|
34
|
-
const trimmed = item.trim();
|
|
35
|
-
if (!trimmed) return null;
|
|
36
|
-
const intValue = Number.parseInt(trimmed, 10);
|
|
37
|
-
return Number.isSafeInteger(intValue) ? intValue : null;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
if (Array.isArray(value)) {
|
|
43
|
-
const ids: number[] = [];
|
|
44
|
-
for (const item of value) {
|
|
45
|
-
const id = normalizeSingle(item);
|
|
46
|
-
if (id !== null) ids.push(id);
|
|
47
|
-
}
|
|
48
|
-
return ids;
|
|
42
|
+
private assertApiPathname(value: unknown, errorPrefix: string): string {
|
|
43
|
+
if (typeof value !== "string") {
|
|
44
|
+
throw new Error(`${errorPrefix} 必须是字符串`);
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (str.startsWith("[")) {
|
|
56
|
-
try {
|
|
57
|
-
const parsed = JSON.parse(str);
|
|
58
|
-
return this.normalizeNumberIdList(parsed);
|
|
59
|
-
} catch {
|
|
60
|
-
// ignore
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const ids: number[] = [];
|
|
65
|
-
const parts = str.split(",");
|
|
66
|
-
for (const part of parts) {
|
|
67
|
-
const id = normalizeSingle(part);
|
|
68
|
-
if (id !== null) ids.push(id);
|
|
69
|
-
}
|
|
70
|
-
return ids;
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (!trimmed) {
|
|
49
|
+
throw new Error(`${errorPrefix} 不允许为空字符串`);
|
|
71
50
|
}
|
|
72
51
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private chunkNumberArray(arr: number[], chunkSize: number): number[][] {
|
|
78
|
-
if (chunkSize <= 0) {
|
|
79
|
-
throw new Error(`chunkSize 必须为正整数 (chunkSize: ${chunkSize})`);
|
|
52
|
+
if (/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/i.test(trimmed)) {
|
|
53
|
+
throw new Error(`${errorPrefix} 不允许包含 method 前缀,应为 url.pathname(例如 /api/app/xxx)`);
|
|
80
54
|
}
|
|
81
|
-
if (arr.length === 0) return [];
|
|
82
55
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
chunks.push(arr.slice(i, i + chunkSize));
|
|
56
|
+
if (!trimmed.startsWith("/")) {
|
|
57
|
+
throw new Error(`${errorPrefix} 必须是 pathname(以 / 开头)`);
|
|
86
58
|
}
|
|
87
|
-
return chunks;
|
|
88
|
-
}
|
|
89
59
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (uniqueIds.length === 0) {
|
|
93
|
-
return new Map();
|
|
60
|
+
if (trimmed.includes(" ")) {
|
|
61
|
+
throw new Error(`${errorPrefix} 不允许包含空格`);
|
|
94
62
|
}
|
|
95
63
|
|
|
96
|
-
|
|
97
|
-
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
98
66
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
table: "addon_admin_api",
|
|
102
|
-
fields: ["id", "method", "path"],
|
|
103
|
-
where: {
|
|
104
|
-
id$in: chunk
|
|
105
|
-
}
|
|
106
|
-
});
|
|
67
|
+
private assertApiPathList(value: unknown, roleCode: string): string[] {
|
|
68
|
+
if (value === null || value === undefined) return [];
|
|
107
69
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
throw new Error(`角色权限数据不合法:addon_admin_role.apis 必须是字符串数组,roleCode=${roleCode}`);
|
|
111
72
|
}
|
|
112
73
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
* @param befly - Befly 上下文
|
|
119
|
-
*/
|
|
120
|
-
constructor(befly: BeflyContext) {
|
|
121
|
-
this.befly = befly;
|
|
74
|
+
const out: string[] = [];
|
|
75
|
+
for (const item of value) {
|
|
76
|
+
out.push(this.assertApiPathname(item, `角色权限数据不合法:addon_admin_role.apis 元素,roleCode=${roleCode}`));
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
122
79
|
}
|
|
123
80
|
|
|
124
81
|
/**
|
|
@@ -127,19 +84,19 @@ export class CacheHelper {
|
|
|
127
84
|
async cacheApis(): Promise<void> {
|
|
128
85
|
try {
|
|
129
86
|
// 检查表是否存在
|
|
130
|
-
const tableExists = await this.
|
|
87
|
+
const tableExists = await this.db.tableExists("addon_admin_api");
|
|
131
88
|
if (!tableExists) {
|
|
132
89
|
Logger.warn("⚠️ 接口表不存在,跳过接口缓存");
|
|
133
90
|
return;
|
|
134
91
|
}
|
|
135
92
|
|
|
136
93
|
// 从数据库查询所有接口
|
|
137
|
-
const apiList = await this.
|
|
94
|
+
const apiList = await this.db.getAll({
|
|
138
95
|
table: "addon_admin_api"
|
|
139
96
|
});
|
|
140
97
|
|
|
141
98
|
// 缓存到 Redis
|
|
142
|
-
const result = await this.
|
|
99
|
+
const result = await this.redis.setObject(CacheKeys.apisAll(), apiList.lists);
|
|
143
100
|
|
|
144
101
|
if (result === null) {
|
|
145
102
|
Logger.warn("⚠️ 接口缓存失败");
|
|
@@ -155,19 +112,19 @@ export class CacheHelper {
|
|
|
155
112
|
async cacheMenus(): Promise<void> {
|
|
156
113
|
try {
|
|
157
114
|
// 检查表是否存在
|
|
158
|
-
const tableExists = await this.
|
|
115
|
+
const tableExists = await this.db.tableExists("addon_admin_menu");
|
|
159
116
|
if (!tableExists) {
|
|
160
117
|
Logger.warn("⚠️ 菜单表不存在,跳过菜单缓存");
|
|
161
118
|
return;
|
|
162
119
|
}
|
|
163
120
|
|
|
164
121
|
// 从数据库查询所有菜单
|
|
165
|
-
const menus = await this.
|
|
122
|
+
const menus = await this.db.getAll({
|
|
166
123
|
table: "addon_admin_menu"
|
|
167
124
|
});
|
|
168
125
|
|
|
169
126
|
// 缓存到 Redis
|
|
170
|
-
const result = await this.
|
|
127
|
+
const result = await this.redis.setObject(CacheKeys.menusAll(), menus.lists);
|
|
171
128
|
|
|
172
129
|
if (result === null) {
|
|
173
130
|
Logger.warn("⚠️ 菜单缓存失败");
|
|
@@ -185,61 +142,43 @@ export class CacheHelper {
|
|
|
185
142
|
async rebuildRoleApiPermissions(): Promise<void> {
|
|
186
143
|
try {
|
|
187
144
|
// 检查表是否存在
|
|
188
|
-
const
|
|
189
|
-
const roleTableExists = await this.befly.db.tableExists("addon_admin_role");
|
|
145
|
+
const roleTableExists = await this.db.tableExists("addon_admin_role");
|
|
190
146
|
|
|
191
|
-
if (!
|
|
192
|
-
Logger.warn("⚠️
|
|
147
|
+
if (!roleTableExists) {
|
|
148
|
+
Logger.warn("⚠️ 角色表不存在,跳过角色权限缓存");
|
|
193
149
|
return;
|
|
194
150
|
}
|
|
195
151
|
|
|
196
152
|
// 查询所有角色(仅取必要字段)
|
|
197
|
-
const roles = await this.
|
|
153
|
+
const roles = await this.db.getAll({
|
|
198
154
|
table: "addon_admin_role",
|
|
199
155
|
fields: ["code", "apis"]
|
|
200
156
|
});
|
|
201
157
|
|
|
202
|
-
const
|
|
203
|
-
const allApiIdsSet = new Set<number>();
|
|
158
|
+
const roleApiPathsMap = new Map<string, string[]>();
|
|
204
159
|
|
|
205
160
|
for (const role of roles.lists) {
|
|
206
161
|
if (!role?.code) continue;
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
for (const id of apiIds) {
|
|
210
|
-
allApiIdsSet.add(id);
|
|
211
|
-
}
|
|
162
|
+
const apiPaths = this.assertApiPathList(role.apis, role.code);
|
|
163
|
+
roleApiPathsMap.set(role.code, apiPaths);
|
|
212
164
|
}
|
|
213
165
|
|
|
214
|
-
const roleCodes = Array.from(
|
|
166
|
+
const roleCodes = Array.from(roleApiPathsMap.keys());
|
|
215
167
|
if (roleCodes.length === 0) {
|
|
216
168
|
Logger.info("✅ 没有需要缓存的角色权限");
|
|
217
169
|
return;
|
|
218
170
|
}
|
|
219
171
|
|
|
220
|
-
// 构建所有需要的 API 映射(按 ID 分块使用 $in,避免全表扫描/避免超长 IN)
|
|
221
|
-
const allApiIds = Array.from(allApiIdsSet);
|
|
222
|
-
const apiMap = await this.buildApiPathMapByIds(allApiIds);
|
|
223
|
-
|
|
224
172
|
// 清理所有角色的缓存 key(保证幂等)
|
|
225
173
|
const roleKeys = roleCodes.map((code) => CacheKeys.roleApis(code));
|
|
226
|
-
await this.
|
|
174
|
+
await this.redis.delBatch(roleKeys);
|
|
227
175
|
|
|
228
176
|
// 批量写入新缓存(只写入非空权限)
|
|
229
177
|
const items: Array<{ key: string; members: string[] }> = [];
|
|
230
178
|
|
|
231
179
|
for (const roleCode of roleCodes) {
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
for (const id of apiIds) {
|
|
236
|
-
const apiPath = apiMap.get(id);
|
|
237
|
-
if (apiPath) {
|
|
238
|
-
membersSet.add(apiPath);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const members = Array.from(membersSet).sort();
|
|
180
|
+
const apiPaths = roleApiPathsMap.get(roleCode) || [];
|
|
181
|
+
const members = Array.from(new Set(apiPaths)).sort();
|
|
243
182
|
|
|
244
183
|
if (members.length > 0) {
|
|
245
184
|
items.push({ key: CacheKeys.roleApis(roleCode), members: members });
|
|
@@ -247,12 +186,13 @@ export class CacheHelper {
|
|
|
247
186
|
}
|
|
248
187
|
|
|
249
188
|
if (items.length > 0) {
|
|
250
|
-
await this.
|
|
189
|
+
await this.redis.saddBatch(items);
|
|
251
190
|
}
|
|
252
191
|
|
|
253
192
|
// 极简方案不做版本/ready/meta:重建完成即生效
|
|
254
193
|
} catch (error: any) {
|
|
255
|
-
Logger.
|
|
194
|
+
Logger.error({ err: error }, "⚠️ 角色权限缓存异常(将阻断启动)");
|
|
195
|
+
throw error;
|
|
256
196
|
}
|
|
257
197
|
}
|
|
258
198
|
|
|
@@ -261,33 +201,28 @@ export class CacheHelper {
|
|
|
261
201
|
* - apiIds 为空数组:仅清理缓存(防止残留)
|
|
262
202
|
* - apiIds 非空:使用 $in 最小查询,DEL 后 SADD
|
|
263
203
|
*/
|
|
264
|
-
async refreshRoleApiPermissions(roleCode: string,
|
|
204
|
+
async refreshRoleApiPermissions(roleCode: string, apiPaths: string[]): Promise<void> {
|
|
265
205
|
if (!roleCode || typeof roleCode !== "string") {
|
|
266
206
|
throw new Error("roleCode 必须是非空字符串");
|
|
267
207
|
}
|
|
268
|
-
|
|
208
|
+
if (!Array.isArray(apiPaths)) {
|
|
209
|
+
throw new Error("apiPaths 必须是数组");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const normalizedPaths = apiPaths.map((p) => this.assertApiPathname(p, `refreshRoleApiPermissions: apiPaths 元素,roleCode=${roleCode}`));
|
|
269
213
|
const roleKey = CacheKeys.roleApis(roleCode);
|
|
270
214
|
|
|
271
|
-
//
|
|
272
|
-
if (
|
|
273
|
-
await this.
|
|
215
|
+
// 空数组短路:保证清理残留
|
|
216
|
+
if (normalizedPaths.length === 0) {
|
|
217
|
+
await this.redis.del(roleKey);
|
|
274
218
|
return;
|
|
275
219
|
}
|
|
276
220
|
|
|
277
|
-
const
|
|
278
|
-
const membersSet = new Set<string>();
|
|
279
|
-
for (const id of normalizedIds) {
|
|
280
|
-
const apiPath = apiMap.get(id);
|
|
281
|
-
if (apiPath) {
|
|
282
|
-
membersSet.add(apiPath);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const members = Array.from(membersSet);
|
|
221
|
+
const members = Array.from(new Set(normalizedPaths));
|
|
287
222
|
|
|
288
|
-
await this.
|
|
223
|
+
await this.redis.del(roleKey);
|
|
289
224
|
if (members.length > 0) {
|
|
290
|
-
await this.
|
|
225
|
+
await this.redis.sadd(roleKey, members);
|
|
291
226
|
}
|
|
292
227
|
}
|
|
293
228
|
|
|
@@ -311,7 +246,7 @@ export class CacheHelper {
|
|
|
311
246
|
*/
|
|
312
247
|
async getApis(): Promise<any[]> {
|
|
313
248
|
try {
|
|
314
|
-
const apis = await this.
|
|
249
|
+
const apis = await this.redis.getObject<any[]>(CacheKeys.apisAll());
|
|
315
250
|
return apis || [];
|
|
316
251
|
} catch (error: any) {
|
|
317
252
|
Logger.error({ err: error }, "获取接口缓存失败");
|
|
@@ -325,7 +260,7 @@ export class CacheHelper {
|
|
|
325
260
|
*/
|
|
326
261
|
async getMenus(): Promise<any[]> {
|
|
327
262
|
try {
|
|
328
|
-
const menus = await this.
|
|
263
|
+
const menus = await this.redis.getObject<any[]>(CacheKeys.menusAll());
|
|
329
264
|
return menus || [];
|
|
330
265
|
} catch (error: any) {
|
|
331
266
|
Logger.error({ err: error }, "获取菜单缓存失败");
|
|
@@ -340,7 +275,7 @@ export class CacheHelper {
|
|
|
340
275
|
*/
|
|
341
276
|
async getRolePermissions(roleCode: string): Promise<string[]> {
|
|
342
277
|
try {
|
|
343
|
-
const permissions = await this.
|
|
278
|
+
const permissions = await this.redis.smembers(CacheKeys.roleApis(roleCode));
|
|
344
279
|
return permissions || [];
|
|
345
280
|
} catch (error: any) {
|
|
346
281
|
Logger.error({ err: error, roleCode: roleCode }, "获取角色权限缓存失败");
|
|
@@ -351,12 +286,13 @@ export class CacheHelper {
|
|
|
351
286
|
/**
|
|
352
287
|
* 检查角色是否有指定接口权限
|
|
353
288
|
* @param roleCode - 角色代码
|
|
354
|
-
* @param apiPath -
|
|
289
|
+
* @param apiPath - 接口路径(url.pathname,例如 /api/user/login;与 method 无关)
|
|
355
290
|
* @returns 是否有权限
|
|
356
291
|
*/
|
|
357
292
|
async checkRolePermission(roleCode: string, apiPath: string): Promise<boolean> {
|
|
358
293
|
try {
|
|
359
|
-
|
|
294
|
+
const pathname = this.assertApiPathname(apiPath, "checkRolePermission: apiPath");
|
|
295
|
+
return await this.redis.sismember(CacheKeys.roleApis(roleCode), pathname);
|
|
360
296
|
} catch (error: any) {
|
|
361
297
|
Logger.error({ err: error, roleCode: roleCode }, "检查角色权限失败");
|
|
362
298
|
return false;
|
|
@@ -370,7 +306,7 @@ export class CacheHelper {
|
|
|
370
306
|
*/
|
|
371
307
|
async deleteRolePermissions(roleCode: string): Promise<boolean> {
|
|
372
308
|
try {
|
|
373
|
-
const result = await this.
|
|
309
|
+
const result = await this.redis.del(CacheKeys.roleApis(roleCode));
|
|
374
310
|
if (result > 0) {
|
|
375
311
|
Logger.info(`✅ 已删除角色 ${roleCode} 的权限缓存`);
|
|
376
312
|
return true;
|
package/lib/cacheKeys.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class CacheKeys {
|
|
|
25
25
|
/**
|
|
26
26
|
* 角色接口权限缓存(Set 集合)
|
|
27
27
|
* - key: befly:role:apis:${roleCode}
|
|
28
|
-
* - member:
|
|
28
|
+
* - member: url.pathname(例如 /api/user/login;与 method 无关)
|
|
29
29
|
*/
|
|
30
30
|
static roleApis(roleCode: string): string {
|
|
31
31
|
return `befly:role:apis:${roleCode}`;
|
package/lib/connect.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 数据库连接管理器
|
|
3
3
|
* 统一管理 SQL 和 Redis 连接
|
|
4
|
-
* 配置从 beflyConfig 全局对象获取
|
|
5
4
|
*/
|
|
6
5
|
|
|
6
|
+
import type { DatabaseConfig, RedisConfig } from "../types/befly.js";
|
|
7
|
+
|
|
7
8
|
import { SQL, RedisClient } from "bun";
|
|
8
9
|
|
|
9
|
-
import { beflyConfig } from "../befly.config.js";
|
|
10
10
|
import { Logger } from "./logger.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 数据库连接管理器
|
|
14
14
|
* 使用静态方法管理全局单例连接
|
|
15
|
-
* 所有配置从 beflyConfig 自动获取
|
|
16
15
|
*/
|
|
17
16
|
export class Connect {
|
|
18
17
|
private static sqlClient: SQL | null = null;
|
|
@@ -29,11 +28,10 @@ export class Connect {
|
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* 连接 SQL 数据库
|
|
32
|
-
* 配置从 beflyConfig.db 获取
|
|
33
31
|
* @returns SQL 客户端实例
|
|
34
32
|
*/
|
|
35
|
-
static async connectSql(): Promise<SQL> {
|
|
36
|
-
const config =
|
|
33
|
+
static async connectSql(dbConfig: DatabaseConfig): Promise<SQL> {
|
|
34
|
+
const config = dbConfig || {};
|
|
37
35
|
|
|
38
36
|
// 构建数据库连接字符串
|
|
39
37
|
const type = config.type || "mysql";
|
|
@@ -137,11 +135,10 @@ export class Connect {
|
|
|
137
135
|
|
|
138
136
|
/**
|
|
139
137
|
* 连接 Redis
|
|
140
|
-
* 配置从 beflyConfig.redis 获取
|
|
141
138
|
* @returns Redis 客户端实例
|
|
142
139
|
*/
|
|
143
|
-
static async connectRedis(): Promise<RedisClient> {
|
|
144
|
-
const config =
|
|
140
|
+
static async connectRedis(redisConfig: RedisConfig): Promise<RedisClient> {
|
|
141
|
+
const config = redisConfig || {};
|
|
145
142
|
|
|
146
143
|
try {
|
|
147
144
|
// 构建 Redis URL
|
|
@@ -212,15 +209,14 @@ export class Connect {
|
|
|
212
209
|
|
|
213
210
|
/**
|
|
214
211
|
* 连接所有数据库(SQL + Redis)
|
|
215
|
-
* 配置从 beflyConfig 自动获取
|
|
216
212
|
*/
|
|
217
|
-
static async connect(): Promise<void> {
|
|
213
|
+
static async connect(config: { db: DatabaseConfig; redis: RedisConfig }): Promise<void> {
|
|
218
214
|
try {
|
|
219
215
|
// 连接 SQL
|
|
220
|
-
await this.connectSql();
|
|
216
|
+
await this.connectSql(config.db || {});
|
|
221
217
|
|
|
222
218
|
// 连接 Redis
|
|
223
|
-
await this.connectRedis();
|
|
219
|
+
await this.connectRedis(config.redis || {});
|
|
224
220
|
} catch (error: any) {
|
|
225
221
|
Logger.error({ err: error }, "数据库初始化失败");
|
|
226
222
|
await this.disconnect();
|