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.
- package/README.md +39 -8
- package/befly.config.ts +19 -2
- package/checks/checkApi.ts +79 -77
- package/checks/checkHook.ts +48 -0
- package/checks/checkMenu.ts +168 -0
- package/checks/checkPlugin.ts +48 -0
- package/checks/checkTable.ts +137 -183
- package/docs/README.md +1 -1
- package/docs/api/api.md +1 -1
- package/docs/guide/quickstart.md +16 -9
- package/docs/hooks/hook.md +2 -2
- package/docs/hooks/rateLimit.md +1 -1
- package/docs/infra/redis.md +7 -7
- package/docs/plugins/plugin.md +23 -21
- package/docs/quickstart.md +16 -9
- package/docs/reference/addon.md +12 -1
- package/docs/reference/config.md +13 -30
- package/docs/reference/sync.md +62 -193
- package/docs/reference/table.md +27 -29
- package/hooks/auth.ts +3 -4
- package/hooks/cors.ts +4 -6
- package/hooks/parser.ts +3 -4
- package/hooks/permission.ts +3 -4
- package/hooks/validator.ts +3 -4
- package/lib/cacheHelper.ts +89 -153
- package/lib/cacheKeys.ts +1 -1
- package/lib/connect.ts +9 -13
- package/lib/dbDialect.ts +285 -0
- package/lib/dbHelper.ts +179 -507
- package/lib/dbUtils.ts +450 -0
- package/lib/logger.ts +41 -5
- package/lib/redisHelper.ts +1 -0
- package/lib/sqlBuilder.ts +358 -58
- package/lib/sqlCheck.ts +136 -0
- package/lib/validator.ts +1 -1
- package/loader/loadApis.ts +23 -126
- package/loader/loadHooks.ts +31 -46
- package/loader/loadPlugins.ts +37 -52
- package/main.ts +58 -19
- package/package.json +24 -25
- package/paths.ts +14 -14
- package/plugins/cache.ts +12 -6
- package/plugins/cipher.ts +2 -2
- package/plugins/config.ts +6 -8
- package/plugins/db.ts +14 -19
- package/plugins/jwt.ts +6 -7
- package/plugins/logger.ts +7 -9
- package/plugins/redis.ts +8 -10
- package/plugins/tool.ts +3 -4
- package/router/api.ts +3 -2
- package/router/static.ts +7 -5
- package/sync/syncApi.ts +80 -235
- package/sync/syncCache.ts +16 -0
- package/sync/syncDev.ts +167 -202
- package/sync/syncMenu.ts +230 -444
- package/sync/syncTable.ts +1247 -0
- package/tests/_mocks/mockSqliteDb.ts +204 -0
- package/tests/addonHelper-cache.test.ts +32 -0
- package/tests/apiHandler-routePath-only.test.ts +32 -0
- package/tests/cacheHelper.test.ts +16 -51
- package/tests/checkApi-routePath-strict.test.ts +166 -0
- package/tests/checkMenu.test.ts +346 -0
- package/tests/checkTable-smoke.test.ts +157 -0
- package/tests/dbDialect-cache.test.ts +23 -0
- package/tests/dbDialect.test.ts +46 -0
- package/tests/dbHelper-advanced.test.ts +1 -1
- package/tests/dbHelper-all-array-types.test.ts +15 -15
- package/tests/dbHelper-batch-write.test.ts +90 -0
- package/tests/dbHelper-columns.test.ts +36 -54
- package/tests/dbHelper-execute.test.ts +26 -26
- package/tests/dbHelper-joins.test.ts +85 -176
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
- package/tests/fixtures/scanFilesApis/a.ts +3 -0
- package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
- package/tests/loadPlugins-order-smoke.test.ts +75 -0
- package/tests/logger.test.ts +6 -6
- package/tests/redisHelper.test.ts +6 -1
- package/tests/scanFiles-routePath.test.ts +46 -0
- package/tests/smoke-sql.test.ts +24 -0
- package/tests/sqlBuilder-advanced.test.ts +18 -5
- package/tests/sqlBuilder.test.ts +24 -0
- package/tests/sync-init-guard.test.ts +105 -0
- package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
- package/tests/syncApi-obsolete-records.test.ts +69 -0
- package/tests/syncApi-type-compat.test.ts +72 -0
- package/tests/syncDev-permissions.test.ts +81 -0
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
- package/tests/syncMenu-duplicate-path.test.ts +122 -0
- package/tests/syncMenu-obsolete-records.test.ts +161 -0
- package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
- package/tests/syncMenu-paths.test.ts +0 -9
- package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
- package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
- package/tests/syncTable-constants.test.ts +101 -0
- package/tests/syncTable-db-integration.test.ts +237 -0
- package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
- package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
- package/tests/syncTable-schema.test.ts +99 -0
- package/tests/syncTable-testkit.test.ts +25 -0
- package/tests/syncTable-types.test.ts +122 -0
- package/tests/tableRef-and-deserialize.test.ts +67 -0
- package/tsconfig.json +1 -1
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +13 -12
- package/types/cache.d.ts +2 -2
- package/types/context.d.ts +1 -1
- package/types/database.d.ts +0 -5
- package/types/hook.d.ts +1 -10
- package/types/plugin.d.ts +2 -96
- package/types/sync.d.ts +19 -25
- package/utils/convertBigIntFields.ts +38 -0
- package/utils/disableMenusGlob.ts +85 -0
- package/utils/importDefault.ts +21 -0
- package/utils/isDirentDirectory.ts +23 -0
- package/utils/loadMenuConfigs.ts +145 -0
- package/utils/processFields.ts +25 -0
- package/utils/scanAddons.ts +72 -0
- package/utils/scanFiles.ts +129 -21
- package/utils/scanSources.ts +64 -0
- package/utils/sortModules.ts +137 -0
- package/checks/checkApp.ts +0 -55
- package/hooks/rateLimit.ts +0 -276
- package/sync/syncAll.ts +0 -35
- package/sync/syncDb/apply.ts +0 -192
- package/sync/syncDb/constants.ts +0 -119
- package/sync/syncDb/ddl.ts +0 -251
- package/sync/syncDb/helpers.ts +0 -84
- package/sync/syncDb/schema.ts +0 -202
- package/sync/syncDb/sqlite.ts +0 -48
- package/sync/syncDb/table.ts +0 -207
- package/sync/syncDb/tableCreate.ts +0 -163
- package/sync/syncDb/types.ts +0 -132
- package/sync/syncDb/version.ts +0 -69
- package/sync/syncDb.ts +0 -168
- package/tests/rateLimit-hook.test.ts +0 -477
- package/tests/syncDb-constants.test.ts +0 -130
- package/tests/syncDb-schema.test.ts +0 -179
- package/tests/syncDb-types.test.ts +0 -139
- package/utils/addonHelper.ts +0 -90
- package/utils/modules.ts +0 -98
- 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:
|
|
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: [
|
|
256
|
-
apis: [
|
|
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("
|
|
262
|
-
expect(serialized.apis).toBe("
|
|
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(
|
|
271
|
-
expect(deserialized.apis.length).toBe(
|
|
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: [
|
|
296
|
-
{ name: "编辑", menus: [
|
|
297
|
-
{ name: "访客", menus: [
|
|
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("
|
|
304
|
-
expect(serializedRoles[1].menus).toBe("
|
|
305
|
-
expect(serializedRoles[2].menus).toBe("
|
|
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([
|
|
312
|
-
expect(deserializedRoles[1].menus).toEqual([
|
|
313
|
-
expect(deserializedRoles[2].menus).toEqual([
|
|
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
|
});
|