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
@@ -0,0 +1,346 @@
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
+
7
+ describe("checkMenu", () => {
8
+ test("重复 path 应阻断菜单同步", async () => {
9
+ const originalCwd = process.cwd();
10
+ const projectDir = join(originalCwd, "temp", `checkMenu-dup-${Date.now()}-${Math.random().toString(16).slice(2)}`);
11
+ const menusJsonPath = join(projectDir, "menus.json");
12
+
13
+ try {
14
+ mkdirSync(projectDir, { recursive: true });
15
+ process.chdir(projectDir);
16
+
17
+ writeFileSync(
18
+ menusJsonPath,
19
+ JSON.stringify(
20
+ [
21
+ { name: "A", path: "/a", sort: 1 },
22
+ { name: "B", path: "/a", sort: 2 }
23
+ ],
24
+ null,
25
+ 4
26
+ ),
27
+ { encoding: "utf8" }
28
+ );
29
+
30
+ let thrown: any = null;
31
+ try {
32
+ await checkMenu([]);
33
+ } catch (error: any) {
34
+ thrown = error;
35
+ }
36
+
37
+ expect(thrown).toBeTruthy();
38
+ expect(thrown.message).toBe("菜单结构检查失败");
39
+ } finally {
40
+ process.chdir(originalCwd);
41
+ rmSync(projectDir, { recursive: true, force: true });
42
+ }
43
+ });
44
+
45
+ test("缺失父级 path 应阻断菜单同步", async () => {
46
+ const originalCwd = process.cwd();
47
+ const projectDir = join(originalCwd, "temp", `checkMenu-parent-${Date.now()}-${Math.random().toString(16).slice(2)}`);
48
+ const menusJsonPath = join(projectDir, "menus.json");
49
+
50
+ try {
51
+ mkdirSync(projectDir, { recursive: true });
52
+ process.chdir(projectDir);
53
+
54
+ writeFileSync(menusJsonPath, JSON.stringify([{ name: "B", path: "/a/b", sort: 1 }], null, 4), { encoding: "utf8" });
55
+
56
+ // 菜单层级应以配置树(children)为准,而非按 URL path 分段强制推导父级。
57
+ // 因此单个菜单(即使 path 含多段)也允许作为根级菜单存在。
58
+ const menus = await checkMenu([]);
59
+ expect(Array.isArray(menus)).toBe(true);
60
+ expect(menus).toHaveLength(1);
61
+ expect(menus[0]?.path).toBe("/a/b");
62
+ } finally {
63
+ process.chdir(originalCwd);
64
+ rmSync(projectDir, { recursive: true, force: true });
65
+ }
66
+ });
67
+
68
+ test("disableMenus(精确)应过滤指定菜单", async () => {
69
+ const originalCwd = process.cwd();
70
+ const projectDir = join(originalCwd, "temp", `checkMenu-disableMenus-exact-${Date.now()}-${Math.random().toString(16).slice(2)}`);
71
+ const menusJsonPath = join(projectDir, "menus.json");
72
+
73
+ try {
74
+ mkdirSync(projectDir, { recursive: true });
75
+ process.chdir(projectDir);
76
+
77
+ writeFileSync(
78
+ menusJsonPath,
79
+ JSON.stringify(
80
+ [
81
+ {
82
+ name: "A",
83
+ path: "/a",
84
+ sort: 1,
85
+ children: [
86
+ {
87
+ name: "B",
88
+ path: "/a/b",
89
+ sort: 2
90
+ }
91
+ ]
92
+ },
93
+ {
94
+ name: "C",
95
+ path: "/c",
96
+ sort: 3
97
+ }
98
+ ],
99
+ null,
100
+ 4
101
+ ),
102
+ { encoding: "utf8" }
103
+ );
104
+
105
+ const menus = await checkMenu([], { disableMenus: ["/a/b"] });
106
+ expect(menus).toHaveLength(2);
107
+ expect(menus[0]?.path).toBe("/a");
108
+ expect(menus[1]?.path).toBe("/c");
109
+ expect(Array.isArray((menus[0] as any)?.children)).toBe(false);
110
+ } finally {
111
+ process.chdir(originalCwd);
112
+ rmSync(projectDir, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ test("disableMenus(glob)应按 Bun.Glob 语义过滤匹配的菜单", async () => {
117
+ const originalCwd = process.cwd();
118
+ const projectDir = join(originalCwd, "temp", `checkMenu-disableMenus-glob-${Date.now()}-${Math.random().toString(16).slice(2)}`);
119
+ const menusJsonPath = join(projectDir, "menus.json");
120
+
121
+ try {
122
+ mkdirSync(projectDir, { recursive: true });
123
+ process.chdir(projectDir);
124
+
125
+ writeFileSync(
126
+ menusJsonPath,
127
+ JSON.stringify(
128
+ [
129
+ { name: "A", path: "/a", sort: 1 },
130
+ { name: "A-1", path: "/a/1", sort: 2 },
131
+ { name: "B", path: "/b", sort: 3 }
132
+ ],
133
+ null,
134
+ 4
135
+ ),
136
+ { encoding: "utf8" }
137
+ );
138
+
139
+ // 注意:此处完全遵循 Bun.Glob 的 match 语义。
140
+ // 通常 "*" 不跨路径分隔符,因此 "/a/*" 仅匹配 "/a/1",不会匹配 "/a"。
141
+ const menus = await checkMenu([], { disableMenus: ["/a/*"] });
142
+ const paths = menus.map((m) => m.path);
143
+ expect(paths).toContain("/a");
144
+ expect(paths).toContain("/b");
145
+ expect(paths).not.toContain("/a/1");
146
+ } finally {
147
+ process.chdir(originalCwd);
148
+ rmSync(projectDir, { recursive: true, force: true });
149
+ }
150
+ });
151
+
152
+ test("disableMenus 规则不合法应阻断启动", async () => {
153
+ const originalCwd = process.cwd();
154
+ const projectDir = join(originalCwd, "temp", `checkMenu-disableMenus-invalid-${Date.now()}-${Math.random().toString(16).slice(2)}`);
155
+ const menusJsonPath = join(projectDir, "menus.json");
156
+
157
+ try {
158
+ mkdirSync(projectDir, { recursive: true });
159
+ process.chdir(projectDir);
160
+
161
+ writeFileSync(menusJsonPath, JSON.stringify([{ name: "A", path: "/a", sort: 1 }], null, 4), { encoding: "utf8" });
162
+
163
+ // 1) disableMenus 必须是数组
164
+ {
165
+ let thrown: any = null;
166
+ try {
167
+ await checkMenu([], { disableMenus: "not-array" as any });
168
+ } catch (error: any) {
169
+ thrown = error;
170
+ }
171
+ expect(thrown).toBeTruthy();
172
+ expect(String(thrown.message).includes("disableMenus")).toBe(true);
173
+ }
174
+
175
+ // 2) 数组元素必须是 string
176
+ {
177
+ let thrown: any = null;
178
+ try {
179
+ await checkMenu([], { disableMenus: [123 as any] });
180
+ } catch (error: any) {
181
+ thrown = error;
182
+ }
183
+ expect(thrown).toBeTruthy();
184
+ expect(String(thrown.message).includes("disableMenus")).toBe(true);
185
+ }
186
+
187
+ // 3) 不允许空字符串
188
+ {
189
+ let thrown: any = null;
190
+ try {
191
+ await checkMenu([], { disableMenus: [" "] });
192
+ } catch (error: any) {
193
+ thrown = error;
194
+ }
195
+ expect(thrown).toBeTruthy();
196
+ expect(String(thrown.message).includes("disableMenus")).toBe(true);
197
+ }
198
+ } finally {
199
+ process.chdir(originalCwd);
200
+ rmSync(projectDir, { recursive: true, force: true });
201
+ }
202
+ });
203
+
204
+ test("默认应屏蔽 /404 /403 /500 以及所有以 /login 结尾的菜单路由", async () => {
205
+ const originalCwd = process.cwd();
206
+ const projectDir = join(originalCwd, "temp", `checkMenu-default-disable-${Date.now()}-${Math.random().toString(16).slice(2)}`);
207
+ const menusJsonPath = join(projectDir, "menus.json");
208
+
209
+ try {
210
+ mkdirSync(projectDir, { recursive: true });
211
+ process.chdir(projectDir);
212
+
213
+ writeFileSync(
214
+ menusJsonPath,
215
+ JSON.stringify(
216
+ [
217
+ { name: "Login", path: "/login", sort: 1 },
218
+ { name: "AddonLogin", path: "/addon/admin/login", sort: 2 },
219
+ { name: "404", path: "/404", sort: 3 },
220
+ { name: "403", path: "/403", sort: 4 },
221
+ { name: "500", path: "/500", sort: 5 },
222
+ { name: "A", path: "/a", sort: 6 }
223
+ ],
224
+ null,
225
+ 4
226
+ ),
227
+ { encoding: "utf8" }
228
+ );
229
+
230
+ const menus = await checkMenu([], { disableMenus: ["**/404", "**/403", "**/500", "**/login"] });
231
+ expect(Array.isArray(menus)).toBe(true);
232
+ expect(menus).toHaveLength(1);
233
+ expect(menus[0]?.path).toBe("/a");
234
+ } finally {
235
+ process.chdir(originalCwd);
236
+ rmSync(projectDir, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ test("合法 menus.json 应通过检查", async () => {
241
+ const originalCwd = process.cwd();
242
+ const projectDir = join(originalCwd, "temp", `checkMenu-ok-${Date.now()}-${Math.random().toString(16).slice(2)}`);
243
+ const menusJsonPath = join(projectDir, "menus.json");
244
+
245
+ try {
246
+ mkdirSync(projectDir, { recursive: true });
247
+ process.chdir(projectDir);
248
+
249
+ writeFileSync(menusJsonPath, JSON.stringify([{ name: "A", path: "/a", sort: 1, children: [{ name: "B", path: "/a/b", sort: 2 }] }], null, 4), { encoding: "utf8" });
250
+
251
+ const menus = await checkMenu([]);
252
+ expect(Array.isArray(menus)).toBe(true);
253
+ expect(menus.length).toBe(1);
254
+ } finally {
255
+ process.chdir(originalCwd);
256
+ rmSync(projectDir, { recursive: true, force: true });
257
+ }
258
+ });
259
+
260
+ test("超过三级菜单应阻断同步", async () => {
261
+ const originalCwd = process.cwd();
262
+ const projectDir = join(originalCwd, "temp", `checkMenu-depth-${Date.now()}-${Math.random().toString(16).slice(2)}`);
263
+ const menusJsonPath = join(projectDir, "menus.json");
264
+
265
+ try {
266
+ mkdirSync(projectDir, { recursive: true });
267
+ process.chdir(projectDir);
268
+
269
+ writeFileSync(
270
+ menusJsonPath,
271
+ JSON.stringify(
272
+ [
273
+ {
274
+ name: "A",
275
+ path: "/a",
276
+ sort: 1,
277
+ children: [
278
+ {
279
+ name: "B",
280
+ path: "/a/b",
281
+ sort: 2,
282
+ children: [
283
+ {
284
+ name: "C",
285
+ path: "/a/b/c",
286
+ sort: 3,
287
+ children: [
288
+ {
289
+ name: "D",
290
+ path: "/a/b/c/d",
291
+ sort: 4
292
+ }
293
+ ]
294
+ }
295
+ ]
296
+ }
297
+ ]
298
+ }
299
+ ],
300
+ null,
301
+ 4
302
+ ),
303
+ { encoding: "utf8" }
304
+ );
305
+
306
+ let thrown: any = null;
307
+ try {
308
+ await checkMenu([]);
309
+ } catch (error: any) {
310
+ thrown = error;
311
+ }
312
+
313
+ expect(thrown).toBeTruthy();
314
+ expect(thrown.message).toBe("菜单结构检查失败");
315
+ } finally {
316
+ process.chdir(originalCwd);
317
+ rmSync(projectDir, { recursive: true, force: true });
318
+ }
319
+ });
320
+
321
+ test("sort 最小值应为 1(sort=0 应阻断启动)", async () => {
322
+ const originalCwd = process.cwd();
323
+ const projectDir = join(originalCwd, "temp", `checkMenu-sort-min-${Date.now()}-${Math.random().toString(16).slice(2)}`);
324
+ const menusJsonPath = join(projectDir, "menus.json");
325
+
326
+ try {
327
+ mkdirSync(projectDir, { recursive: true });
328
+ process.chdir(projectDir);
329
+
330
+ writeFileSync(menusJsonPath, JSON.stringify([{ name: "A", path: "/a", sort: 0 }], null, 4), { encoding: "utf8" });
331
+
332
+ let thrown: any = null;
333
+ try {
334
+ await checkMenu([]);
335
+ } catch (error: any) {
336
+ thrown = error;
337
+ }
338
+
339
+ expect(thrown).toBeTruthy();
340
+ expect(thrown.message).toBe("菜单结构检查失败");
341
+ } finally {
342
+ process.chdir(originalCwd);
343
+ rmSync(projectDir, { recursive: true, force: true });
344
+ }
345
+ });
346
+ });
@@ -0,0 +1,157 @@
1
+ import type { ScanFileResult } from "../utils/scanFiles.js";
2
+
3
+ import { describe, expect, test } from "bun:test";
4
+
5
+ import { checkTable } from "../checks/checkTable.js";
6
+ import { Logger } from "../lib/logger.js";
7
+
8
+ describe("checkTable - smoke", () => {
9
+ test("应忽略非 table 项;合法表定义不应抛错", async () => {
10
+ const items: ScanFileResult[] = [
11
+ {
12
+ type: "api",
13
+ source: "app",
14
+ sourceName: "项目",
15
+ filePath: "DUMMY",
16
+ relativePath: "DUMMY",
17
+ fileName: "dummy",
18
+ moduleName: "app_dummy",
19
+ addonName: "",
20
+ content: {}
21
+ } as any,
22
+ {
23
+ type: "table",
24
+ source: "app",
25
+ sourceName: "项目",
26
+ filePath: "DUMMY",
27
+ relativePath: "testCustomers",
28
+ fileName: "testCustomers",
29
+ moduleName: "app_testCustomers",
30
+ addonName: "",
31
+ content: {
32
+ customerName: { name: "客户名", type: "string", max: 32 }
33
+ }
34
+ } as any
35
+ ];
36
+
37
+ await checkTable(items);
38
+ expect(true).toBe(true);
39
+ });
40
+
41
+ test("unique 和 index 同时为 true 时应阻断启动(抛错)", async () => {
42
+ const items: ScanFileResult[] = [
43
+ {
44
+ type: "table",
45
+ source: "app",
46
+ sourceName: "项目",
47
+ filePath: "DUMMY",
48
+ relativePath: "testMenu",
49
+ fileName: "testMenu",
50
+ moduleName: "app_testMenu",
51
+ addonName: "",
52
+ content: {
53
+ path: { name: "路径", type: "string", max: 128, unique: true, index: true }
54
+ }
55
+ } as any
56
+ ];
57
+
58
+ let thrownError: any = null;
59
+ try {
60
+ await checkTable(items);
61
+ } catch (error: any) {
62
+ thrownError = error;
63
+ }
64
+
65
+ expect(Boolean(thrownError)).toBe(true);
66
+ expect(String(thrownError?.message || "")).toContain("表结构检查失败");
67
+ });
68
+
69
+ test("sourceName 缺失时:日志不应出现 undefined表(允许前缀为空)", async () => {
70
+ const calls: Array<{ level: string; args: unknown[] }> = [];
71
+ const mockLogger = {
72
+ info(...args: unknown[]) {
73
+ calls.push({ level: "info", args: args });
74
+ },
75
+ warn(...args: unknown[]) {
76
+ calls.push({ level: "warn", args: args });
77
+ },
78
+ error(...args: unknown[]) {
79
+ calls.push({ level: "error", args: args });
80
+ },
81
+ debug(...args: unknown[]) {
82
+ calls.push({ level: "debug", args: args });
83
+ }
84
+ } as any;
85
+
86
+ Logger.setMock(mockLogger);
87
+
88
+ try {
89
+ await checkTable([
90
+ {
91
+ type: "table",
92
+ source: "app",
93
+ filePath: "DUMMY",
94
+ relativePath: "TestCustomers",
95
+ fileName: "TestCustomers",
96
+ moduleName: "app_TestCustomers",
97
+ addonName: "",
98
+ content: {}
99
+ } as any
100
+ ]);
101
+ } catch {
102
+ // 触发 hasError 后会抛错:这里只验证日志前缀
103
+ } finally {
104
+ Logger.setMock(null);
105
+ }
106
+
107
+ const warnMessages = calls.filter((item) => item.level === "warn").map((item) => String(item.args[0]));
108
+
109
+ expect(warnMessages.some((msg) => msg.includes("表 TestCustomers"))).toBe(true);
110
+ expect(warnMessages.some((msg) => msg.includes("undefined表"))).toBe(false);
111
+ });
112
+
113
+ test("sourceName 非字符串时:日志不应出现 undefined表(允许前缀为空)", async () => {
114
+ const calls: Array<{ level: string; args: unknown[] }> = [];
115
+ const mockLogger = {
116
+ info(...args: unknown[]) {
117
+ calls.push({ level: "info", args: args });
118
+ },
119
+ warn(...args: unknown[]) {
120
+ calls.push({ level: "warn", args: args });
121
+ },
122
+ error(...args: unknown[]) {
123
+ calls.push({ level: "error", args: args });
124
+ },
125
+ debug(...args: unknown[]) {
126
+ calls.push({ level: "debug", args: args });
127
+ }
128
+ } as any;
129
+
130
+ Logger.setMock(mockLogger);
131
+
132
+ try {
133
+ await checkTable([
134
+ {
135
+ type: "table",
136
+ source: "app",
137
+ sourceName: 123,
138
+ filePath: "DUMMY",
139
+ relativePath: "TestCustomers",
140
+ fileName: "TestCustomers",
141
+ moduleName: "app_TestCustomers",
142
+ addonName: "",
143
+ content: {}
144
+ } as any
145
+ ]);
146
+ } catch {
147
+ // 触发 hasError 后会抛错:这里只验证日志前缀
148
+ } finally {
149
+ Logger.setMock(null);
150
+ }
151
+
152
+ const warnMessages = calls.filter((item) => item.level === "warn").map((item) => String(item.args[0]));
153
+
154
+ expect(warnMessages.some((msg) => msg.includes("表 TestCustomers"))).toBe(true);
155
+ expect(warnMessages.some((msg) => msg.includes("undefined表"))).toBe(false);
156
+ });
157
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { getDialectByName } from "../lib/dbDialect.js";
4
+
5
+ describe("dbDialect - getDialectByName cache", () => {
6
+ it("should return stable singleton instances", () => {
7
+ const mysql1 = getDialectByName("mysql");
8
+ const mysql2 = getDialectByName("mysql");
9
+ expect(mysql1).toBe(mysql2);
10
+
11
+ const pg1 = getDialectByName("postgresql");
12
+ const pg2 = getDialectByName("postgresql");
13
+ expect(pg1).toBe(pg2);
14
+
15
+ const sqlite1 = getDialectByName("sqlite");
16
+ const sqlite2 = getDialectByName("sqlite");
17
+ expect(sqlite1).toBe(sqlite2);
18
+
19
+ expect(mysql1).not.toBe(pg1);
20
+ expect(mysql1).not.toBe(sqlite1);
21
+ expect(pg1).not.toBe(sqlite1);
22
+ });
23
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { MySqlDialect, PostgresDialect, SqliteDialect } from "../lib/dbDialect.js";
4
+
5
+ describe("DbDialect - quoteIdent", () => {
6
+ it("MySqlDialect: 使用反引号", () => {
7
+ const dialect = new MySqlDialect();
8
+ expect(dialect.quoteIdent("users")).toBe("`users`");
9
+ expect(dialect.quoteIdent("created_at")).toBe("`created_at`");
10
+ });
11
+
12
+ it("PostgresDialect: 使用双引号", () => {
13
+ const dialect = new PostgresDialect();
14
+ expect(dialect.quoteIdent("users")).toBe('"users"');
15
+ expect(dialect.quoteIdent("created_at")).toBe('"created_at"');
16
+ });
17
+
18
+ it("SqliteDialect: 使用双引号", () => {
19
+ const dialect = new SqliteDialect();
20
+ expect(dialect.quoteIdent("users")).toBe('"users"');
21
+ expect(dialect.quoteIdent("created_at")).toBe('"created_at"');
22
+ });
23
+ });
24
+
25
+ describe("DbDialect - getTableColumnsQuery/tableExistsQuery", () => {
26
+ it("PostgresDialect: columns 查询应带参数", () => {
27
+ const dialect = new PostgresDialect();
28
+ const query = dialect.getTableColumnsQuery("users");
29
+ expect(query.sql).toContain("information_schema.columns");
30
+ expect(query.params).toEqual(["users"]);
31
+ });
32
+
33
+ it('SqliteDialect: columns 查询应为 PRAGMA table_info("table")', () => {
34
+ const dialect = new SqliteDialect();
35
+ const query = dialect.getTableColumnsQuery("users");
36
+ expect(query.sql).toBe('PRAGMA table_info("users")');
37
+ expect(query.params).toEqual([]);
38
+ });
39
+
40
+ it("SqliteDialect: tableExistsQuery 应查 sqlite_master", () => {
41
+ const dialect = new SqliteDialect();
42
+ const query = dialect.tableExistsQuery("users");
43
+ expect(query.sql).toContain("sqlite_master");
44
+ expect(query.params).toEqual(["users"]);
45
+ });
46
+ });
@@ -666,7 +666,7 @@ describe("DbHelper - 代码逻辑问题分析", () => {
666
666
  // **建议修复**:
667
667
  // 添加查询超时机制
668
668
 
669
- const mockExecuteWithTimeout = async (sql: string, params: any[], timeout: number = 30000) => {
669
+ const mockExecuteWithTimeout = async (sql: string, params: unknown[], timeout: number = 30000) => {
670
670
  const timeoutPromise = new Promise((_, reject) => {
671
671
  setTimeout(() => reject(new Error("查询超时")), timeout);
672
672
  });
@@ -252,14 +252,14 @@ describe("所有数组类型的序列化/反序列化支持", () => {
252
252
  id: 1,
253
253
  name: "管理员",
254
254
  code: "admin",
255
- menus: [1, 2, 3, 10, 11, 12, 20, 21], // array_number_string
256
- apis: [100, 101, 102, 200, 201, 202] // array_number_string
255
+ menus: ["/dashboard", "/permission/role"], // array_text
256
+ apis: ["/api/login", "/api/user/list"] // array_text
257
257
  };
258
258
 
259
259
  // 模拟写入数据库
260
260
  const serialized = serializeArrayFields(roleData);
261
- expect(serialized.menus).toBe("[1,2,3,10,11,12,20,21]");
262
- expect(serialized.apis).toBe("[100,101,102,200,201,202]");
261
+ expect(serialized.menus).toBe('["/dashboard","/permission/role"]');
262
+ expect(serialized.apis).toBe('["/api/login","/api/user/list"]');
263
263
 
264
264
  // 模拟从数据库查询
265
265
  const deserialized = deserializeArrayFields(serialized);
@@ -267,8 +267,8 @@ describe("所有数组类型的序列化/反序列化支持", () => {
267
267
  expect(deserialized.apis).toEqual(roleData.apis);
268
268
 
269
269
  // 验证可以直接使用数组方法
270
- expect(deserialized.menus.includes(1)).toBe(true);
271
- expect(deserialized.apis.length).toBe(6);
270
+ expect(deserialized.menus.includes("/dashboard")).toBe(true);
271
+ expect(deserialized.apis.length).toBe(2);
272
272
  });
273
273
 
274
274
  test("实际场景: 文章标签和分类", () => {
@@ -292,25 +292,25 @@ describe("所有数组类型的序列化/反序列化支持", () => {
292
292
 
293
293
  test("完整流程: 批量插入场景", () => {
294
294
  const roles = [
295
- { name: "管理员", menus: [1, 2, 3], apis: [10, 20] },
296
- { name: "编辑", menus: [2, 3], apis: [20] },
297
- { name: "访客", menus: [3], apis: [] }
295
+ { name: "管理员", menus: ["/dashboard", "/permission/role"], apis: ["/api/login", "/api/user/list"] },
296
+ { name: "编辑", menus: ["/dashboard"], apis: ["/api/user/list"] },
297
+ { name: "访客", menus: ["/dashboard"], apis: [] }
298
298
  ];
299
299
 
300
300
  // 模拟批量序列化
301
301
  const serializedRoles = roles.map((role) => serializeArrayFields(role));
302
302
 
303
- expect(serializedRoles[0].menus).toBe("[1,2,3]");
304
- expect(serializedRoles[1].menus).toBe("[2,3]");
305
- expect(serializedRoles[2].menus).toBe("[3]");
303
+ expect(serializedRoles[0].menus).toBe('["/dashboard","/permission/role"]');
304
+ expect(serializedRoles[1].menus).toBe('["/dashboard"]');
305
+ expect(serializedRoles[2].menus).toBe('["/dashboard"]');
306
306
  expect(serializedRoles[2].apis).toBe("[]");
307
307
 
308
308
  // 模拟批量反序列化
309
309
  const deserializedRoles = serializedRoles.map((role) => deserializeArrayFields(role));
310
310
 
311
- expect(deserializedRoles[0].menus).toEqual([1, 2, 3]);
312
- expect(deserializedRoles[1].menus).toEqual([2, 3]);
313
- expect(deserializedRoles[2].menus).toEqual([3]);
311
+ expect(deserializedRoles[0].menus).toEqual(["/dashboard", "/permission/role"]);
312
+ expect(deserializedRoles[1].menus).toEqual(["/dashboard"]);
313
+ expect(deserializedRoles[2].menus).toEqual(["/dashboard"]);
314
314
  expect(deserializedRoles[2].apis).toEqual([]);
315
315
  });
316
316
  });