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.
Files changed (141) hide show
  1. package/README.md +39 -8
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +1 -1
  9. package/docs/api/api.md +1 -1
  10. package/docs/guide/quickstart.md +16 -9
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +7 -7
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +16 -9
  16. package/docs/reference/addon.md +12 -1
  17. package/docs/reference/config.md +13 -30
  18. package/docs/reference/sync.md +62 -193
  19. package/docs/reference/table.md +27 -29
  20. package/hooks/auth.ts +3 -4
  21. package/hooks/cors.ts +4 -6
  22. package/hooks/parser.ts +3 -4
  23. package/hooks/permission.ts +3 -4
  24. package/hooks/validator.ts +4 -5
  25. package/lib/cacheHelper.ts +89 -153
  26. package/lib/cacheKeys.ts +1 -1
  27. package/lib/connect.ts +9 -13
  28. package/lib/dbDialect.ts +285 -0
  29. package/lib/dbHelper.ts +179 -507
  30. package/lib/dbUtils.ts +450 -0
  31. package/lib/logger.ts +41 -5
  32. package/lib/redisHelper.ts +1 -0
  33. package/lib/sqlBuilder.ts +358 -58
  34. package/lib/sqlCheck.ts +136 -0
  35. package/lib/validator.ts +1 -1
  36. package/loader/loadApis.ts +23 -126
  37. package/loader/loadHooks.ts +31 -46
  38. package/loader/loadPlugins.ts +37 -52
  39. package/main.ts +58 -19
  40. package/package.json +24 -25
  41. package/paths.ts +14 -14
  42. package/plugins/cache.ts +12 -6
  43. package/plugins/cipher.ts +2 -2
  44. package/plugins/config.ts +6 -8
  45. package/plugins/db.ts +14 -19
  46. package/plugins/jwt.ts +6 -7
  47. package/plugins/logger.ts +7 -9
  48. package/plugins/redis.ts +8 -10
  49. package/plugins/tool.ts +3 -4
  50. package/router/api.ts +3 -2
  51. package/router/static.ts +7 -5
  52. package/sync/syncApi.ts +80 -235
  53. package/sync/syncCache.ts +16 -0
  54. package/sync/syncDev.ts +167 -202
  55. package/sync/syncMenu.ts +230 -444
  56. package/sync/syncTable.ts +1247 -0
  57. package/tests/_mocks/mockSqliteDb.ts +204 -0
  58. package/tests/addonHelper-cache.test.ts +32 -0
  59. package/tests/apiHandler-routePath-only.test.ts +32 -0
  60. package/tests/cacheHelper.test.ts +16 -51
  61. package/tests/checkApi-routePath-strict.test.ts +166 -0
  62. package/tests/checkMenu.test.ts +346 -0
  63. package/tests/checkTable-smoke.test.ts +157 -0
  64. package/tests/dbDialect-cache.test.ts +23 -0
  65. package/tests/dbDialect.test.ts +46 -0
  66. package/tests/dbHelper-advanced.test.ts +1 -1
  67. package/tests/dbHelper-all-array-types.test.ts +15 -15
  68. package/tests/dbHelper-batch-write.test.ts +90 -0
  69. package/tests/dbHelper-columns.test.ts +36 -54
  70. package/tests/dbHelper-execute.test.ts +26 -26
  71. package/tests/dbHelper-joins.test.ts +85 -176
  72. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  73. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  75. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  76. package/tests/logger.test.ts +6 -6
  77. package/tests/redisHelper.test.ts +6 -1
  78. package/tests/scanFiles-routePath.test.ts +46 -0
  79. package/tests/smoke-sql.test.ts +24 -0
  80. package/tests/sqlBuilder-advanced.test.ts +18 -5
  81. package/tests/sqlBuilder.test.ts +24 -0
  82. package/tests/sync-init-guard.test.ts +105 -0
  83. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  84. package/tests/syncApi-obsolete-records.test.ts +69 -0
  85. package/tests/syncApi-type-compat.test.ts +72 -0
  86. package/tests/syncDev-permissions.test.ts +81 -0
  87. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  88. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  89. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  90. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  91. package/tests/syncMenu-paths.test.ts +0 -9
  92. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  93. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  94. package/tests/syncTable-constants.test.ts +101 -0
  95. package/tests/syncTable-db-integration.test.ts +237 -0
  96. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  97. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  98. package/tests/syncTable-schema.test.ts +99 -0
  99. package/tests/syncTable-testkit.test.ts +25 -0
  100. package/tests/syncTable-types.test.ts +122 -0
  101. package/tests/tableRef-and-deserialize.test.ts +67 -0
  102. package/tsconfig.json +1 -1
  103. package/types/api.d.ts +1 -1
  104. package/types/befly.d.ts +13 -12
  105. package/types/cache.d.ts +2 -2
  106. package/types/context.d.ts +1 -1
  107. package/types/database.d.ts +0 -5
  108. package/types/hook.d.ts +1 -10
  109. package/types/plugin.d.ts +2 -96
  110. package/types/sync.d.ts +19 -25
  111. package/utils/convertBigIntFields.ts +38 -0
  112. package/utils/disableMenusGlob.ts +85 -0
  113. package/utils/importDefault.ts +21 -0
  114. package/utils/isDirentDirectory.ts +23 -0
  115. package/utils/loadMenuConfigs.ts +145 -0
  116. package/utils/processFields.ts +25 -0
  117. package/utils/scanAddons.ts +72 -0
  118. package/utils/scanFiles.ts +129 -21
  119. package/utils/scanSources.ts +64 -0
  120. package/utils/sortModules.ts +137 -0
  121. package/checks/checkApp.ts +0 -55
  122. package/hooks/rateLimit.ts +0 -276
  123. package/sync/syncAll.ts +0 -35
  124. package/sync/syncDb/apply.ts +0 -192
  125. package/sync/syncDb/constants.ts +0 -119
  126. package/sync/syncDb/ddl.ts +0 -251
  127. package/sync/syncDb/helpers.ts +0 -84
  128. package/sync/syncDb/schema.ts +0 -202
  129. package/sync/syncDb/sqlite.ts +0 -48
  130. package/sync/syncDb/table.ts +0 -207
  131. package/sync/syncDb/tableCreate.ts +0 -163
  132. package/sync/syncDb/types.ts +0 -132
  133. package/sync/syncDb/version.ts +0 -69
  134. package/sync/syncDb.ts +0 -168
  135. package/tests/rateLimit-hook.test.ts +0 -477
  136. package/tests/syncDb-constants.test.ts +0 -130
  137. package/tests/syncDb-schema.test.ts +0 -179
  138. package/tests/syncDb-types.test.ts +0 -139
  139. package/utils/addonHelper.ts +0 -90
  140. package/utils/modules.ts +0 -98
  141. package/utils/route.ts +0 -23
package/sync/syncApi.ts CHANGED
@@ -1,269 +1,114 @@
1
- /**
2
- * SyncApi 命令 - 同步 API 接口数据到数据库
3
- * 说明:遍历所有 API 文件,收集接口路由信息并同步到 addon_admin_api 表
4
- *
5
- * 流程:
6
- * 1. 扫描项目 apis 目录下所有项目 API 文件
7
- * 2. 扫描 node_modules/@befly-addon/* 目录下所有组件 API 文件
8
- * 3. 提取每个 API 的 name、method、auth 等信息
9
- * 4. 根据接口路径检查是否存在
10
- * 5. 存在则更新,不存在则新增
11
- * 6. 删除配置中不存在的接口记录
12
- */
13
- import type { SyncApiOptions, ApiInfo } from "../types/sync.js";
1
+ import type { BeflyContext } from "../types/befly.js";
2
+ import type { SyncApiItem } from "../types/sync.js";
14
3
 
15
- import { join, relative } from "pathe";
4
+ import { keyBy } from "es-toolkit/array";
16
5
 
17
- import { CacheHelper } from "../lib/cacheHelper.js";
18
- import { Connect } from "../lib/connect.js";
19
- import { DbHelper } from "../lib/dbHelper.js";
20
6
  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";
25
7
 
26
- /**
27
- * API 文件中提取接口信息
28
- */
29
- async function extractApiInfo(filePath: string, apiRoot: string, type: "app" | "addon", addonName: string = "", addonTitle: string = ""): Promise<ApiInfo | null> {
30
- try {
31
- const normalizedFilePath = filePath.replace(/\\/g, "/");
32
- const apiModule = await import(normalizedFilePath);
33
- const apiConfig = apiModule.default;
8
+ export async function syncApi(ctx: Pick<BeflyContext, "db" | "cache">, apis: SyncApiItem[]): Promise<void> {
9
+ const tableName = "addon_admin_api";
34
10
 
35
- if (!apiConfig || !apiConfig.name) {
36
- return null;
37
- }
38
-
39
- let apiPath = "";
40
-
41
- if (type === "addon") {
42
- // Addon 接口:保留完整目录层级
43
- // 例: apis/menu/list.ts → /api/addon/admin/menu/list
44
- const relativePath = relative(apiRoot, filePath);
45
- const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, "");
46
- apiPath = `/api/addon/${addonName}/${pathWithoutExt}`;
47
- } else {
48
- // 项目接口:保留完整目录层级
49
- // 例: apis/user/list.ts → /api/user/list
50
- const relativePath = relative(apiRoot, filePath);
51
- const pathWithoutExt = relativePath.replace(/\.(ts|js)$/, "");
52
- apiPath = `/api/${pathWithoutExt}`;
53
- }
54
-
55
- return {
56
- name: apiConfig.name || "",
57
- path: apiPath,
58
- method: apiConfig.method || "POST",
59
- description: apiConfig.description || "",
60
- addonName: addonName,
61
- addonTitle: addonTitle || addonName
62
- };
63
- } catch (error: any) {
64
- Logger.error({ err: error }, "同步 API 失败");
65
- throw error;
11
+ if (!ctx.db) {
12
+ throw new Error("syncApi: ctx.db 未初始化(Db 插件未加载或注入失败)");
66
13
  }
67
- }
68
-
69
- /**
70
- * 扫描所有 API 文件
71
- */
72
- async function scanAllApis(): Promise<ApiInfo[]> {
73
- const apis: ApiInfo[] = [];
74
-
75
- // 1. 扫描项目 API(只扫描 apis 目录)
76
- try {
77
- const projectApisDir = join(projectDir, "apis");
78
-
79
- // 扫描项目 API 文件
80
- const projectApiFiles: string[] = [];
81
- try {
82
- const files = await scanFiles(projectApisDir);
83
- for (const { filePath } of files) {
84
- projectApiFiles.push(filePath);
85
- }
86
- } catch (error: any) {
87
- Logger.warn(`扫描项目 API 目录失败: ${projectApisDir} - ${error.message}`);
88
- }
89
14
 
90
- for (const filePath of projectApiFiles) {
91
- const apiInfo = await extractApiInfo(filePath, projectApisDir, "app", "", "项目接口");
92
- if (apiInfo) {
93
- apis.push(apiInfo);
94
- }
95
- }
15
+ if (!ctx.cache) {
16
+ throw new Error("syncApi: ctx.cache 未初始化(cache 插件未加载或注入失败)");
17
+ }
96
18
 
97
- // 2. 扫描组件 API (node_modules/@befly-addon/*)
98
- const addonNames = scanAddons();
19
+ if (!(await ctx.db.tableExists(tableName))) {
20
+ Logger.debug(`${tableName} 表不存在`);
21
+ return;
22
+ }
99
23
 
100
- for (const addonName of addonNames) {
101
- // addonName 格式: admin, demo 等
24
+ const allDbApis = await ctx.db.getAll({
25
+ table: tableName,
26
+ fields: ["id", "routePath", "name", "addonName", "state"],
27
+ where: { state$gte: 0 }
28
+ } as any);
102
29
 
103
- // 检查 apis 子目录是否存在
104
- if (!addonDirExists(addonName, "apis")) {
105
- continue;
106
- }
30
+ const dbLists = allDbApis.lists || [];
31
+ const allDbApiMap = keyBy(dbLists, (item: any) => item.routePath);
107
32
 
108
- const addonApisDir = getAddonDir(addonName, "apis");
33
+ const insData: SyncApiItem[] = [];
34
+ const updData: Array<{ id: number; api: SyncApiItem }> = [];
35
+ const delData: number[] = [];
109
36
 
110
- // 读取 addon 配置
111
- const addonPackageJsonPath = getAddonDir(addonName, "package.json");
112
- let addonTitle = addonName;
113
- try {
114
- const packageJson = await import(addonPackageJsonPath, { with: { type: "json" } });
115
- addonTitle = packageJson.default?.title || addonName;
116
- } catch {
117
- // 忽略配置读取错误
118
- }
37
+ // 1) 先构建当前扫描到的 routePath 集合(用于删除差集)
38
+ const apiRouteKeys = new Set<string>();
119
39
 
120
- // 扫描 addon API 文件
121
- const addonApiFiles: string[] = [];
122
- try {
123
- const files = await scanFiles(addonApisDir);
124
- for (const { filePath } of files) {
125
- addonApiFiles.push(filePath);
126
- }
127
- } catch (error: any) {
128
- Logger.warn(`扫描 addon API 目录失败: ${addonApisDir} - ${error.message}`);
129
- }
40
+ // 2) 插入 / 更新(存在不一定更新:仅当 name/routePath/addonName 任一不匹配时更新)
41
+ for (const api of apis) {
42
+ const apiType = api.type;
43
+ // 兼容:历史/测试构造的数据可能没有 type 字段;此时应按 API 处理。
44
+ // 因此仅当 type **显式存在** 且不为 "api" 时才跳过,避免误把真实 API 条目过滤掉。
45
+ if (apiType && apiType !== "api") {
46
+ continue;
47
+ }
130
48
 
131
- for (const filePath of addonApiFiles) {
132
- const apiInfo = await extractApiInfo(filePath, addonApisDir, "addon", addonName, addonTitle);
133
- if (apiInfo) {
134
- apis.push(apiInfo);
135
- }
49
+ const routePath = api.routePath;
50
+ apiRouteKeys.add(api.routePath);
51
+ const item = (allDbApiMap as any)[routePath];
52
+ if (item) {
53
+ const shouldUpdate = api.name !== item.name || api.routePath !== item.routePath || api.addonName !== item.addonName;
54
+ if (shouldUpdate) {
55
+ updData.push({ id: item.id, api: api });
136
56
  }
57
+ } else {
58
+ insData.push(api);
137
59
  }
60
+ }
138
61
 
139
- return apis;
140
- } catch (error: any) {
141
- Logger.error({ err: error }, "接口扫描失败");
142
- return apis;
62
+ // 3) 删除:用差集(DB - 当前扫描)得到要删除的 id
63
+ for (const record of dbLists) {
64
+ if (!apiRouteKeys.has(record.routePath)) {
65
+ delData.push(record.id);
66
+ }
143
67
  }
144
- }
145
68
 
146
- /**
147
- * 同步 API 数据到数据库
148
- */
149
- async function syncApis(helper: any, apis: ApiInfo[]): Promise<void> {
150
- for (const api of apis) {
69
+ if (updData.length > 0) {
151
70
  try {
152
- // 根据 path 查询是否存在
153
- const existing = await helper.getOne({
154
- table: "addon_admin_api",
155
- where: { path: api.path }
156
- });
157
-
158
- if (existing) {
159
- // 检查是否需要更新
160
- const needUpdate = existing.name !== api.name || existing.method !== api.method || existing.description !== api.description || existing.addonName !== api.addonName || existing.addonTitle !== api.addonTitle;
161
-
162
- if (needUpdate) {
163
- await helper.updData({
164
- table: "addon_admin_api",
165
- where: { id: existing.id },
71
+ await ctx.db.updBatch(
72
+ tableName,
73
+ updData.map((item) => {
74
+ return {
75
+ id: item.id,
166
76
  data: {
167
- name: api.name,
168
- method: api.method,
169
- description: api.description,
170
- addonName: api.addonName,
171
- addonTitle: api.addonTitle
77
+ name: item.api.name,
78
+ routePath: item.api.routePath,
79
+ addonName: item.api.addonName
172
80
  }
173
- });
174
- }
175
- } else {
176
- await helper.insData({
177
- table: "addon_admin_api",
178
- data: {
179
- name: api.name,
180
- path: api.path,
181
- method: api.method,
182
- description: api.description,
183
- addonName: api.addonName,
184
- addonTitle: api.addonTitle
185
- }
186
- });
187
- }
81
+ };
82
+ })
83
+ );
188
84
  } catch (error: any) {
189
- Logger.error({ err: error, api: api.name }, "同步接口失败");
85
+ Logger.error({ err: error }, "同步接口批量更新失败");
190
86
  }
191
87
  }
192
- }
193
-
194
- /**
195
- * 删除配置中不存在的记录
196
- */
197
- async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promise<void> {
198
- const allRecords = await helper.getAll({
199
- table: "addon_admin_api",
200
- where: { state$gte: 0 }
201
- });
202
-
203
- for (const record of allRecords.lists) {
204
- if (record.path && !apiPaths.has(record.path)) {
205
- await helper.delForce({
206
- table: "addon_admin_api",
207
- where: { id: record.id }
208
- });
209
- }
210
- }
211
- }
212
88
 
213
- /**
214
- * SyncApi 命令主函数
215
- */
216
- export async function syncApiCommand(options: SyncApiOptions = {}): Promise<void> {
217
- try {
218
- if (options.plan) {
219
- Logger.debug("[计划] 同步 API 接口到数据库(plan 模式不执行)");
220
- return;
221
- }
222
-
223
- // 连接数据库(SQL + Redis)
224
- await Connect.connect();
225
-
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
-
230
- // 1. 检查表是否存在(addon_admin_api 来自 addon-admin 组件)
231
- const exists = await helper.tableExists("addon_admin_api");
232
-
233
- if (!exists) {
234
- Logger.debug("表 addon_admin_api 不存在,跳过 API 同步(需要安装 addon-admin 组件)");
235
- return;
236
- }
237
-
238
- // 2. 扫描所有 API 文件
239
- const apis = await scanAllApis();
240
- const apiPaths = new Set(apis.map((api) => api.path));
241
-
242
- // 3. 同步 API 数据
243
- await syncApis(helper, apis);
244
-
245
- // 4. 删除文件中不存在的接口
246
- await deleteObsoleteRecords(helper, apiPaths);
247
-
248
- // 5. 缓存接口数据到 Redis
89
+ if (insData.length > 0) {
249
90
  try {
250
- await cacheHelper.cacheApis();
251
- } catch {
252
- // 忽略缓存错误
91
+ await ctx.db.insBatch(
92
+ tableName,
93
+ insData.map((api) => {
94
+ return {
95
+ name: api.name,
96
+ routePath: api.routePath,
97
+ addonName: api.addonName
98
+ };
99
+ })
100
+ );
101
+ } catch (error: any) {
102
+ Logger.error({ err: error }, "同步接口批量新增失败");
253
103
  }
104
+ }
254
105
 
255
- // 6. API 表发生变更后,重建角色接口权限缓存(不使用定时器)
256
- // 说明:role permission set 的成员是 METHOD/path,API 的 method/path 变更会影响所有角色权限。
106
+ if (delData.length > 0) {
257
107
  try {
258
- await cacheHelper.rebuildRoleApiPermissions();
108
+ await ctx.db.delForceBatch(tableName, delData);
259
109
  } catch (error: any) {
260
- // 不阻塞 syncApi 主流程,但记录日志
261
- Logger.warn({ err: error }, "API 同步完成,但重建角色权限缓存失败");
110
+ Logger.error({ err: error }, "同步接口批量删除失败");
262
111
  }
263
- } catch (error: any) {
264
- Logger.error({ err: error }, "API 同步失败");
265
- throw error;
266
- } finally {
267
- await Connect.disconnect();
268
112
  }
113
+ // 缓存同步职责已收敛到 syncCache(启动流程单点调用),此处只负责 DB 同步。
269
114
  }
@@ -0,0 +1,16 @@
1
+ import type { BeflyContext } from "../types/befly.js";
2
+
3
+ export async function syncCache(ctx: Pick<BeflyContext, "cache">): Promise<void> {
4
+ if (!ctx.cache) {
5
+ throw new Error("syncCache: ctx.cache 未初始化(cache 插件未加载或注入失败)");
6
+ }
7
+
8
+ // 1) 缓存接口列表
9
+ await ctx.cache.cacheApis();
10
+
11
+ // 2) 缓存菜单列表
12
+ await ctx.cache.cacheMenus();
13
+
14
+ // 3) 重建角色权限缓存(严格模式下要求 role.apis 必须为 pathname 字符串数组)
15
+ await ctx.cache.rebuildRoleApiPermissions();
16
+ }