befly 3.10.0 → 3.10.2

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 (79) hide show
  1. package/.gitignore +0 -0
  2. package/README.md +10 -13
  3. package/configs/presetFields.ts +10 -0
  4. package/configs/presetRegexp.ts +225 -0
  5. package/docs/README.md +17 -11
  6. package/docs/api/api.md +15 -1
  7. package/docs/guide/quickstart.md +19 -5
  8. package/docs/infra/redis.md +23 -11
  9. package/docs/quickstart.md +5 -335
  10. package/docs/reference/addon.md +0 -15
  11. package/docs/reference/config.md +1 -1
  12. package/docs/reference/logger.md +3 -3
  13. package/docs/reference/sync.md +99 -73
  14. package/docs/reference/table.md +1 -1
  15. package/package.json +15 -16
  16. package/docs/cipher.md +0 -582
  17. package/docs/database.md +0 -1176
  18. package/tests/_mocks/mockSqliteDb.ts +0 -204
  19. package/tests/addonHelper-cache.test.ts +0 -32
  20. package/tests/api-integration-array-number.test.ts +0 -282
  21. package/tests/apiHandler-routePath-only.test.ts +0 -32
  22. package/tests/befly-config-env.test.ts +0 -78
  23. package/tests/cacheHelper.test.ts +0 -323
  24. package/tests/cacheKeys.test.ts +0 -41
  25. package/tests/checkApi-routePath-strict.test.ts +0 -166
  26. package/tests/checkMenu.test.ts +0 -346
  27. package/tests/checkTable-smoke.test.ts +0 -157
  28. package/tests/cipher.test.ts +0 -249
  29. package/tests/dbDialect-cache.test.ts +0 -23
  30. package/tests/dbDialect.test.ts +0 -46
  31. package/tests/dbHelper-advanced.test.ts +0 -723
  32. package/tests/dbHelper-all-array-types.test.ts +0 -316
  33. package/tests/dbHelper-array-serialization.test.ts +0 -258
  34. package/tests/dbHelper-batch-write.test.ts +0 -90
  35. package/tests/dbHelper-columns.test.ts +0 -234
  36. package/tests/dbHelper-execute.test.ts +0 -187
  37. package/tests/dbHelper-joins.test.ts +0 -221
  38. package/tests/fields-redis-cache.test.ts +0 -127
  39. package/tests/fields-validate.test.ts +0 -99
  40. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +0 -3
  41. package/tests/fixtures/scanFilesApis/a.ts +0 -3
  42. package/tests/fixtures/scanFilesApis/sub/b.ts +0 -3
  43. package/tests/getClientIp.test.ts +0 -54
  44. package/tests/integration.test.ts +0 -189
  45. package/tests/jwt.test.ts +0 -65
  46. package/tests/loadPlugins-order-smoke.test.ts +0 -75
  47. package/tests/logger.test.ts +0 -325
  48. package/tests/redisHelper.test.ts +0 -495
  49. package/tests/redisKeys.test.ts +0 -9
  50. package/tests/scanConfig.test.ts +0 -144
  51. package/tests/scanFiles-routePath.test.ts +0 -46
  52. package/tests/smoke-sql.test.ts +0 -24
  53. package/tests/sqlBuilder-advanced.test.ts +0 -608
  54. package/tests/sqlBuilder.test.ts +0 -209
  55. package/tests/sync-connection.test.ts +0 -183
  56. package/tests/sync-init-guard.test.ts +0 -105
  57. package/tests/syncApi-insBatch-fields-consistent.test.ts +0 -61
  58. package/tests/syncApi-obsolete-records.test.ts +0 -69
  59. package/tests/syncApi-type-compat.test.ts +0 -72
  60. package/tests/syncDev-permissions.test.ts +0 -81
  61. package/tests/syncMenu-disableMenus-hard-delete.test.ts +0 -88
  62. package/tests/syncMenu-duplicate-path.test.ts +0 -122
  63. package/tests/syncMenu-obsolete-records.test.ts +0 -161
  64. package/tests/syncMenu-parentPath-from-tree.test.ts +0 -75
  65. package/tests/syncMenu-paths.test.ts +0 -59
  66. package/tests/syncTable-apply.test.ts +0 -279
  67. package/tests/syncTable-array-number.test.ts +0 -160
  68. package/tests/syncTable-constants.test.ts +0 -101
  69. package/tests/syncTable-db-integration.test.ts +0 -237
  70. package/tests/syncTable-ddl.test.ts +0 -245
  71. package/tests/syncTable-helpers.test.ts +0 -99
  72. package/tests/syncTable-schema.test.ts +0 -99
  73. package/tests/syncTable-testkit.test.ts +0 -25
  74. package/tests/syncTable-types.test.ts +0 -122
  75. package/tests/tableRef-and-deserialize.test.ts +0 -67
  76. package/tests/util.test.ts +0 -100
  77. package/tests/validator-array-number.test.ts +0 -310
  78. package/tests/validator-default.test.ts +0 -373
  79. package/tests/validator.test.ts +0 -679
@@ -1,323 +0,0 @@
1
- /**
2
- * CacheHelper 单元测试
3
- */
4
-
5
- import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
6
-
7
- import { CacheHelper } from "../lib/cacheHelper.js";
8
- import { CacheKeys } from "../lib/cacheKeys.js";
9
- import { setMockLogger } from "../lib/logger.js";
10
-
11
- // Mock pino logger
12
- const mockPino = {
13
- info: mock(() => {}),
14
- warn: mock(() => {}),
15
- error: mock(() => {}),
16
- debug: mock(() => {}),
17
- fatal: mock(() => {}),
18
- trace: mock(() => {}),
19
- silent: mock(() => {}),
20
- child: mock(() => mockPino),
21
- level: "info"
22
- };
23
-
24
- describe("CacheHelper", () => {
25
- let cacheHelper: CacheHelper;
26
- let mockDb: any;
27
- let mockRedis: any;
28
-
29
- beforeEach(() => {
30
- // 设置 mock logger
31
- setMockLogger(mockPino as any);
32
-
33
- // Mock 数据库方法
34
- mockDb = {
35
- tableExists: mock(() => Promise.resolve(true)),
36
- getAll: mock(() => Promise.resolve({ lists: [], total: 0 }))
37
- };
38
-
39
- // Mock Redis 方法
40
- mockRedis = {
41
- setObject: mock(() => Promise.resolve("OK")),
42
- getObject: mock(() => Promise.resolve(null)),
43
- sadd: mock(() => Promise.resolve(1)),
44
- saddBatch: mock(() => Promise.resolve(1)),
45
- smembers: mock(() => Promise.resolve([])),
46
- sismember: mock(() => Promise.resolve(0)),
47
- del: mock(() => Promise.resolve(1)),
48
- delBatch: mock(() => Promise.resolve(1)),
49
- exists: mock(() => Promise.resolve(true))
50
- };
51
-
52
- cacheHelper = new CacheHelper({ db: mockDb, redis: mockRedis });
53
- });
54
-
55
- afterEach(() => {
56
- // 重置 mock logger
57
- setMockLogger(null);
58
- });
59
-
60
- describe("cacheApis", () => {
61
- it("表不存在时跳过缓存", async () => {
62
- mockDb.tableExists = mock(() => Promise.resolve(false));
63
-
64
- await cacheHelper.cacheApis();
65
-
66
- expect(mockDb.tableExists).toHaveBeenCalledWith("addon_admin_api");
67
- expect(mockDb.getAll).not.toHaveBeenCalled();
68
- });
69
-
70
- it("正常缓存接口列表", async () => {
71
- const apis = [
72
- { id: 1, name: "登录", routePath: "/api/login" },
73
- { id: 2, name: "用户列表", routePath: "/api/user/list" }
74
- ];
75
- mockDb.getAll = mock(() => Promise.resolve({ lists: apis, total: apis.length }));
76
-
77
- await cacheHelper.cacheApis();
78
-
79
- expect(mockRedis.setObject).toHaveBeenCalledWith(CacheKeys.apisAll(), apis);
80
- });
81
-
82
- it("缓存失败时记录警告", async () => {
83
- mockRedis.setObject = mock(() => Promise.resolve(null));
84
-
85
- await cacheHelper.cacheApis();
86
-
87
- expect(mockPino.warn).toHaveBeenCalled();
88
- });
89
-
90
- it("异常时记录错误", async () => {
91
- mockDb.getAll = mock(() => Promise.reject(new Error("DB Error")));
92
-
93
- await cacheHelper.cacheApis();
94
-
95
- expect(mockPino.error).toHaveBeenCalled();
96
- });
97
- });
98
-
99
- describe("cacheMenus", () => {
100
- it("表不存在时跳过缓存", async () => {
101
- mockDb.tableExists = mock(() => Promise.resolve(false));
102
-
103
- await cacheHelper.cacheMenus();
104
-
105
- expect(mockDb.tableExists).toHaveBeenCalledWith("addon_admin_menu");
106
- expect(mockDb.getAll).not.toHaveBeenCalled();
107
- });
108
-
109
- it("正常缓存菜单列表", async () => {
110
- const menus = [
111
- { id: 1, pid: 0, name: "首页", path: "/home", sort: 1 },
112
- { id: 2, pid: 0, name: "用户管理", path: "/user", sort: 2 }
113
- ];
114
- mockDb.getAll = mock(() => Promise.resolve({ lists: menus, total: menus.length }));
115
-
116
- await cacheHelper.cacheMenus();
117
-
118
- expect(mockRedis.setObject).toHaveBeenCalledWith(CacheKeys.menusAll(), menus);
119
- });
120
- });
121
-
122
- describe("rebuildRoleApiPermissions", () => {
123
- it("表不存在时跳过缓存", async () => {
124
- mockDb.tableExists = mock((table: string) => {
125
- if (table === "addon_admin_role") return Promise.resolve(false);
126
- return Promise.resolve(false);
127
- });
128
-
129
- await cacheHelper.rebuildRoleApiPermissions();
130
-
131
- expect(mockPino.warn).toHaveBeenCalled();
132
- });
133
-
134
- it("正常重建角色权限(覆盖更新)", async () => {
135
- const roles = [
136
- { id: 1, code: "admin", apis: ["/api/login", "/api/user/list", "/api/user/del"] },
137
- { id: 2, code: "user", apis: ["/api/login"] }
138
- ];
139
-
140
- mockDb.getAll = mock((opts: any) => {
141
- if (opts.table === "addon_admin_role") return Promise.resolve({ lists: roles, total: roles.length });
142
- return Promise.resolve({ lists: [], total: 0 });
143
- });
144
-
145
- await cacheHelper.rebuildRoleApiPermissions();
146
-
147
- // 验证批量删除角色 key
148
- expect(mockRedis.delBatch).toHaveBeenCalledTimes(1);
149
- const delBatchArgs = (mockRedis.delBatch as any).mock.calls[0][0] as string[];
150
- expect(delBatchArgs).toEqual([CacheKeys.roleApis("admin"), CacheKeys.roleApis("user")]);
151
-
152
- // 验证批量写入
153
- expect(mockRedis.saddBatch).toHaveBeenCalledTimes(1);
154
- const saddBatchArgs = (mockRedis.saddBatch as any).mock.calls[0][0] as Array<{
155
- key: string;
156
- members: string[];
157
- }>;
158
- expect(saddBatchArgs).toEqual([
159
- {
160
- key: CacheKeys.roleApis("admin"),
161
- members: ["/api/login", "/api/user/del", "/api/user/list"]
162
- },
163
- { key: CacheKeys.roleApis("user"), members: ["/api/login"] }
164
- ]);
165
- });
166
-
167
- it("无权限时仍会清理旧缓存,但不写入成员", async () => {
168
- const roles = [{ id: 1, code: "empty", apis: [] }];
169
-
170
- mockDb.getAll = mock((opts: any) => {
171
- if (opts.table === "addon_admin_role") return Promise.resolve({ lists: roles, total: roles.length });
172
- return Promise.resolve({ lists: [], total: 0 });
173
- });
174
-
175
- await cacheHelper.rebuildRoleApiPermissions();
176
-
177
- expect(mockRedis.delBatch).toHaveBeenCalledTimes(1);
178
- expect(mockRedis.saddBatch).not.toHaveBeenCalled();
179
- });
180
- });
181
-
182
- describe("refreshRoleApiPermissions", () => {
183
- it("apiPaths 为空数组时只清理缓存,不查询 DB", async () => {
184
- mockDb.getAll = mock(() => Promise.resolve({ lists: [], total: 0 }));
185
-
186
- await cacheHelper.refreshRoleApiPermissions("admin", []);
187
-
188
- expect(mockRedis.del).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
189
- expect(mockDb.getAll).not.toHaveBeenCalled();
190
- });
191
-
192
- it("apiPaths 非空时直接覆盖更新该角色缓存(DEL + SADD)", async () => {
193
- await cacheHelper.refreshRoleApiPermissions("admin", ["/api/login", "/api/user/list"]);
194
-
195
- expect(mockRedis.del).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
196
- expect(mockRedis.sadd).toHaveBeenCalledWith(CacheKeys.roleApis("admin"), ["/api/login", "/api/user/list"]);
197
- });
198
- });
199
-
200
- describe("getApis", () => {
201
- it("返回缓存的接口列表", async () => {
202
- const apis = [{ id: 1, name: "登录" }];
203
- mockRedis.getObject = mock(() => Promise.resolve(apis));
204
-
205
- const result = await cacheHelper.getApis();
206
-
207
- expect(result).toEqual(apis);
208
- });
209
-
210
- it("缓存不存在时返回空数组", async () => {
211
- mockRedis.getObject = mock(() => Promise.resolve(null));
212
-
213
- const result = await cacheHelper.getApis();
214
-
215
- expect(result).toEqual([]);
216
- });
217
- });
218
-
219
- describe("getMenus", () => {
220
- it("返回缓存的菜单列表", async () => {
221
- const menus = [{ id: 1, name: "首页" }];
222
- mockRedis.getObject = mock(() => Promise.resolve(menus));
223
-
224
- const result = await cacheHelper.getMenus();
225
-
226
- expect(result).toEqual(menus);
227
- });
228
-
229
- it("缓存不存在时返回空数组", async () => {
230
- mockRedis.getObject = mock(() => Promise.resolve(null));
231
-
232
- const result = await cacheHelper.getMenus();
233
-
234
- expect(result).toEqual([]);
235
- });
236
- });
237
-
238
- describe("getRolePermissions", () => {
239
- it("返回角色的权限列表", async () => {
240
- const permissions = ["/api/login", "/api/user/list"];
241
- mockRedis.smembers = mock(() => Promise.resolve(permissions));
242
-
243
- const result = await cacheHelper.getRolePermissions("admin");
244
-
245
- expect(mockRedis.smembers).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
246
- expect(result).toEqual(permissions);
247
- });
248
-
249
- it("权限不存在时返回空数组", async () => {
250
- mockRedis.smembers = mock(() => Promise.resolve(null));
251
-
252
- const result = await cacheHelper.getRolePermissions("unknown");
253
-
254
- expect(result).toEqual([]);
255
- });
256
- });
257
-
258
- describe("checkRolePermission", () => {
259
- it("有权限时返回 true", async () => {
260
- mockRedis.sismember = mock(() => Promise.resolve(true));
261
-
262
- const result = await cacheHelper.checkRolePermission("admin", "/api/login");
263
-
264
- expect(mockRedis.sismember).toHaveBeenCalledWith(CacheKeys.roleApis("admin"), "/api/login");
265
- expect(result).toBe(true);
266
- });
267
-
268
- it("无权限时返回 false", async () => {
269
- mockRedis.sismember = mock(() => Promise.resolve(false));
270
-
271
- const result = await cacheHelper.checkRolePermission("user", "/api/admin/del");
272
-
273
- expect(result).toBe(false);
274
- });
275
- });
276
-
277
- describe("deleteRolePermissions", () => {
278
- it("删除成功返回 true", async () => {
279
- mockRedis.del = mock(() => Promise.resolve(1));
280
-
281
- const result = await cacheHelper.deleteRolePermissions("admin");
282
-
283
- expect(mockRedis.del).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
284
- expect(result).toBe(true);
285
- });
286
-
287
- it("不存在时返回 false", async () => {
288
- mockRedis.del = mock(() => Promise.resolve(0));
289
-
290
- const result = await cacheHelper.deleteRolePermissions("unknown");
291
-
292
- expect(result).toBe(false);
293
- });
294
- });
295
-
296
- describe("cacheAll", () => {
297
- it("按顺序调用三个缓存方法", async () => {
298
- const callOrder: string[] = [];
299
-
300
- // 使用 spy 记录调用顺序
301
- const originalCacheApis = cacheHelper.cacheApis.bind(cacheHelper);
302
- const originalCacheMenus = cacheHelper.cacheMenus.bind(cacheHelper);
303
- const originalRebuildRoleApiPermissions = cacheHelper.rebuildRoleApiPermissions.bind(cacheHelper);
304
-
305
- cacheHelper.cacheApis = async () => {
306
- callOrder.push("apis");
307
- await originalCacheApis();
308
- };
309
- cacheHelper.cacheMenus = async () => {
310
- callOrder.push("menus");
311
- await originalCacheMenus();
312
- };
313
- cacheHelper.rebuildRoleApiPermissions = async () => {
314
- callOrder.push("permissions");
315
- await originalRebuildRoleApiPermissions();
316
- };
317
-
318
- await cacheHelper.cacheAll();
319
-
320
- expect(callOrder).toEqual(["apis", "menus", "permissions"]);
321
- });
322
- });
323
- });
@@ -1,41 +0,0 @@
1
- /**
2
- * CacheKeys 测试
3
- */
4
-
5
- import { describe, expect, test } from "bun:test";
6
-
7
- import { CacheKeys } from "../lib/cacheKeys.js";
8
-
9
- describe("CacheKeys - Key 生成函数", () => {
10
- test("apisAll - 返回固定的接口缓存键", () => {
11
- expect(CacheKeys.apisAll()).toBe("befly:apis:all");
12
- });
13
-
14
- test("menusAll - 返回固定的菜单缓存键", () => {
15
- expect(CacheKeys.menusAll()).toBe("befly:menus:all");
16
- });
17
-
18
- test("roleInfo - 返回带角色代码的缓存键", () => {
19
- expect(CacheKeys.roleInfo("admin")).toBe("befly:role:info:admin");
20
- expect(CacheKeys.roleInfo("user")).toBe("befly:role:info:user");
21
- });
22
-
23
- test("roleApis - 返回带角色代码的权限缓存键", () => {
24
- expect(CacheKeys.roleApis("admin")).toBe("befly:role:apis:admin");
25
- expect(CacheKeys.roleApis("guest")).toBe("befly:role:apis:guest");
26
- });
27
-
28
- test("tableColumns - 返回带表名的结构缓存键", () => {
29
- expect(CacheKeys.tableColumns("user")).toBe("befly:table:columns:user");
30
- expect(CacheKeys.tableColumns("addon_admin_role")).toBe("befly:table:columns:addon_admin_role");
31
- });
32
-
33
- test("roleInfo - 特殊字符处理", () => {
34
- expect(CacheKeys.roleInfo("super-admin")).toBe("befly:role:info:super-admin");
35
- expect(CacheKeys.roleInfo("role_1")).toBe("befly:role:info:role_1");
36
- });
37
-
38
- test("tableColumns - 空字符串", () => {
39
- expect(CacheKeys.tableColumns("")).toBe("befly:table:columns:");
40
- });
41
- });
@@ -1,166 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { checkApi } from "../checks/checkApi.js";
4
-
5
- describe("checkApi - routePath strict", () => {
6
- test("合法 routePath 应通过", async () => {
7
- let thrown: any = null;
8
-
9
- try {
10
- await checkApi([
11
- {
12
- name: "hello",
13
- handler: () => {
14
- return null;
15
- },
16
- routePath: "/api/hello",
17
- routePrefix: "/app"
18
- }
19
- ]);
20
- } catch (error: any) {
21
- thrown = error;
22
- }
23
-
24
- expect(thrown).toBeNull();
25
- });
26
-
27
- test("routePath 为空字符串应阻断启动", async () => {
28
- let thrown: any = null;
29
-
30
- try {
31
- await checkApi([
32
- {
33
- name: "hello",
34
- handler: () => null,
35
- routePath: "",
36
- routePrefix: "/app"
37
- }
38
- ]);
39
- } catch (error: any) {
40
- thrown = error;
41
- }
42
-
43
- expect(thrown).toBeTruthy();
44
- expect(thrown.message).toBe("接口结构检查失败");
45
- });
46
-
47
- test("routePath 非字符串应阻断启动", async () => {
48
- let thrown: any = null;
49
-
50
- try {
51
- await checkApi([
52
- {
53
- name: "hello",
54
- handler: () => null,
55
- routePath: 123,
56
- routePrefix: "/app"
57
- } as any
58
- ]);
59
- } catch (error: any) {
60
- thrown = error;
61
- }
62
-
63
- expect(thrown).toBeTruthy();
64
- expect(thrown.message).toBe("接口结构检查失败");
65
- });
66
-
67
- test("routePath 不允许 method 前缀(POST/api/...)", async () => {
68
- let thrown: any = null;
69
-
70
- try {
71
- await checkApi([
72
- {
73
- name: "hello",
74
- handler: () => null,
75
- routePath: "POST/api/hello",
76
- routePrefix: "/app"
77
- }
78
- ]);
79
- } catch (error: any) {
80
- thrown = error;
81
- }
82
-
83
- expect(thrown).toBeTruthy();
84
- expect(thrown.message).toBe("接口结构检查失败");
85
- });
86
-
87
- test("routePath 不允许 method + 空格(POST /api/...)", async () => {
88
- let thrown: any = null;
89
-
90
- try {
91
- await checkApi([
92
- {
93
- name: "hello",
94
- handler: () => null,
95
- routePath: "POST /api/hello",
96
- routePrefix: "/app"
97
- }
98
- ]);
99
- } catch (error: any) {
100
- thrown = error;
101
- }
102
-
103
- expect(thrown).toBeTruthy();
104
- expect(thrown.message).toBe("接口结构检查失败");
105
- });
106
-
107
- test("routePath 必须以 /api/ 开头", async () => {
108
- let thrown: any = null;
109
-
110
- try {
111
- await checkApi([
112
- {
113
- name: "hello",
114
- handler: () => null,
115
- routePath: "/app/hello",
116
- routePrefix: "/app"
117
- }
118
- ]);
119
- } catch (error: any) {
120
- thrown = error;
121
- }
122
-
123
- expect(thrown).toBeTruthy();
124
- expect(thrown.message).toBe("接口结构检查失败");
125
- });
126
-
127
- test("routePath 不允许包含空格", async () => {
128
- let thrown: any = null;
129
-
130
- try {
131
- await checkApi([
132
- {
133
- name: "hello",
134
- handler: () => null,
135
- routePath: "/api/hello world",
136
- routePrefix: "/app"
137
- }
138
- ]);
139
- } catch (error: any) {
140
- thrown = error;
141
- }
142
-
143
- expect(thrown).toBeTruthy();
144
- expect(thrown.message).toBe("接口结构检查失败");
145
- });
146
-
147
- test("routePath 不允许出现 /api//(重复斜杠)", async () => {
148
- let thrown: any = null;
149
-
150
- try {
151
- await checkApi([
152
- {
153
- name: "hello",
154
- handler: () => null,
155
- routePath: "/api//hello",
156
- routePrefix: "/app"
157
- }
158
- ]);
159
- } catch (error: any) {
160
- thrown = error;
161
- }
162
-
163
- expect(thrown).toBeTruthy();
164
- expect(thrown.message).toBe("接口结构检查失败");
165
- });
166
- });