befly 3.9.40 → 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 +3 -4
  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/syncDev.ts CHANGED
@@ -1,230 +1,195 @@
1
- /**
2
- * SyncDev 命令 - 同步开发者管理员到数据库
3
- * - 邮箱: 通过 DEV_EMAIL 环境变量配置(默认 dev@qq.com)
4
- * - 姓名: 开发者
5
- * - 密码: 使用 bcrypt 加密,通过 DEV_PASSWORD 环境变量配置
6
- * - 角色: 同步 dev, user, admin, guest 四个角色
7
- * - dev: 拥有所有菜单和接口权限
8
- * - user, admin, guest: 菜单和接口权限为空
9
- * - 同步完成后:重建角色接口权限缓存到 Redis(极简方案:覆盖更新)
10
- * - 表名: addon_admin_admin
11
- */
12
-
13
- import type { SyncDevOptions } from "../types/sync.js";
14
-
15
- import { beflyConfig } from "../befly.config.js";
16
- import { CacheHelper } from "../lib/cacheHelper.js";
1
+ import type { BeflyContext } from "../types/befly.js";
2
+
17
3
  import { Cipher } from "../lib/cipher.js";
18
- import { Connect } from "../lib/connect.js";
19
- import { DbHelper } from "../lib/dbHelper.js";
20
4
  import { Logger } from "../lib/logger.js";
21
- import { RedisHelper } from "../lib/redisHelper.js";
22
-
23
- /**
24
- * SyncDev 命令主函数
25
- */
26
- export async function syncDevCommand(options: SyncDevOptions = {}): Promise<void> {
27
- try {
28
- if (options.plan) {
29
- Logger.debug("[计划] 同步完成后将初始化/更新开发管理员账号(plan 模式不执行)");
30
- return;
31
- }
32
5
 
33
- if (!beflyConfig.devPassword) {
34
- // 未配置开发者密码,跳过同步
35
- return;
36
- }
6
+ export type SyncDevConfig = {
7
+ devEmail?: string;
8
+ devPassword?: string;
9
+ };
37
10
 
38
- // 连接数据库(SQL + Redis)
39
- await Connect.connect();
11
+ export async function syncDev(ctx: BeflyContext, config: SyncDevConfig = {}): Promise<void> {
12
+ if (!config.devPassword) {
13
+ return;
14
+ }
40
15
 
41
- const redisHelper = new RedisHelper();
42
- const helper = new DbHelper({ redis: redisHelper } as any, Connect.getSql());
16
+ const devEmail = typeof config.devEmail === "string" && config.devEmail.length > 0 ? config.devEmail : "dev@qq.com";
43
17
 
44
- // 检查 addon_admin_admin 表是否存在
45
- const existAdmin = await helper.tableExists("addon_admin_admin");
46
- if (!existAdmin) {
47
- Logger.debug("[SyncDev] 表 addon_admin_admin 不存在,跳过开发者账号同步");
48
- return;
49
- }
18
+ if (!ctx.db) {
19
+ throw new Error("syncDev: ctx.db 未初始化(Db 插件未加载或注入失败)");
20
+ }
50
21
 
51
- // 检查 addon_admin_role 表是否存在
52
- const existRole = await helper.tableExists("addon_admin_role");
53
- if (!existRole) {
54
- Logger.debug("[SyncDev] 表 addon_admin_role 不存在,跳过开发者账号同步");
55
- return;
56
- }
22
+ if (!ctx.cache) {
23
+ throw new Error("syncDev: ctx.cache 未初始化(cache 插件未加载或注入失败)");
24
+ }
57
25
 
58
- // 检查 addon_admin_menu 表是否存在
59
- const existMenu = await helper.tableExists("addon_admin_menu");
60
- if (!existMenu) {
61
- Logger.debug("[SyncDev] 表 addon_admin_menu 不存在,跳过开发者账号同步");
62
- return;
63
- }
26
+ if (!(await ctx.db.tableExists("addon_admin_admin"))) {
27
+ Logger.debug(`addon_admin_admin 表不存在`);
28
+ return;
29
+ }
30
+ if (!(await ctx.db.tableExists("addon_admin_role"))) {
31
+ Logger.debug(`addon_admin_role 表不存在`);
32
+ return;
33
+ }
34
+ if (!(await ctx.db.tableExists("addon_admin_menu"))) {
35
+ Logger.debug(`addon_admin_menu 表不存在`);
36
+ return;
37
+ }
64
38
 
65
- // 查询所有菜单 ID
66
- const allMenus = await helper.getAll({
67
- table: "addon_admin_menu",
68
- fields: ["id"],
39
+ const allMenus = await ctx.db.getAll({
40
+ table: "addon_admin_menu",
41
+ fields: ["path"],
42
+ where: { state$gte: 0 },
43
+ orderBy: ["id#ASC"]
44
+ } as any);
45
+
46
+ const menuPaths = Array.from(new Set((allMenus.lists || []).map((m: any) => (typeof m?.path === "string" ? m.path.trim() : "")).filter((p: string) => p.length > 0)));
47
+
48
+ const existApi = await ctx.db.tableExists("addon_admin_api");
49
+ let apiPaths: string[] = [];
50
+ if (existApi) {
51
+ const allApis = await ctx.db.getAll({
52
+ table: "addon_admin_api",
53
+ fields: ["routePath"],
54
+ where: { state$gte: 0 },
69
55
  orderBy: ["id#ASC"]
70
- });
56
+ } as any);
57
+
58
+ apiPaths = Array.from(
59
+ new Set(
60
+ (allApis.lists || []).map((a: any) => {
61
+ if (typeof a?.routePath !== "string") {
62
+ throw new Error("syncDev: addon_admin_api.routePath 必须是字符串");
63
+ }
64
+
65
+ const routePath = a.routePath.trim();
66
+ if (routePath.length === 0) {
67
+ throw new Error("syncDev: addon_admin_api.routePath 不允许为空字符串");
68
+ }
69
+
70
+ if (!routePath.startsWith("/")) {
71
+ throw new Error(`syncDev: addon_admin_api.routePath 必须是 pathname(以 / 开头),当前值=${routePath}`);
72
+ }
73
+
74
+ return routePath;
75
+ })
76
+ )
77
+ );
78
+ }
71
79
 
72
- if (!allMenus || !Array.isArray(allMenus.lists)) {
73
- Logger.debug("[SyncDev] 菜单数据为空,跳过开发者账号同步");
74
- return;
80
+ const roles = [
81
+ {
82
+ code: "dev",
83
+ name: "开发者角色",
84
+ description: "拥有所有菜单和接口权限的开发者角色",
85
+ menus: menuPaths,
86
+ apis: apiPaths,
87
+ sort: 0
88
+ },
89
+ {
90
+ code: "user",
91
+ name: "用户角色",
92
+ description: "普通用户角色",
93
+ menus: [],
94
+ apis: [],
95
+ sort: 1
96
+ },
97
+ {
98
+ code: "admin",
99
+ name: "管理员角色",
100
+ description: "管理员角色",
101
+ menus: [],
102
+ apis: [],
103
+ sort: 2
104
+ },
105
+ {
106
+ code: "guest",
107
+ name: "访客角色",
108
+ description: "访客角色",
109
+ menus: [],
110
+ apis: [],
111
+ sort: 3
75
112
  }
113
+ ];
76
114
 
77
- const menuIds = allMenus.lists.length > 0 ? allMenus.lists.map((m: any) => m.id) : [];
115
+ let devRole = null;
116
+ for (const roleConfig of roles) {
117
+ const existingRole = await ctx.db.getOne({
118
+ table: "addon_admin_role",
119
+ where: { code: roleConfig.code }
120
+ });
78
121
 
79
- // 查询所有接口 ID
80
- const existApi = await helper.tableExists("addon_admin_api");
81
- let apiIds: number[] = [];
82
- if (existApi) {
83
- const allApis = await helper.getAll({
84
- table: "addon_admin_api",
85
- fields: ["id"],
86
- orderBy: ["id#ASC"]
87
- });
122
+ if (existingRole) {
123
+ const nextMenus = roleConfig.menus;
124
+ const nextApis = roleConfig.apis;
88
125
 
89
- if (allApis && Array.isArray(allApis.lists) && allApis.lists.length > 0) {
90
- apiIds = allApis.lists.map((a: any) => a.id);
91
- }
92
- }
126
+ const existingMenus = Array.isArray(existingRole.menus) ? existingRole.menus : [];
127
+ const existingApis = Array.isArray(existingRole.apis) ? existingRole.apis : [];
93
128
 
94
- // 定义四个角色的配置
95
- const roles = [
96
- {
97
- code: "dev",
98
- name: "开发者角色",
99
- description: "拥有所有菜单和接口权限的开发者角色",
100
- menus: menuIds,
101
- apis: apiIds,
102
- sort: 0
103
- },
104
- {
105
- code: "user",
106
- name: "用户角色",
107
- description: "普通用户角色",
108
- menus: [],
109
- apis: [],
110
- sort: 1
111
- },
112
- {
113
- code: "admin",
114
- name: "管理员角色",
115
- description: "管理员角色",
116
- menus: [],
117
- apis: [],
118
- sort: 2
119
- },
120
- {
121
- code: "guest",
122
- name: "访客角色",
123
- description: "访客角色",
124
- menus: [],
125
- apis: [],
126
- sort: 3
127
- }
128
- ];
129
+ const menusChanged = existingMenus.length !== nextMenus.length || existingMenus.some((v: any, i: number) => v !== nextMenus[i]);
130
+ const apisChanged = existingApis.length !== nextApis.length || existingApis.some((v: any, i: number) => v !== nextApis[i]);
129
131
 
130
- // 同步所有角色
131
- let devRole = null;
132
- for (const roleConfig of roles) {
133
- const existingRole = await helper.getOne({
134
- table: "addon_admin_role",
135
- where: { code: roleConfig.code }
136
- });
132
+ const hasChanges = existingRole.name !== roleConfig.name || existingRole.description !== roleConfig.description || menusChanged || apisChanged || existingRole.sort !== roleConfig.sort;
137
133
 
138
- if (existingRole) {
139
- // 检查字段是否有变化
140
- const existingMenusJson = JSON.stringify(existingRole.menus || []);
141
- const existingApisJson = JSON.stringify(existingRole.apis || []);
142
- const nextMenusJson = JSON.stringify(roleConfig.menus);
143
- const nextApisJson = JSON.stringify(roleConfig.apis);
144
-
145
- const hasChanges = existingRole.name !== roleConfig.name || existingRole.description !== roleConfig.description || existingMenusJson !== nextMenusJson || existingApisJson !== nextApisJson || existingRole.sort !== roleConfig.sort;
146
-
147
- if (hasChanges) {
148
- // 更新现有角色
149
- await helper.updData({
150
- table: "addon_admin_role",
151
- where: { code: roleConfig.code },
152
- data: {
153
- name: roleConfig.name,
154
- description: roleConfig.description,
155
- menus: roleConfig.menus,
156
- apis: roleConfig.apis,
157
- sort: roleConfig.sort
158
- }
159
- });
160
- }
161
- if (roleConfig.code === "dev") {
162
- devRole = existingRole;
163
- }
164
- } else {
165
- // 创建新角色
166
- const roleId = await helper.insData({
134
+ if (hasChanges) {
135
+ await ctx.db.updData({
167
136
  table: "addon_admin_role",
168
- data: roleConfig
137
+ where: { code: roleConfig.code },
138
+ data: {
139
+ name: roleConfig.name,
140
+ description: roleConfig.description,
141
+ menus: roleConfig.menus,
142
+ apis: roleConfig.apis,
143
+ sort: roleConfig.sort
144
+ }
169
145
  });
170
- if (roleConfig.code === "dev") {
171
- devRole = { id: roleId };
172
- }
146
+ }
147
+ if (roleConfig.code === "dev") {
148
+ devRole = existingRole;
149
+ }
150
+ } else {
151
+ const roleId = await ctx.db.insData({
152
+ table: "addon_admin_role",
153
+ data: roleConfig
154
+ });
155
+ if (roleConfig.code === "dev") {
156
+ devRole = { id: roleId };
173
157
  }
174
158
  }
159
+ }
175
160
 
176
- if (!devRole) {
177
- Logger.error("dev 角色不存在,无法创建开发者账号");
178
- return;
179
- }
161
+ if (!devRole) {
162
+ Logger.error("dev 角色不存在,无法创建开发者账号");
163
+ return;
164
+ }
180
165
 
181
- // 先对密码进行 SHA-256 + 盐值 哈希(模拟前端加密),再用 bcrypt 存储
182
- const sha256Hashed = Cipher.sha256(beflyConfig.devPassword + "befly");
183
- const hashed = await Cipher.hashPassword(sha256Hashed);
184
-
185
- // 准备开发管理员数据
186
- const devData = {
187
- nickname: "开发者",
188
- email: beflyConfig.devEmail,
189
- username: "dev",
190
- password: hashed,
191
- roleCode: "dev",
192
- roleType: "admin"
193
- };
194
-
195
- // 查询现有账号
196
- const existing = await helper.getOne({
166
+ const sha256Hashed = Cipher.sha256(config.devPassword + "befly");
167
+ const hashed = await Cipher.hashPassword(sha256Hashed);
168
+
169
+ const devData = {
170
+ nickname: "开发者",
171
+ email: devEmail,
172
+ username: "dev",
173
+ password: hashed,
174
+ roleCode: "dev",
175
+ roleType: "admin"
176
+ };
177
+
178
+ const existing = await ctx.db.getOne({
179
+ table: "addon_admin_admin",
180
+ where: { email: devEmail }
181
+ });
182
+
183
+ if (existing) {
184
+ await ctx.db.updData({
197
185
  table: "addon_admin_admin",
198
- where: { email: beflyConfig.devEmail }
186
+ where: { email: devEmail },
187
+ data: devData
188
+ });
189
+ } else {
190
+ await ctx.db.insData({
191
+ table: "addon_admin_admin",
192
+ data: devData
199
193
  });
200
-
201
- if (existing) {
202
- // 更新现有账号
203
- await helper.updData({
204
- table: "addon_admin_admin",
205
- where: { email: beflyConfig.devEmail },
206
- data: devData
207
- });
208
- } else {
209
- // 插入新账号
210
- await helper.insData({
211
- table: "addon_admin_admin",
212
- data: devData
213
- });
214
- }
215
-
216
- // 重建角色接口权限缓存到 Redis(极简方案:覆盖更新)
217
- // 说明:syncDev 会修改角色 apis,需同步刷新对应角色权限缓存
218
- try {
219
- const cacheHelper = new CacheHelper({ db: helper, redis: redisHelper } as any);
220
- await cacheHelper.rebuildRoleApiPermissions();
221
- } catch (error: any) {
222
- Logger.warn({ err: error }, "[SyncDev] 重建角色接口权限缓存失败");
223
- }
224
- } catch (error: any) {
225
- Logger.error({ err: error }, "同步开发者管理员失败");
226
- throw error;
227
- } finally {
228
- await Connect.disconnect();
229
194
  }
230
195
  }