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.
Files changed (144) hide show
  1. package/README.md +47 -19
  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 +17 -11
  9. package/docs/api/api.md +16 -2
  10. package/docs/guide/quickstart.md +31 -10
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +26 -14
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +5 -328
  16. package/docs/reference/addon.md +0 -4
  17. package/docs/reference/config.md +14 -31
  18. package/docs/reference/logger.md +3 -3
  19. package/docs/reference/sync.md +132 -237
  20. package/docs/reference/table.md +28 -30
  21. package/hooks/auth.ts +3 -4
  22. package/hooks/cors.ts +4 -6
  23. package/hooks/parser.ts +3 -4
  24. package/hooks/permission.ts +3 -4
  25. package/hooks/validator.ts +3 -4
  26. package/lib/cacheHelper.ts +89 -153
  27. package/lib/cacheKeys.ts +1 -1
  28. package/lib/connect.ts +9 -13
  29. package/lib/dbDialect.ts +285 -0
  30. package/lib/dbHelper.ts +179 -507
  31. package/lib/dbUtils.ts +450 -0
  32. package/lib/logger.ts +41 -5
  33. package/lib/redisHelper.ts +1 -0
  34. package/lib/sqlBuilder.ts +358 -58
  35. package/lib/sqlCheck.ts +136 -0
  36. package/lib/validator.ts +1 -1
  37. package/loader/loadApis.ts +23 -126
  38. package/loader/loadHooks.ts +31 -46
  39. package/loader/loadPlugins.ts +37 -52
  40. package/main.ts +58 -19
  41. package/package.json +24 -25
  42. package/paths.ts +14 -14
  43. package/plugins/cache.ts +12 -6
  44. package/plugins/cipher.ts +2 -2
  45. package/plugins/config.ts +6 -8
  46. package/plugins/db.ts +14 -19
  47. package/plugins/jwt.ts +6 -7
  48. package/plugins/logger.ts +7 -9
  49. package/plugins/redis.ts +8 -10
  50. package/plugins/tool.ts +3 -4
  51. package/router/api.ts +3 -2
  52. package/router/static.ts +7 -5
  53. package/sync/syncApi.ts +80 -235
  54. package/sync/syncCache.ts +16 -0
  55. package/sync/syncDev.ts +167 -202
  56. package/sync/syncMenu.ts +230 -444
  57. package/sync/syncTable.ts +1247 -0
  58. package/tests/_mocks/mockSqliteDb.ts +204 -0
  59. package/tests/addonHelper-cache.test.ts +32 -0
  60. package/tests/apiHandler-routePath-only.test.ts +32 -0
  61. package/tests/cacheHelper.test.ts +16 -51
  62. package/tests/checkApi-routePath-strict.test.ts +166 -0
  63. package/tests/checkMenu.test.ts +346 -0
  64. package/tests/checkTable-smoke.test.ts +157 -0
  65. package/tests/dbDialect-cache.test.ts +23 -0
  66. package/tests/dbDialect.test.ts +46 -0
  67. package/tests/dbHelper-advanced.test.ts +1 -1
  68. package/tests/dbHelper-all-array-types.test.ts +15 -15
  69. package/tests/dbHelper-batch-write.test.ts +90 -0
  70. package/tests/dbHelper-columns.test.ts +36 -54
  71. package/tests/dbHelper-execute.test.ts +26 -26
  72. package/tests/dbHelper-joins.test.ts +85 -176
  73. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  75. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  76. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  77. package/tests/logger.test.ts +6 -6
  78. package/tests/redisHelper.test.ts +6 -1
  79. package/tests/scanFiles-routePath.test.ts +46 -0
  80. package/tests/smoke-sql.test.ts +24 -0
  81. package/tests/sqlBuilder-advanced.test.ts +18 -5
  82. package/tests/sqlBuilder.test.ts +24 -0
  83. package/tests/sync-init-guard.test.ts +105 -0
  84. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  85. package/tests/syncApi-obsolete-records.test.ts +69 -0
  86. package/tests/syncApi-type-compat.test.ts +72 -0
  87. package/tests/syncDev-permissions.test.ts +81 -0
  88. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  89. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  90. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  91. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  92. package/tests/syncMenu-paths.test.ts +0 -9
  93. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  94. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  95. package/tests/syncTable-constants.test.ts +101 -0
  96. package/tests/syncTable-db-integration.test.ts +237 -0
  97. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  98. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  99. package/tests/syncTable-schema.test.ts +99 -0
  100. package/tests/syncTable-testkit.test.ts +25 -0
  101. package/tests/syncTable-types.test.ts +122 -0
  102. package/tests/tableRef-and-deserialize.test.ts +67 -0
  103. package/tsconfig.json +1 -1
  104. package/types/api.d.ts +1 -1
  105. package/types/befly.d.ts +13 -12
  106. package/types/cache.d.ts +2 -2
  107. package/types/context.d.ts +1 -1
  108. package/types/database.d.ts +0 -5
  109. package/types/hook.d.ts +1 -10
  110. package/types/plugin.d.ts +2 -96
  111. package/types/sync.d.ts +19 -25
  112. package/utils/convertBigIntFields.ts +38 -0
  113. package/utils/disableMenusGlob.ts +85 -0
  114. package/utils/importDefault.ts +21 -0
  115. package/utils/isDirentDirectory.ts +23 -0
  116. package/utils/loadMenuConfigs.ts +145 -0
  117. package/utils/processFields.ts +25 -0
  118. package/utils/scanAddons.ts +72 -0
  119. package/utils/scanFiles.ts +129 -21
  120. package/utils/scanSources.ts +64 -0
  121. package/utils/sortModules.ts +137 -0
  122. package/checks/checkApp.ts +0 -55
  123. package/docs/cipher.md +0 -582
  124. package/docs/database.md +0 -1176
  125. package/hooks/rateLimit.ts +0 -276
  126. package/sync/syncAll.ts +0 -35
  127. package/sync/syncDb/apply.ts +0 -192
  128. package/sync/syncDb/constants.ts +0 -119
  129. package/sync/syncDb/ddl.ts +0 -251
  130. package/sync/syncDb/helpers.ts +0 -84
  131. package/sync/syncDb/schema.ts +0 -202
  132. package/sync/syncDb/sqlite.ts +0 -48
  133. package/sync/syncDb/table.ts +0 -207
  134. package/sync/syncDb/tableCreate.ts +0 -163
  135. package/sync/syncDb/types.ts +0 -132
  136. package/sync/syncDb/version.ts +0 -69
  137. package/sync/syncDb.ts +0 -168
  138. package/tests/rateLimit-hook.test.ts +0 -477
  139. package/tests/syncDb-constants.test.ts +0 -130
  140. package/tests/syncDb-schema.test.ts +0 -179
  141. package/tests/syncDb-types.test.ts +0 -139
  142. package/utils/addonHelper.ts +0 -90
  143. package/utils/modules.ts +0 -98
  144. package/utils/route.ts +0 -23
@@ -183,7 +183,7 @@ describe("SqlBuilder - 复杂 WHERE 条件", () => {
183
183
  const result = builder.select(["*"]).from("users").where({ $or: [] }).toSelectSql();
184
184
 
185
185
  // **问题**:空 $or 应该跳过还是报错?
186
- console.log("$or 空数组生成的 SQL:", result.sql);
186
+ expect(typeof result.sql).toBe("string");
187
187
  });
188
188
 
189
189
  test("$and + $or 嵌套", () => {
@@ -234,7 +234,7 @@ describe("SqlBuilder - 复杂 WHERE 条件", () => {
234
234
  .toSelectSql();
235
235
 
236
236
  expect(result.sql).toBeDefined();
237
- console.log("深层嵌套生成的 SQL:", result.sql);
237
+ expect(typeof result.sql).toBe("string");
238
238
  });
239
239
  });
240
240
 
@@ -272,9 +272,15 @@ describe("SqlBuilder - 字段名转义", () => {
272
272
  expect(result.sql).toContain("`profiles`.`bio`");
273
273
  });
274
274
 
275
- test("函数调用不应转义", () => {
275
+ test("函数调用必须使用 selectRaw", () => {
276
276
  const builder = new SqlBuilder();
277
- const result = builder.select(["COUNT(*)", "MAX(age)"]).from("users").toSelectSql();
277
+
278
+ expect(() => {
279
+ builder.select(["COUNT(*)", "MAX(age)"]).from("users").toSelectSql();
280
+ }).toThrow("selectRaw");
281
+
282
+ const builder2 = new SqlBuilder();
283
+ const result = builder2.selectRaw("COUNT(*)").selectRaw("MAX(age)").from("users").toSelectSql();
278
284
 
279
285
  expect(result.sql).toContain("COUNT(*)");
280
286
  expect(result.sql).toContain("MAX(age)");
@@ -283,7 +289,7 @@ describe("SqlBuilder - 字段名转义", () => {
283
289
 
284
290
  test("AS 别名应正确处理", () => {
285
291
  const builder = new SqlBuilder();
286
- const result = builder.select(["name AS userName", "COUNT(*) AS total"]).from("users").toSelectSql();
292
+ const result = builder.select(["name AS userName"]).selectRaw("COUNT(*) AS total").from("users").toSelectSql();
287
293
 
288
294
  expect(result.sql).toContain("`name` AS userName");
289
295
  expect(result.sql).toContain("COUNT(*) AS total");
@@ -322,6 +328,13 @@ describe("SqlBuilder - 表名转义", () => {
322
328
  expect(result.sql).toContain("FROM `user_profiles`");
323
329
  expect(result.sql).not.toContain("``");
324
330
  });
331
+
332
+ test("已有反引号的表名带别名应正确处理", () => {
333
+ const builder = new SqlBuilder();
334
+ const result = builder.select(["*"]).from("`user_profiles` up").toSelectSql();
335
+
336
+ expect(result.sql).toContain("FROM `user_profiles` up");
337
+ });
325
338
  });
326
339
 
327
340
  describe("SqlBuilder - ORDER BY", () => {
@@ -132,6 +132,30 @@ describe("SqlBuilder - INSERT", () => {
132
132
  expect(result.sql).toContain("VALUES (?, ?)");
133
133
  expect(result.params).toEqual(["John", 25]);
134
134
  });
135
+
136
+ test("插入单条数据 - 参数为 undefined 应抛错", () => {
137
+ const builder = new SqlBuilder();
138
+ expect(() => {
139
+ builder.toInsertSql("users", { name: undefined as any, age: 25 });
140
+ }).toThrow("undefined");
141
+ });
142
+
143
+ test("批量插入 - 行字段不一致应抛错", () => {
144
+ const builder = new SqlBuilder();
145
+ expect(() => {
146
+ builder.toInsertSql("users", [{ name: "John", age: 25 }, { name: "Jane" }] as any);
147
+ }).toThrow("字段必须一致");
148
+ });
149
+
150
+ test("批量插入 - 缺字段或 undefined 应抛错", () => {
151
+ const builder = new SqlBuilder();
152
+ expect(() => {
153
+ builder.toInsertSql("users", [
154
+ { name: "John", age: 25 },
155
+ { name: "Jane", age: undefined as any }
156
+ ] as any);
157
+ }).toThrow("undefined");
158
+ });
135
159
  });
136
160
 
137
161
  describe("SqlBuilder - UPDATE", () => {
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { syncApi } from "../sync/syncApi.js";
4
+ import { syncDev } from "../sync/syncDev.js";
5
+ import { syncMenu } from "../sync/syncMenu.js";
6
+
7
+ describe("sync - init guard", () => {
8
+ test("syncApi: ctx.db 缺失时应给出明确错误", async () => {
9
+ const ctx = {} as any;
10
+
11
+ let error: any = null;
12
+ try {
13
+ await syncApi(ctx, [] as any);
14
+ } catch (err: any) {
15
+ error = err;
16
+ }
17
+
18
+ expect(typeof error?.message).toBe("string");
19
+ expect(error.message).toBe("syncApi: ctx.db 未初始化(Db 插件未加载或注入失败)");
20
+ });
21
+
22
+ test("syncApi: ctx.cache 缺失时应给出明确错误", async () => {
23
+ const ctx = { db: {} } as any;
24
+
25
+ let error: any = null;
26
+ try {
27
+ await syncApi(ctx, [] as any);
28
+ } catch (err: any) {
29
+ error = err;
30
+ }
31
+
32
+ expect(typeof error?.message).toBe("string");
33
+ expect(error.message).toBe("syncApi: ctx.cache 未初始化(cache 插件未加载或注入失败)");
34
+ });
35
+
36
+ test("syncDev: ctx.db 缺失时应给出明确错误", async () => {
37
+ const ctx = {} as any;
38
+
39
+ let error: any = null;
40
+ try {
41
+ await syncDev(ctx, { devEmail: "dev@qq.com", devPassword: "dev-password" });
42
+ } catch (err: any) {
43
+ error = err;
44
+ }
45
+
46
+ expect(typeof error?.message).toBe("string");
47
+ expect(error.message).toBe("syncDev: ctx.db 未初始化(Db 插件未加载或注入失败)");
48
+ });
49
+
50
+ test("syncDev: ctx.cache 缺失时应给出明确错误", async () => {
51
+ const ctx = { db: {} } as any;
52
+
53
+ let error: any = null;
54
+ try {
55
+ await syncDev(ctx, { devEmail: "dev@qq.com", devPassword: "dev-password" });
56
+ } catch (err: any) {
57
+ error = err;
58
+ }
59
+
60
+ expect(typeof error?.message).toBe("string");
61
+ expect(error.message).toBe("syncDev: ctx.cache 未初始化(cache 插件未加载或注入失败)");
62
+ });
63
+
64
+ test("syncMenu: ctx.db 缺失时应给出明确错误", async () => {
65
+ const ctx = {} as any;
66
+
67
+ let error: any = null;
68
+ try {
69
+ await syncMenu(ctx, [] as any);
70
+ } catch (err: any) {
71
+ error = err;
72
+ }
73
+
74
+ expect(typeof error?.message).toBe("string");
75
+ expect(error.message).toBe("syncMenu: ctx.db 未初始化(Db 插件未加载或注入失败)");
76
+ });
77
+
78
+ test("syncMenu: ctx.cache 缺失时应给出明确错误", async () => {
79
+ const ctx = { db: {} } as any;
80
+
81
+ let error: any = null;
82
+ try {
83
+ await syncMenu(ctx, [] as any);
84
+ } catch (err: any) {
85
+ error = err;
86
+ }
87
+
88
+ expect(typeof error?.message).toBe("string");
89
+ expect(error.message).toBe("syncMenu: ctx.cache 未初始化(cache 插件未加载或注入失败)");
90
+ });
91
+
92
+ test("syncMenu: ctx.config 缺失时应给出明确错误", async () => {
93
+ const ctx = { db: {}, cache: {} } as any;
94
+
95
+ let error: any = null;
96
+ try {
97
+ await syncMenu(ctx, [] as any);
98
+ } catch (err: any) {
99
+ error = err;
100
+ }
101
+
102
+ expect(typeof error?.message).toBe("string");
103
+ expect(error.message).toBe("syncMenu: ctx.config 未初始化(config 插件未加载或注入失败)");
104
+ });
105
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { syncApi } from "../sync/syncApi.js";
4
+
5
+ describe("syncApi - insBatch rows consistency", () => {
6
+ test("当部分 api 缺少 addonName 时,insBatch 仍应传入稳定字段(addonName 为空字符串)", async () => {
7
+ const calls: any = {
8
+ insBatch: [] as any[],
9
+ cacheApis: 0,
10
+ rebuildRoleApiPermissions: 0
11
+ };
12
+
13
+ const ctx: any = {
14
+ db: {
15
+ tableExists: async () => true,
16
+ getAll: async () => {
17
+ return { lists: [], total: 0 };
18
+ },
19
+ insBatch: async (_table: string, dataList: any[]) => {
20
+ calls.insBatch.push({ table: _table, dataList: dataList });
21
+ return [1, 2];
22
+ },
23
+ updBatch: async () => 0,
24
+ delForceBatch: async () => 0
25
+ },
26
+ cache: {
27
+ cacheApis: async () => {
28
+ calls.cacheApis += 1;
29
+ },
30
+ rebuildRoleApiPermissions: async () => {
31
+ calls.rebuildRoleApiPermissions += 1;
32
+ }
33
+ }
34
+ };
35
+
36
+ // 根因修复后:scanFiles 会确保 API 记录总是携带 addonName(app/core 为 "")。
37
+ // 因此这里模拟真实扫描结果:第二条的 addonName 应该是空字符串而非 undefined。
38
+ const apis: any[] = [
39
+ { type: "api", name: "A", routePath: "/api/addon/admin/a", addonName: "admin" },
40
+ { name: "B", routePath: "/api/app/b", addonName: "" }
41
+ ];
42
+
43
+ await syncApi(ctx, apis as any);
44
+
45
+ expect(calls.insBatch.length).toBe(1);
46
+ expect(calls.insBatch[0].table).toBe("addon_admin_api");
47
+
48
+ const rows = calls.insBatch[0].dataList;
49
+ expect(rows.length).toBe(2);
50
+
51
+ expect(Object.keys(rows[0]).sort()).toEqual(Object.keys(rows[1]).sort());
52
+
53
+ expect(typeof rows[0].addonName).toBe("string");
54
+ expect(typeof rows[1].addonName).toBe("string");
55
+ expect(rows[1].addonName).toBe("");
56
+
57
+ // 缓存同步职责已收敛到 syncCache(启动流程单点调用)
58
+ expect(calls.cacheApis).toBe(0);
59
+ expect(calls.rebuildRoleApiPermissions).toBe(0);
60
+ });
61
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { syncApi } from "../sync/syncApi.js";
4
+
5
+ describe("syncApi - delete obsolete records", () => {
6
+ test("应删除不在配置中的接口记录", async () => {
7
+ const existingRecords = [
8
+ { id: 1, routePath: "/api/app/testSyncKeep", name: "Keep", addonName: "", state: 0 },
9
+ { id: 2, routePath: "/api/app/testSyncRemove", name: "Remove", addonName: "", state: 0 }
10
+ ];
11
+
12
+ const existingByPath = new Map<string, any>();
13
+ for (const record of existingRecords) {
14
+ existingByPath.set(record.routePath, record);
15
+ }
16
+
17
+ const calls = {
18
+ delForceBatch: [] as any[],
19
+ getAllArgs: null as any
20
+ };
21
+
22
+ const dbHelper = {
23
+ tableExists: async () => true,
24
+ updBatch: async () => 0,
25
+ insBatch: async () => [],
26
+ getAll: async (options: any) => {
27
+ calls.getAllArgs = options;
28
+ return { lists: existingRecords };
29
+ },
30
+ delForceBatch: async (_table: any, ids: any[]) => {
31
+ calls.delForceBatch.push(ids);
32
+ return ids.length;
33
+ }
34
+ } as any;
35
+
36
+ const ctx = {
37
+ db: dbHelper,
38
+ addons: [],
39
+ cache: {
40
+ cacheApis: async () => {},
41
+ rebuildRoleApiPermissions: async () => {}
42
+ }
43
+ } as any;
44
+
45
+ const apiItems = [
46
+ {
47
+ source: "app",
48
+ sourceName: "项目",
49
+ filePath: "DUMMY",
50
+ relativePath: "testSyncKeep",
51
+ fileName: "testSyncKeep",
52
+ moduleName: "app_testSyncKeep",
53
+ name: "Keep",
54
+ routePath: "/api/app/testSyncKeep",
55
+ addonName: "",
56
+ fileBaseName: "testSyncKeep.ts",
57
+ fileDir: "DUMMY",
58
+ content: { name: "Keep", handler: async () => {} }
59
+ }
60
+ ] as any;
61
+
62
+ await syncApi(ctx, apiItems);
63
+
64
+ expect(calls.getAllArgs?.fields).toEqual(["id", "routePath", "name", "addonName", "state"]);
65
+
66
+ expect(calls.delForceBatch).toHaveLength(1);
67
+ expect(calls.delForceBatch[0]).toEqual([2]);
68
+ });
69
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { syncApi } from "../sync/syncApi.js";
4
+
5
+ describe("syncApi - type compatibility", () => {
6
+ test("缺少 type 时应视为 api;非 api type 应被跳过", async () => {
7
+ const existingRecords = [
8
+ { id: 1, routePath: "/api/app/keep", name: "Keep", addonName: "", state: 0 },
9
+ { id: 2, routePath: "/api/app/skip", name: "Skip", addonName: "", state: 0 }
10
+ ];
11
+
12
+ const calls = {
13
+ delForceBatch: [] as any[],
14
+ getAllArgs: null as any
15
+ };
16
+
17
+ const dbHelper = {
18
+ tableExists: async () => true,
19
+ updBatch: async () => 0,
20
+ insBatch: async () => [],
21
+ getAll: async (options: any) => {
22
+ calls.getAllArgs = options;
23
+ return { lists: existingRecords };
24
+ },
25
+ delForceBatch: async (_table: any, ids: any[]) => {
26
+ calls.delForceBatch.push(ids);
27
+ return ids.length;
28
+ }
29
+ } as any;
30
+
31
+ const ctx = {
32
+ db: dbHelper,
33
+ addons: [],
34
+ cache: {
35
+ cacheApis: async () => {},
36
+ rebuildRoleApiPermissions: async () => {}
37
+ }
38
+ } as any;
39
+
40
+ const apiItems = [
41
+ // 不带 type:应按 api 处理,保留 keep
42
+ {
43
+ source: "app",
44
+ sourceName: "项目",
45
+ filePath: "DUMMY",
46
+ relativePath: "keep",
47
+ fileName: "keep",
48
+ moduleName: "app_keep",
49
+ name: "Keep",
50
+ routePath: "/api/app/keep",
51
+ addonName: "",
52
+ fileBaseName: "keep.ts",
53
+ fileDir: "DUMMY",
54
+ content: { name: "Keep", handler: async () => {} }
55
+ },
56
+ // 带非 api type:应被跳过,因此 DB 中的 skip 会被当作“配置不存在”而删除
57
+ {
58
+ type: "menu",
59
+ name: "Skip",
60
+ routePath: "/api/app/skip",
61
+ addonName: ""
62
+ }
63
+ ] as any;
64
+
65
+ await syncApi(ctx, apiItems);
66
+
67
+ expect(calls.getAllArgs?.fields).toEqual(["id", "routePath", "name", "addonName", "state"]);
68
+
69
+ expect(calls.delForceBatch).toHaveLength(1);
70
+ expect(calls.delForceBatch[0]).toEqual([2]);
71
+ });
72
+ });
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { syncDev } from "../sync/syncDev.js";
4
+
5
+ describe("syncDev - dev role permissions", () => {
6
+ test("dev 角色应拥有所有菜单和接口(state>=0)", async () => {
7
+ const calls = {
8
+ getAll: [],
9
+ insData: [],
10
+ updData: [],
11
+ rebuildRoleApiPermissionsCount: 0
12
+ };
13
+
14
+ let nextId = 100;
15
+
16
+ const ctx = {
17
+ db: {
18
+ tableExists: async (table) => {
19
+ return table === "addon_admin_admin" || table === "addon_admin_role" || table === "addon_admin_menu" || table === "addon_admin_api";
20
+ },
21
+ getAll: async (options) => {
22
+ calls.getAll.push(options);
23
+
24
+ if (options?.table === "addon_admin_menu") {
25
+ return {
26
+ lists: [
27
+ { path: "/dashboard", state: 0 },
28
+ { path: "/permission/role", state: 0 }
29
+ ]
30
+ };
31
+ }
32
+ if (options?.table === "addon_admin_api") {
33
+ return {
34
+ lists: [
35
+ { routePath: "/api/health", state: 0 },
36
+ { routePath: "/api/addon/addonAdmin/auth/login", state: 0 }
37
+ ]
38
+ };
39
+ }
40
+
41
+ return { lists: [] };
42
+ },
43
+ getOne: async (_options) => {
44
+ // 让所有角色/管理员都走插入逻辑,便于断言插入数据
45
+ return null;
46
+ },
47
+ insData: async (options) => {
48
+ calls.insData.push(options);
49
+ nextId += 1;
50
+ return nextId;
51
+ },
52
+ updData: async (options) => {
53
+ calls.updData.push(options);
54
+ return 1;
55
+ }
56
+ },
57
+ cache: {
58
+ rebuildRoleApiPermissions: async () => {
59
+ calls.rebuildRoleApiPermissionsCount += 1;
60
+ }
61
+ }
62
+ };
63
+
64
+ await syncDev(ctx, { devEmail: "dev@qq.com", devPassword: "dev-password" });
65
+
66
+ // 断言读取菜单/接口时按 state>=0 查询
67
+ const menuGetAll = calls.getAll.find((c) => c?.table === "addon_admin_menu");
68
+ expect(menuGetAll?.where?.state$gte).toBe(0);
69
+
70
+ const apiGetAll = calls.getAll.find((c) => c?.table === "addon_admin_api");
71
+ expect(apiGetAll?.where?.state$gte).toBe(0);
72
+
73
+ // 断言 dev 角色写入时包含“所有路径”(按查询结果写入;统一为 pathname,不包含 method)
74
+ const devRoleInsert = calls.insData.find((c) => c?.table === "addon_admin_role" && c?.data?.code === "dev");
75
+ expect(devRoleInsert).toBeTruthy();
76
+ expect(devRoleInsert.data.menus).toEqual(["/dashboard", "/permission/role"]);
77
+ expect(devRoleInsert.data.apis).toEqual(["/api/health", "/api/addon/addonAdmin/auth/login"]);
78
+
79
+ expect(calls.rebuildRoleApiPermissionsCount).toBe(0);
80
+ });
81
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { checkMenu } from "../checks/checkMenu.js";
6
+ import { syncMenu } from "../sync/syncMenu.js";
7
+
8
+ describe("syncMenu - disableMenus hard delete", () => {
9
+ test("命中 disableMenus 的菜单应被强制删除(不分 state)", async () => {
10
+ const originalCwd = process.cwd();
11
+ const projectDir = join(originalCwd, "temp", `syncMenu-disableMenus-hard-delete-${Date.now()}-${Math.random().toString(16).slice(2)}`);
12
+ const menusJsonPath = join(projectDir, "menus.json");
13
+
14
+ // /addon/admin/403 为禁用项,即使 state=-1 也应该被硬删除
15
+ const existingMenus = [
16
+ { id: 1, path: "/addon/admin/403", parentPath: "", name: "403", sort: 1, state: -1 },
17
+ { id: 2, path: "/keep", parentPath: "", name: "Keep", sort: 2, state: 0 }
18
+ ];
19
+
20
+ const calls = {
21
+ delForceBatch: [] as Array<{ table: string; ids: number[] }>
22
+ };
23
+
24
+ const dbHelper = {
25
+ tableExists: async () => true,
26
+ trans: async (callback: any) => {
27
+ return await callback(dbHelper);
28
+ },
29
+ getAll: async (options: any) => {
30
+ // syncMenu 会调用一次“全量不带 where”,一次 state>=0 的逻辑已在内存过滤
31
+ if (options?.table === "addon_admin_menu") {
32
+ return { lists: existingMenus };
33
+ }
34
+ return { lists: [] };
35
+ },
36
+ updBatch: async () => 0,
37
+ insBatch: async () => [],
38
+ delForceBatch: async (table: string, ids: number[]) => {
39
+ calls.delForceBatch.push({ table: table, ids: ids });
40
+ return ids.length;
41
+ }
42
+ } as any;
43
+
44
+ const ctx = {
45
+ db: dbHelper,
46
+ addons: [],
47
+ config: {
48
+ disableMenus: ["**/403"]
49
+ },
50
+ cache: {
51
+ cacheMenus: async () => {}
52
+ }
53
+ } as any;
54
+
55
+ try {
56
+ mkdirSync(projectDir, { recursive: true });
57
+ process.chdir(projectDir);
58
+
59
+ // 配置中仅保留 /keep
60
+ writeFileSync(
61
+ menusJsonPath,
62
+ JSON.stringify(
63
+ [
64
+ {
65
+ name: "Keep",
66
+ path: "/keep",
67
+ sort: 2
68
+ }
69
+ ],
70
+ null,
71
+ 4
72
+ ),
73
+ { encoding: "utf8" }
74
+ );
75
+
76
+ const menus = await checkMenu(ctx.addons, { disableMenus: ctx.config.disableMenus });
77
+ await syncMenu(ctx, menus);
78
+ } finally {
79
+ process.chdir(originalCwd);
80
+ rmSync(projectDir, { recursive: true, force: true });
81
+ }
82
+
83
+ expect(calls.delForceBatch).toHaveLength(1);
84
+ expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
85
+ // 应包含禁用菜单 id=1;/keep 不应被删
86
+ expect(calls.delForceBatch[0].ids).toEqual([1]);
87
+ });
88
+ });