befly 3.9.38 → 3.9.40
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 +37 -38
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +1 -1
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +8 -8
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +48 -44
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -52
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -65
- package/sync/syncMenu.ts +190 -55
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
package/router/api.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* API路由处理器
|
|
3
3
|
* 处理 /api/* 路径的请求
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
import {
|
|
6
|
+
import type { ApiRoute } from "../types/api.js";
|
|
7
|
+
import type { BeflyContext } from "../types/befly.js";
|
|
8
|
+
// 类型导入
|
|
9
|
+
import type { RequestContext } from "../types/context.js";
|
|
10
|
+
import type { Hook } from "../types/hook.js";
|
|
8
11
|
|
|
12
|
+
import { withCtx } from "../lib/asyncContext.js";
|
|
13
|
+
import { Logger } from "../lib/logger.js";
|
|
9
14
|
// 相对导入
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import type { RequestContext } from '../types/context.js';
|
|
15
|
-
import type { ApiRoute } from '../types/api.js';
|
|
16
|
-
import type { Hook } from '../types/hook.js';
|
|
17
|
-
import type { BeflyContext } from '../types/befly.js';
|
|
15
|
+
import { genShortId } from "../utils/genShortId.js";
|
|
16
|
+
import { getClientIp } from "../utils/getClientIp.js";
|
|
17
|
+
import { FinalResponse } from "../utils/response.js";
|
|
18
|
+
import { makeRouteKey } from "../utils/route.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* API处理器工厂函数
|
|
@@ -23,78 +24,106 @@ import type { BeflyContext } from '../types/befly.js';
|
|
|
23
24
|
* @param context - 应用上下文
|
|
24
25
|
*/
|
|
25
26
|
export function apiHandler(apis: Map<string, ApiRoute>, hooks: Hook[], context: BeflyContext) {
|
|
26
|
-
return async (req: Request): Promise<Response> => {
|
|
27
|
+
return async (req: Request, server?: any): Promise<Response> => {
|
|
27
28
|
// 1. 生成请求 ID
|
|
28
29
|
const requestId = genShortId();
|
|
29
30
|
|
|
30
31
|
// 2. 创建请求上下文
|
|
31
32
|
const url = new URL(req.url);
|
|
32
|
-
const apiPath =
|
|
33
|
-
|
|
33
|
+
const apiPath = makeRouteKey(req.method, url.pathname);
|
|
34
|
+
|
|
35
|
+
const clientIp = getClientIp(req, server);
|
|
36
|
+
|
|
37
|
+
const now = Date.now();
|
|
34
38
|
|
|
35
39
|
const ctx: RequestContext = {
|
|
36
40
|
method: req.method,
|
|
37
41
|
body: {},
|
|
38
42
|
user: {},
|
|
39
43
|
req: req,
|
|
40
|
-
now:
|
|
44
|
+
now: now,
|
|
41
45
|
ip: clientIp,
|
|
42
46
|
headers: req.headers,
|
|
43
47
|
route: apiPath,
|
|
44
48
|
requestId: requestId,
|
|
45
49
|
corsHeaders: {
|
|
46
|
-
|
|
50
|
+
"X-Request-ID": requestId
|
|
47
51
|
},
|
|
48
52
|
api: apis.get(apiPath),
|
|
49
53
|
response: undefined,
|
|
50
54
|
result: undefined
|
|
51
55
|
};
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
return withCtx(
|
|
58
|
+
{
|
|
59
|
+
requestId: requestId,
|
|
60
|
+
method: req.method,
|
|
61
|
+
route: apiPath,
|
|
62
|
+
ip: clientIp,
|
|
63
|
+
now: now
|
|
64
|
+
},
|
|
65
|
+
async () => {
|
|
66
|
+
try {
|
|
67
|
+
// 4. 串联执行所有钩子
|
|
68
|
+
for (const hook of hooks) {
|
|
69
|
+
await hook.handler(context, ctx);
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
// 如果钩子已经设置了 response,停止执行
|
|
72
|
+
if (ctx.response) {
|
|
73
|
+
return ctx.response;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// hooks 全部通过后记录请求日志(拦截请求仅由 ErrorResponse 记录)
|
|
78
|
+
if (ctx.api && req.method !== "OPTIONS") {
|
|
79
|
+
const logData: Record<string, any> = {
|
|
80
|
+
event: "request",
|
|
81
|
+
apiName: ctx.api.name
|
|
82
|
+
};
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (req.method !== 'OPTIONS') {
|
|
67
|
-
ctx.response = Response.json(
|
|
68
|
-
{
|
|
69
|
-
code: 1,
|
|
70
|
-
msg: '接口不存在'
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
headers: ctx.corsHeaders
|
|
84
|
+
if (ctx.body && Object.keys(ctx.body).length > 0) {
|
|
85
|
+
logData.body = ctx.body;
|
|
74
86
|
}
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
} else if (ctx.api.handler) {
|
|
78
|
-
const result = await ctx.api.handler(context, ctx);
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
Logger.info(logData, "request");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. 执行 API handler
|
|
92
|
+
if (!ctx.api) {
|
|
93
|
+
if (req.method !== "OPTIONS") {
|
|
94
|
+
ctx.response = Response.json(
|
|
95
|
+
{
|
|
96
|
+
code: 1,
|
|
97
|
+
msg: "接口不存在"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
headers: ctx.corsHeaders
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
} else if (ctx.api.handler) {
|
|
105
|
+
const result = await ctx.api.handler(context, ctx);
|
|
106
|
+
|
|
107
|
+
if (result instanceof Response) {
|
|
108
|
+
ctx.response = result;
|
|
109
|
+
} else {
|
|
110
|
+
ctx.result = result;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 7. 返回响应(自动处理 response/result/日志)
|
|
115
|
+
return FinalResponse(ctx);
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
// 全局错误处理
|
|
118
|
+
const errorPath = ctx.api ? apiPath : req.url;
|
|
119
|
+
Logger.error({ err: err, path: errorPath }, "请求错误");
|
|
120
|
+
ctx.result = {
|
|
121
|
+
code: 1,
|
|
122
|
+
msg: "内部服务错误"
|
|
123
|
+
};
|
|
124
|
+
return FinalResponse(ctx);
|
|
84
125
|
}
|
|
85
126
|
}
|
|
86
|
-
|
|
87
|
-
// 7. 返回响应(自动处理 response/result/日志)
|
|
88
|
-
return FinalResponse(ctx);
|
|
89
|
-
} catch (err: any) {
|
|
90
|
-
// 全局错误处理
|
|
91
|
-
const errorPath = ctx.api ? apiPath : req.url;
|
|
92
|
-
Logger.error({ err: err, path: errorPath }, '请求错误');
|
|
93
|
-
ctx.result = {
|
|
94
|
-
code: 1,
|
|
95
|
-
msg: '内部服务错误'
|
|
96
|
-
};
|
|
97
|
-
return FinalResponse(ctx);
|
|
98
|
-
}
|
|
127
|
+
);
|
|
99
128
|
};
|
|
100
129
|
}
|
package/router/static.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 静态文件路由处理器
|
|
3
3
|
* 处理 /* 路径的静态文件请求
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// 外部依赖
|
|
7
|
-
import { join } from
|
|
7
|
+
import { join } from "pathe";
|
|
8
8
|
|
|
9
|
+
import { beflyConfig } from "../befly.config.js";
|
|
10
|
+
import { Logger } from "../lib/logger.js";
|
|
9
11
|
// 相对导入
|
|
10
|
-
import { projectDir } from
|
|
11
|
-
import {
|
|
12
|
-
import { setCorsOptions } from '../util.js';
|
|
13
|
-
import { beflyConfig } from '../befly.config.js';
|
|
12
|
+
import { projectDir } from "../paths.js";
|
|
13
|
+
import { setCorsOptions } from "../utils/cors.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* 静态文件处理器工厂
|
|
@@ -21,11 +21,11 @@ export function staticHandler() {
|
|
|
21
21
|
const corsHeaders = setCorsOptions(req, beflyConfig.cors);
|
|
22
22
|
|
|
23
23
|
const url = new URL(req.url);
|
|
24
|
-
const filePath = join(projectDir,
|
|
24
|
+
const filePath = join(projectDir, "public", url.pathname);
|
|
25
25
|
|
|
26
26
|
try {
|
|
27
27
|
// OPTIONS预检请求
|
|
28
|
-
if (req.method ===
|
|
28
|
+
if (req.method === "OPTIONS") {
|
|
29
29
|
return new Response(null, {
|
|
30
30
|
status: 204,
|
|
31
31
|
headers: corsHeaders
|
|
@@ -36,7 +36,7 @@ export function staticHandler() {
|
|
|
36
36
|
if (await file.exists()) {
|
|
37
37
|
return new Response(file, {
|
|
38
38
|
headers: {
|
|
39
|
-
|
|
39
|
+
"Content-Type": file.type || "application/octet-stream",
|
|
40
40
|
...corsHeaders
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -44,7 +44,7 @@ export function staticHandler() {
|
|
|
44
44
|
return Response.json(
|
|
45
45
|
{
|
|
46
46
|
code: 1,
|
|
47
|
-
msg:
|
|
47
|
+
msg: "文件未找到"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
headers: corsHeaders
|
|
@@ -52,12 +52,12 @@ export function staticHandler() {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
} catch (error: any) {
|
|
55
|
-
Logger.error({ err: error },
|
|
55
|
+
Logger.error({ err: error }, "静态文件处理失败");
|
|
56
56
|
|
|
57
57
|
return Response.json(
|
|
58
58
|
{
|
|
59
59
|
code: 1,
|
|
60
|
-
msg:
|
|
60
|
+
msg: "文件读取失败"
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
headers: corsHeaders
|
package/sync/syncAll.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* Sync 命令 - 一次性执行所有同步操作
|
|
3
|
-
* 按顺序执行:syncDb → syncApi → syncMenu → syncDev
|
|
3
|
+
* 按顺序执行:syncDb → syncApi → syncMenu → syncDev(syncDev 内会重建角色接口权限缓存)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
6
|
+
import type { SyncOptions } from "../types/sync.js";
|
|
7
|
+
|
|
8
|
+
import { checkApp } from "../checks/checkApp.js";
|
|
9
|
+
import { Logger } from "../lib/logger.js";
|
|
10
|
+
import { syncApiCommand } from "./syncApi.js";
|
|
11
|
+
import { syncDbCommand } from "./syncDb.js";
|
|
12
|
+
import { syncDevCommand } from "./syncDev.js";
|
|
13
|
+
import { syncMenuCommand } from "./syncMenu.js";
|
|
14
14
|
|
|
15
15
|
export async function syncAllCommand(options: SyncOptions = {}) {
|
|
16
16
|
try {
|
|
@@ -26,10 +26,10 @@ export async function syncAllCommand(options: SyncOptions = {}) {
|
|
|
26
26
|
// 3. 同步菜单(并缓存)
|
|
27
27
|
await syncMenuCommand();
|
|
28
28
|
|
|
29
|
-
// 4.
|
|
29
|
+
// 4. 同步开发管理员(syncDev 内会重建角色接口权限缓存)
|
|
30
30
|
await syncDevCommand();
|
|
31
31
|
} catch (error: any) {
|
|
32
|
-
Logger.error({ err: error },
|
|
32
|
+
Logger.error({ err: error }, "同步过程中发生错误");
|
|
33
33
|
throw error;
|
|
34
34
|
}
|
|
35
35
|
}
|
package/sync/syncApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* SyncApi 命令 - 同步 API 接口数据到数据库
|
|
3
3
|
* 说明:遍历所有 API 文件,收集接口路由信息并同步到 addon_admin_api 表
|
|
4
4
|
*
|
|
@@ -10,27 +10,25 @@
|
|
|
10
10
|
* 5. 存在则更新,不存在则新增
|
|
11
11
|
* 6. 删除配置中不存在的接口记录
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { join, dirname, relative, basename } from 'pathe';
|
|
15
|
-
import { Connect } from '../lib/connect.js';
|
|
16
|
-
import { DbHelper } from '../lib/dbHelper.js';
|
|
17
|
-
import { RedisHelper } from '../lib/redisHelper.js';
|
|
18
|
-
import { RedisKeys } from 'befly-shared/redisKeys';
|
|
19
|
-
import { scanFiles } from 'befly-shared/scanFiles';
|
|
20
|
-
import { scanAddons, addonDirExists, getAddonDir } from 'befly-shared/addonHelper';
|
|
13
|
+
import type { SyncApiOptions, ApiInfo } from "../types/sync.js";
|
|
21
14
|
|
|
22
|
-
import {
|
|
23
|
-
import { projectDir } from '../paths.js';
|
|
24
|
-
import { beflyConfig } from '../befly.config.js';
|
|
15
|
+
import { join, relative } from "pathe";
|
|
25
16
|
|
|
26
|
-
import
|
|
17
|
+
import { CacheHelper } from "../lib/cacheHelper.js";
|
|
18
|
+
import { Connect } from "../lib/connect.js";
|
|
19
|
+
import { DbHelper } from "../lib/dbHelper.js";
|
|
20
|
+
import { Logger } from "../lib/logger.js";
|
|
21
|
+
import { RedisHelper } from "../lib/redisHelper.js";
|
|
22
|
+
import { projectDir } from "../paths.js";
|
|
23
|
+
import { scanAddons, addonDirExists, getAddonDir } from "../utils/addonHelper.js";
|
|
24
|
+
import { scanFiles } from "../utils/scanFiles.js";
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
27
|
* 从 API 文件中提取接口信息
|
|
30
28
|
*/
|
|
31
|
-
async function extractApiInfo(filePath: string, apiRoot: string, type:
|
|
29
|
+
async function extractApiInfo(filePath: string, apiRoot: string, type: "app" | "addon", addonName: string = "", addonTitle: string = ""): Promise<ApiInfo | null> {
|
|
32
30
|
try {
|
|
33
|
-
const normalizedFilePath = filePath.replace(/\\/g,
|
|
31
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
34
32
|
const apiModule = await import(normalizedFilePath);
|
|
35
33
|
const apiConfig = apiModule.default;
|
|
36
34
|
|
|
@@ -38,32 +36,32 @@ async function extractApiInfo(filePath: string, apiRoot: string, type: 'app' | '
|
|
|
38
36
|
return null;
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
let apiPath =
|
|
39
|
+
let apiPath = "";
|
|
42
40
|
|
|
43
|
-
if (type ===
|
|
41
|
+
if (type === "addon") {
|
|
44
42
|
// Addon 接口:保留完整目录层级
|
|
45
43
|
// 例: apis/menu/list.ts → /api/addon/admin/menu/list
|
|
46
44
|
const relativePath = relative(apiRoot, filePath);
|
|
47
|
-
const pathWithoutExt = relativePath.replace(/\.(ts|js)$/,
|
|
45
|
+
const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, "");
|
|
48
46
|
apiPath = `/api/addon/${addonName}/${pathWithoutExt}`;
|
|
49
47
|
} else {
|
|
50
48
|
// 项目接口:保留完整目录层级
|
|
51
49
|
// 例: apis/user/list.ts → /api/user/list
|
|
52
50
|
const relativePath = relative(apiRoot, filePath);
|
|
53
|
-
const pathWithoutExt = relativePath.replace(/\.(ts|js)$/,
|
|
51
|
+
const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, "");
|
|
54
52
|
apiPath = `/api/${pathWithoutExt}`;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
return {
|
|
58
|
-
name: apiConfig.name ||
|
|
56
|
+
name: apiConfig.name || "",
|
|
59
57
|
path: apiPath,
|
|
60
|
-
method: apiConfig.method ||
|
|
61
|
-
description: apiConfig.description ||
|
|
58
|
+
method: apiConfig.method || "POST",
|
|
59
|
+
description: apiConfig.description || "",
|
|
62
60
|
addonName: addonName,
|
|
63
61
|
addonTitle: addonTitle || addonName
|
|
64
62
|
};
|
|
65
63
|
} catch (error: any) {
|
|
66
|
-
Logger.error({ err: error },
|
|
64
|
+
Logger.error({ err: error }, "同步 API 失败");
|
|
67
65
|
throw error;
|
|
68
66
|
}
|
|
69
67
|
}
|
|
@@ -71,12 +69,12 @@ async function extractApiInfo(filePath: string, apiRoot: string, type: 'app' | '
|
|
|
71
69
|
/**
|
|
72
70
|
* 扫描所有 API 文件
|
|
73
71
|
*/
|
|
74
|
-
async function scanAllApis(
|
|
72
|
+
async function scanAllApis(): Promise<ApiInfo[]> {
|
|
75
73
|
const apis: ApiInfo[] = [];
|
|
76
74
|
|
|
77
75
|
// 1. 扫描项目 API(只扫描 apis 目录)
|
|
78
76
|
try {
|
|
79
|
-
const projectApisDir = join(projectDir,
|
|
77
|
+
const projectApisDir = join(projectDir, "apis");
|
|
80
78
|
|
|
81
79
|
// 扫描项目 API 文件
|
|
82
80
|
const projectApiFiles: string[] = [];
|
|
@@ -90,7 +88,7 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
|
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
for (const filePath of projectApiFiles) {
|
|
93
|
-
const apiInfo = await extractApiInfo(filePath, projectApisDir,
|
|
91
|
+
const apiInfo = await extractApiInfo(filePath, projectApisDir, "app", "", "项目接口");
|
|
94
92
|
if (apiInfo) {
|
|
95
93
|
apis.push(apiInfo);
|
|
96
94
|
}
|
|
@@ -103,19 +101,19 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
|
|
|
103
101
|
// addonName 格式: admin, demo 等
|
|
104
102
|
|
|
105
103
|
// 检查 apis 子目录是否存在
|
|
106
|
-
if (!addonDirExists(addonName,
|
|
104
|
+
if (!addonDirExists(addonName, "apis")) {
|
|
107
105
|
continue;
|
|
108
106
|
}
|
|
109
107
|
|
|
110
|
-
const addonApisDir = getAddonDir(addonName,
|
|
108
|
+
const addonApisDir = getAddonDir(addonName, "apis");
|
|
111
109
|
|
|
112
110
|
// 读取 addon 配置
|
|
113
|
-
const addonPackageJsonPath = getAddonDir(addonName,
|
|
111
|
+
const addonPackageJsonPath = getAddonDir(addonName, "package.json");
|
|
114
112
|
let addonTitle = addonName;
|
|
115
113
|
try {
|
|
116
|
-
const packageJson = await import(addonPackageJsonPath, { with: { type:
|
|
114
|
+
const packageJson = await import(addonPackageJsonPath, { with: { type: "json" } });
|
|
117
115
|
addonTitle = packageJson.default?.title || addonName;
|
|
118
|
-
} catch
|
|
116
|
+
} catch {
|
|
119
117
|
// 忽略配置读取错误
|
|
120
118
|
}
|
|
121
119
|
|
|
@@ -131,7 +129,7 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
|
|
|
131
129
|
}
|
|
132
130
|
|
|
133
131
|
for (const filePath of addonApiFiles) {
|
|
134
|
-
const apiInfo = await extractApiInfo(filePath, addonApisDir,
|
|
132
|
+
const apiInfo = await extractApiInfo(filePath, addonApisDir, "addon", addonName, addonTitle);
|
|
135
133
|
if (apiInfo) {
|
|
136
134
|
apis.push(apiInfo);
|
|
137
135
|
}
|
|
@@ -140,7 +138,7 @@ async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
|
|
|
140
138
|
|
|
141
139
|
return apis;
|
|
142
140
|
} catch (error: any) {
|
|
143
|
-
Logger.error({ err: error },
|
|
141
|
+
Logger.error({ err: error }, "接口扫描失败");
|
|
144
142
|
return apis;
|
|
145
143
|
}
|
|
146
144
|
}
|
|
@@ -153,7 +151,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
|
|
|
153
151
|
try {
|
|
154
152
|
// 根据 path 查询是否存在
|
|
155
153
|
const existing = await helper.getOne({
|
|
156
|
-
table:
|
|
154
|
+
table: "addon_admin_api",
|
|
157
155
|
where: { path: api.path }
|
|
158
156
|
});
|
|
159
157
|
|
|
@@ -163,7 +161,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
|
|
|
163
161
|
|
|
164
162
|
if (needUpdate) {
|
|
165
163
|
await helper.updData({
|
|
166
|
-
table:
|
|
164
|
+
table: "addon_admin_api",
|
|
167
165
|
where: { id: existing.id },
|
|
168
166
|
data: {
|
|
169
167
|
name: api.name,
|
|
@@ -176,7 +174,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
|
|
|
176
174
|
}
|
|
177
175
|
} else {
|
|
178
176
|
await helper.insData({
|
|
179
|
-
table:
|
|
177
|
+
table: "addon_admin_api",
|
|
180
178
|
data: {
|
|
181
179
|
name: api.name,
|
|
182
180
|
path: api.path,
|
|
@@ -188,7 +186,7 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
|
|
|
188
186
|
});
|
|
189
187
|
}
|
|
190
188
|
} catch (error: any) {
|
|
191
|
-
Logger.error({ err: error, api: api.name },
|
|
189
|
+
Logger.error({ err: error, api: api.name }, "同步接口失败");
|
|
192
190
|
}
|
|
193
191
|
}
|
|
194
192
|
}
|
|
@@ -198,14 +196,14 @@ async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
|
|
|
198
196
|
*/
|
|
199
197
|
async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promise<void> {
|
|
200
198
|
const allRecords = await helper.getAll({
|
|
201
|
-
table:
|
|
199
|
+
table: "addon_admin_api",
|
|
202
200
|
where: { state$gte: 0 }
|
|
203
201
|
});
|
|
204
202
|
|
|
205
203
|
for (const record of allRecords.lists) {
|
|
206
204
|
if (record.path && !apiPaths.has(record.path)) {
|
|
207
205
|
await helper.delForce({
|
|
208
|
-
table:
|
|
206
|
+
table: "addon_admin_api",
|
|
209
207
|
where: { id: record.id }
|
|
210
208
|
});
|
|
211
209
|
}
|
|
@@ -218,25 +216,27 @@ async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promis
|
|
|
218
216
|
export async function syncApiCommand(options: SyncApiOptions = {}): Promise<void> {
|
|
219
217
|
try {
|
|
220
218
|
if (options.plan) {
|
|
221
|
-
Logger.debug(
|
|
219
|
+
Logger.debug("[计划] 同步 API 接口到数据库(plan 模式不执行)");
|
|
222
220
|
return;
|
|
223
221
|
}
|
|
224
222
|
|
|
225
223
|
// 连接数据库(SQL + Redis)
|
|
226
224
|
await Connect.connect();
|
|
227
225
|
|
|
228
|
-
const
|
|
226
|
+
const redisHelper = new RedisHelper();
|
|
227
|
+
const helper = new DbHelper({ redis: redisHelper } as any, Connect.getSql());
|
|
228
|
+
const cacheHelper = new CacheHelper({ db: helper, redis: redisHelper } as any);
|
|
229
229
|
|
|
230
230
|
// 1. 检查表是否存在(addon_admin_api 来自 addon-admin 组件)
|
|
231
|
-
const exists = await helper.tableExists(
|
|
231
|
+
const exists = await helper.tableExists("addon_admin_api");
|
|
232
232
|
|
|
233
233
|
if (!exists) {
|
|
234
|
-
Logger.debug(
|
|
234
|
+
Logger.debug("表 addon_admin_api 不存在,跳过 API 同步(需要安装 addon-admin 组件)");
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
// 2. 扫描所有 API 文件
|
|
239
|
-
const apis = await scanAllApis(
|
|
239
|
+
const apis = await scanAllApis();
|
|
240
240
|
const apiPaths = new Set(apis.map((api) => api.path));
|
|
241
241
|
|
|
242
242
|
// 3. 同步 API 数据
|
|
@@ -247,18 +247,21 @@ export async function syncApiCommand(options: SyncApiOptions = {}): Promise<void
|
|
|
247
247
|
|
|
248
248
|
// 5. 缓存接口数据到 Redis
|
|
249
249
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
await cacheHelper.cacheApis();
|
|
251
|
+
} catch {
|
|
252
|
+
// 忽略缓存错误
|
|
253
|
+
}
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
// 6. API 表发生变更后,重建角色接口权限缓存(不使用定时器)
|
|
256
|
+
// 说明:role permission set 的成员是 METHOD/path,API 的 method/path 变更会影响所有角色权限。
|
|
257
|
+
try {
|
|
258
|
+
await cacheHelper.rebuildRoleApiPermissions();
|
|
257
259
|
} catch (error: any) {
|
|
258
|
-
//
|
|
260
|
+
// 不阻塞 syncApi 主流程,但记录日志
|
|
261
|
+
Logger.warn({ err: error }, "API 同步完成,但重建角色权限缓存失败");
|
|
259
262
|
}
|
|
260
263
|
} catch (error: any) {
|
|
261
|
-
Logger.error({ err: error },
|
|
264
|
+
Logger.error({ err: error }, "API 同步失败");
|
|
262
265
|
throw error;
|
|
263
266
|
} finally {
|
|
264
267
|
await Connect.disconnect();
|