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,122 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach, mock } 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 { setMockLogger } from "../lib/logger.js";
|
|
7
|
+
import { syncMenu } from "../sync/syncMenu.js";
|
|
8
|
+
|
|
9
|
+
// Mock pino logger
|
|
10
|
+
const mockPino = {
|
|
11
|
+
info: mock(() => {}),
|
|
12
|
+
warn: mock(() => {}),
|
|
13
|
+
error: mock(() => {}),
|
|
14
|
+
debug: mock(() => {}),
|
|
15
|
+
fatal: mock(() => {}),
|
|
16
|
+
trace: mock(() => {}),
|
|
17
|
+
silent: mock(() => {}),
|
|
18
|
+
child: mock(() => mockPino),
|
|
19
|
+
level: "info"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("syncMenu - duplicate path records", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
setMockLogger(mockPino as any);
|
|
25
|
+
mockPino.warn.mockClear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
setMockLogger(null);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("应检测重复 path 并删除多余记录(保留 id 最大的一条)", async () => {
|
|
33
|
+
const originalCwd = process.cwd();
|
|
34
|
+
const projectDir = join(originalCwd, "temp", `syncMenu-duplicate-path-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
35
|
+
const menusJsonPath = join(projectDir, "menus.json");
|
|
36
|
+
|
|
37
|
+
const existingMenus = [
|
|
38
|
+
{ id: 10, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 },
|
|
39
|
+
{ id: 20, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 }
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const calls = {
|
|
43
|
+
delForceBatch: [] as any[],
|
|
44
|
+
updBatchCount: 0,
|
|
45
|
+
insBatchCount: 0
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const dbHelper = {
|
|
49
|
+
tableExists: async () => true,
|
|
50
|
+
trans: async (callback: any) => {
|
|
51
|
+
return await callback(dbHelper);
|
|
52
|
+
},
|
|
53
|
+
getAll: async (options: any) => {
|
|
54
|
+
const stateGte = options?.where?.state$gte;
|
|
55
|
+
if (typeof stateGte === "number") {
|
|
56
|
+
return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
|
|
57
|
+
}
|
|
58
|
+
return { lists: existingMenus };
|
|
59
|
+
},
|
|
60
|
+
updBatch: async () => {
|
|
61
|
+
calls.updBatchCount += 1;
|
|
62
|
+
return 0;
|
|
63
|
+
},
|
|
64
|
+
insBatch: async () => {
|
|
65
|
+
calls.insBatchCount += 1;
|
|
66
|
+
return [];
|
|
67
|
+
},
|
|
68
|
+
delForceBatch: async (table: string, ids: number[]) => {
|
|
69
|
+
calls.delForceBatch.push({ table: table, ids: ids });
|
|
70
|
+
return ids.length;
|
|
71
|
+
}
|
|
72
|
+
} as any;
|
|
73
|
+
|
|
74
|
+
const ctx = {
|
|
75
|
+
db: dbHelper,
|
|
76
|
+
addons: [],
|
|
77
|
+
config: {
|
|
78
|
+
disableMenus: []
|
|
79
|
+
},
|
|
80
|
+
cache: {
|
|
81
|
+
cacheMenus: async () => {}
|
|
82
|
+
}
|
|
83
|
+
} as any;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(projectDir, { recursive: true });
|
|
87
|
+
process.chdir(projectDir);
|
|
88
|
+
|
|
89
|
+
writeFileSync(
|
|
90
|
+
menusJsonPath,
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
name: "Keep",
|
|
95
|
+
path: "/keep",
|
|
96
|
+
sort: 999
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
null,
|
|
100
|
+
4
|
|
101
|
+
),
|
|
102
|
+
{ encoding: "utf8" }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const menus = await checkMenu(ctx.addons);
|
|
106
|
+
await syncMenu(ctx, menus);
|
|
107
|
+
} finally {
|
|
108
|
+
process.chdir(originalCwd);
|
|
109
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
expect(mockPino.warn).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(calls.updBatchCount).toBe(0);
|
|
114
|
+
expect(calls.insBatchCount).toBe(0);
|
|
115
|
+
|
|
116
|
+
expect(calls.delForceBatch).toHaveLength(1);
|
|
117
|
+
expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
|
|
118
|
+
|
|
119
|
+
// 只应删除较小 id 的重复记录,保留 id=20
|
|
120
|
+
expect(calls.delForceBatch[0].ids).toEqual([10]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
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 - delete obsolete records", () => {
|
|
9
|
+
test("应删除不在配置中的菜单记录(仅 state>=0)", async () => {
|
|
10
|
+
const originalCwd = process.cwd();
|
|
11
|
+
const projectDir = join(originalCwd, "temp", `syncMenu-obsolete-records-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
12
|
+
const menusJsonPath = join(projectDir, "menus.json");
|
|
13
|
+
|
|
14
|
+
const existingMenus = [
|
|
15
|
+
{ id: 1, path: "/a", parentPath: "", state: 0 },
|
|
16
|
+
{ id: 2, path: "/b", parentPath: "", state: -1 },
|
|
17
|
+
{ id: 3, path: "", parentPath: "", state: 0 }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const calls = {
|
|
21
|
+
getAllCount: 0,
|
|
22
|
+
delForceBatch: [] as any[]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const dbHelper = {
|
|
26
|
+
tableExists: async () => true,
|
|
27
|
+
trans: async (callback: any) => {
|
|
28
|
+
return await callback(dbHelper);
|
|
29
|
+
},
|
|
30
|
+
getAll: async (options: any) => {
|
|
31
|
+
calls.getAllCount += 1;
|
|
32
|
+
const stateGte = options?.where?.state$gte;
|
|
33
|
+
if (typeof stateGte === "number") {
|
|
34
|
+
return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
|
|
35
|
+
}
|
|
36
|
+
return { lists: existingMenus };
|
|
37
|
+
},
|
|
38
|
+
updBatch: async () => {
|
|
39
|
+
return 0;
|
|
40
|
+
},
|
|
41
|
+
insBatch: async () => {
|
|
42
|
+
return [];
|
|
43
|
+
},
|
|
44
|
+
delForceBatch: async (table: string, ids: number[]) => {
|
|
45
|
+
calls.delForceBatch.push({ table: table, ids: ids });
|
|
46
|
+
return ids.length;
|
|
47
|
+
}
|
|
48
|
+
} as any;
|
|
49
|
+
|
|
50
|
+
const ctx = {
|
|
51
|
+
db: dbHelper,
|
|
52
|
+
addons: [],
|
|
53
|
+
config: {
|
|
54
|
+
disableMenus: []
|
|
55
|
+
},
|
|
56
|
+
cache: {
|
|
57
|
+
cacheMenus: async () => {}
|
|
58
|
+
}
|
|
59
|
+
} as any;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(projectDir, { recursive: true });
|
|
63
|
+
process.chdir(projectDir);
|
|
64
|
+
|
|
65
|
+
writeFileSync(menusJsonPath, "[]", { encoding: "utf8" });
|
|
66
|
+
const menus = await checkMenu(ctx.addons);
|
|
67
|
+
await syncMenu(ctx, menus);
|
|
68
|
+
} finally {
|
|
69
|
+
process.chdir(originalCwd);
|
|
70
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect(calls.getAllCount).toBe(1);
|
|
74
|
+
expect(calls.delForceBatch).toHaveLength(1);
|
|
75
|
+
expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
|
|
76
|
+
expect(calls.delForceBatch[0].ids).toEqual([1]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("不应删除仍在配置中的菜单记录", async () => {
|
|
80
|
+
const originalCwd = process.cwd();
|
|
81
|
+
const projectDir = join(originalCwd, "temp", `syncMenu-obsolete-records-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
82
|
+
const menusJsonPath = join(projectDir, "menus.json");
|
|
83
|
+
|
|
84
|
+
const existingMenus = [
|
|
85
|
+
{ id: 1, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 },
|
|
86
|
+
{ id: 2, path: "/remove", parentPath: "", name: "Remove", sort: 999, state: 0 }
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const calls = {
|
|
90
|
+
delForceBatch: [] as any[],
|
|
91
|
+
updBatchCount: 0
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const dbHelper = {
|
|
95
|
+
tableExists: async () => true,
|
|
96
|
+
trans: async (callback: any) => {
|
|
97
|
+
return await callback(dbHelper);
|
|
98
|
+
},
|
|
99
|
+
getAll: async (options: any) => {
|
|
100
|
+
const stateGte = options?.where?.state$gte;
|
|
101
|
+
if (typeof stateGte === "number") {
|
|
102
|
+
return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
|
|
103
|
+
}
|
|
104
|
+
return { lists: existingMenus };
|
|
105
|
+
},
|
|
106
|
+
updBatch: async () => {
|
|
107
|
+
calls.updBatchCount += 1;
|
|
108
|
+
return 0;
|
|
109
|
+
},
|
|
110
|
+
insBatch: async () => {
|
|
111
|
+
return [];
|
|
112
|
+
},
|
|
113
|
+
delForceBatch: async (table: string, ids: number[]) => {
|
|
114
|
+
calls.delForceBatch.push({ table: table, ids: ids });
|
|
115
|
+
return ids.length;
|
|
116
|
+
}
|
|
117
|
+
} as any;
|
|
118
|
+
|
|
119
|
+
const ctx = {
|
|
120
|
+
db: dbHelper,
|
|
121
|
+
addons: [],
|
|
122
|
+
config: {
|
|
123
|
+
disableMenus: []
|
|
124
|
+
},
|
|
125
|
+
cache: {
|
|
126
|
+
cacheMenus: async () => {}
|
|
127
|
+
}
|
|
128
|
+
} as any;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
mkdirSync(projectDir, { recursive: true });
|
|
132
|
+
process.chdir(projectDir);
|
|
133
|
+
|
|
134
|
+
writeFileSync(
|
|
135
|
+
menusJsonPath,
|
|
136
|
+
JSON.stringify(
|
|
137
|
+
[
|
|
138
|
+
{
|
|
139
|
+
name: "Keep",
|
|
140
|
+
path: "/keep",
|
|
141
|
+
sort: 999
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
null,
|
|
145
|
+
4
|
|
146
|
+
),
|
|
147
|
+
{ encoding: "utf8" }
|
|
148
|
+
);
|
|
149
|
+
const menus = await checkMenu(ctx.addons);
|
|
150
|
+
await syncMenu(ctx, menus);
|
|
151
|
+
} finally {
|
|
152
|
+
process.chdir(originalCwd);
|
|
153
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
expect(calls.delForceBatch).toHaveLength(1);
|
|
157
|
+
expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
|
|
158
|
+
expect(calls.delForceBatch[0].ids).toEqual([2]);
|
|
159
|
+
expect(calls.updBatchCount).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { __test__ } from "../sync/syncMenu.js";
|
|
4
|
+
|
|
5
|
+
describe("syncMenu - parentPath derived from tree", () => {
|
|
6
|
+
test("根级菜单不应强制按 URL path 推导 parentPath(避免把同级菜单挂到首页下面)", () => {
|
|
7
|
+
const mergedMenus: any[] = [
|
|
8
|
+
{
|
|
9
|
+
name: "首页",
|
|
10
|
+
path: "/addon/admin",
|
|
11
|
+
sort: 1
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "日志管理",
|
|
15
|
+
path: "/addon/admin/log",
|
|
16
|
+
sort: 40
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "配置管理",
|
|
20
|
+
path: "/addon/admin/config",
|
|
21
|
+
sort: 30
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const map = __test__.flattenMenusToDefMap(mergedMenus as any);
|
|
26
|
+
|
|
27
|
+
expect(map.get("/addon/admin")?.parentPath).toBe("");
|
|
28
|
+
expect(map.get("/addon/admin/log")?.parentPath).toBe("");
|
|
29
|
+
expect(map.get("/addon/admin/config")?.parentPath).toBe("");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("子菜单应跟随 children 嵌套关系设置 parentPath", () => {
|
|
33
|
+
const mergedMenus: any[] = [
|
|
34
|
+
{
|
|
35
|
+
name: "日志管理",
|
|
36
|
+
path: "/addon/admin/log",
|
|
37
|
+
sort: 40,
|
|
38
|
+
children: [
|
|
39
|
+
{
|
|
40
|
+
name: "登录日志",
|
|
41
|
+
path: "/addon/admin/log/login",
|
|
42
|
+
sort: 1
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const map = __test__.flattenMenusToDefMap(mergedMenus as any);
|
|
49
|
+
|
|
50
|
+
expect(map.get("/addon/admin/log")?.parentPath).toBe("");
|
|
51
|
+
expect(map.get("/addon/admin/log/login")?.parentPath).toBe("/addon/admin/log");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("显式 parentPath(包括空字符串)优先生效", () => {
|
|
55
|
+
const mergedMenus: any[] = [
|
|
56
|
+
{
|
|
57
|
+
name: "自定义根",
|
|
58
|
+
path: "/x",
|
|
59
|
+
parentPath: "/custom",
|
|
60
|
+
sort: 1
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "显式根",
|
|
64
|
+
path: "/y",
|
|
65
|
+
parentPath: "",
|
|
66
|
+
sort: 2
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const map = __test__.flattenMenusToDefMap(mergedMenus as any);
|
|
71
|
+
|
|
72
|
+
expect(map.get("/x")?.parentPath).toBe("/custom");
|
|
73
|
+
expect(map.get("/y")?.parentPath).toBe("");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -57,12 +57,3 @@ describe("syncMenu - scanViewsDir paths", () => {
|
|
|
57
57
|
expect(user?.sort).toBe(1);
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
-
|
|
61
|
-
describe("syncMenu - normalizeMenuPath", () => {
|
|
62
|
-
test("去掉尾随 / 且折叠多 /", () => {
|
|
63
|
-
expect(__test__.normalizeMenuPath("/addon/a/")).toBe("/addon/a");
|
|
64
|
-
expect(__test__.normalizeMenuPath("//addon//a//b/")).toBe("/addon/a/b");
|
|
65
|
-
expect(__test__.normalizeMenuPath("addon/a")).toBe("/addon/a");
|
|
66
|
-
expect(__test__.normalizeMenuPath("/")).toBe("/");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* syncTable 变更应用模块测试
|
|
3
3
|
*
|
|
4
4
|
* 测试 apply.ts 中的函数:
|
|
5
5
|
* - compareFieldDefinition
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { describe, test, expect
|
|
8
|
+
import { describe, test, expect } from "bun:test";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
// 设置数据库类型为 MySQL
|
|
13
|
-
setDbType("mysql");
|
|
14
|
-
|
|
15
|
-
let compareFieldDefinition: any;
|
|
16
|
-
|
|
17
|
-
beforeAll(async () => {
|
|
18
|
-
const apply = await import("../sync/syncDb/apply.js");
|
|
19
|
-
compareFieldDefinition = apply.compareFieldDefinition;
|
|
20
|
-
});
|
|
10
|
+
import { syncTable } from "../sync/syncTable.js";
|
|
21
11
|
|
|
22
12
|
describe("compareFieldDefinition", () => {
|
|
23
13
|
describe("长度变化检测", () => {
|
|
@@ -37,7 +27,7 @@ describe("compareFieldDefinition", () => {
|
|
|
37
27
|
default: null
|
|
38
28
|
};
|
|
39
29
|
|
|
40
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
30
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
41
31
|
const lengthChange = changes.find((c: any) => c.type === "length");
|
|
42
32
|
|
|
43
33
|
expect(lengthChange).toBeDefined();
|
|
@@ -61,7 +51,7 @@ describe("compareFieldDefinition", () => {
|
|
|
61
51
|
default: null
|
|
62
52
|
};
|
|
63
53
|
|
|
64
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
54
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
65
55
|
const lengthChange = changes.find((c: any) => c.type === "length");
|
|
66
56
|
|
|
67
57
|
expect(lengthChange).toBeUndefined();
|
|
@@ -85,7 +75,7 @@ describe("compareFieldDefinition", () => {
|
|
|
85
75
|
default: null
|
|
86
76
|
};
|
|
87
77
|
|
|
88
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
78
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
89
79
|
const commentChange = changes.find((c: any) => c.type === "comment");
|
|
90
80
|
|
|
91
81
|
expect(commentChange).toBeDefined();
|
|
@@ -109,7 +99,7 @@ describe("compareFieldDefinition", () => {
|
|
|
109
99
|
default: null
|
|
110
100
|
};
|
|
111
101
|
|
|
112
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
102
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
113
103
|
const commentChange = changes.find((c: any) => c.type === "comment");
|
|
114
104
|
|
|
115
105
|
expect(commentChange).toBeUndefined();
|
|
@@ -133,7 +123,7 @@ describe("compareFieldDefinition", () => {
|
|
|
133
123
|
default: null
|
|
134
124
|
};
|
|
135
125
|
|
|
136
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
126
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
137
127
|
const typeChange = changes.find((c: any) => c.type === "datatype");
|
|
138
128
|
|
|
139
129
|
expect(typeChange).toBeDefined();
|
|
@@ -157,7 +147,7 @@ describe("compareFieldDefinition", () => {
|
|
|
157
147
|
default: 0
|
|
158
148
|
};
|
|
159
149
|
|
|
160
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
150
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
161
151
|
const typeChange = changes.find((c: any) => c.type === "datatype");
|
|
162
152
|
|
|
163
153
|
expect(typeChange).toBeUndefined();
|
|
@@ -181,7 +171,7 @@ describe("compareFieldDefinition", () => {
|
|
|
181
171
|
default: null
|
|
182
172
|
};
|
|
183
173
|
|
|
184
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
174
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
185
175
|
const nullableChange = changes.find((c: any) => c.type === "nullable");
|
|
186
176
|
|
|
187
177
|
expect(nullableChange).toBeDefined();
|
|
@@ -207,7 +197,7 @@ describe("compareFieldDefinition", () => {
|
|
|
207
197
|
default: "new"
|
|
208
198
|
};
|
|
209
199
|
|
|
210
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
200
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
211
201
|
const defaultChange = changes.find((c: any) => c.type === "default");
|
|
212
202
|
|
|
213
203
|
expect(defaultChange).toBeDefined();
|
|
@@ -231,7 +221,7 @@ describe("compareFieldDefinition", () => {
|
|
|
231
221
|
default: null // null 会被解析为空字符串
|
|
232
222
|
};
|
|
233
223
|
|
|
234
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
224
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
235
225
|
const defaultChange = changes.find((c: any) => c.type === "default");
|
|
236
226
|
|
|
237
227
|
// null -> '' (空字符串),与现有值相同,无变化
|
|
@@ -256,7 +246,7 @@ describe("compareFieldDefinition", () => {
|
|
|
256
246
|
default: "new"
|
|
257
247
|
};
|
|
258
248
|
|
|
259
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
249
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
260
250
|
|
|
261
251
|
expect(changes.length).toBe(4); // length, comment, nullable, default
|
|
262
252
|
expect(changes.some((c: any) => c.type === "length")).toBe(true);
|
|
@@ -281,7 +271,7 @@ describe("compareFieldDefinition", () => {
|
|
|
281
271
|
default: null
|
|
282
272
|
};
|
|
283
273
|
|
|
284
|
-
const changes = compareFieldDefinition(existingColumn, fieldDef);
|
|
274
|
+
const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
|
|
285
275
|
|
|
286
276
|
expect(changes.length).toBe(0);
|
|
287
277
|
});
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 测试
|
|
2
|
+
* 测试 syncTable 对 array_number_string 和 array_number_text 类型的支持
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { syncTable } from "../sync/syncTable.js";
|
|
8
8
|
|
|
9
|
-
describe("
|
|
9
|
+
describe("syncTable - array_number 类型支持", () => {
|
|
10
10
|
// ==================== 类型判断测试 ====================
|
|
11
11
|
|
|
12
12
|
test("isStringOrArrayType: array_number_string 需要长度", () => {
|
|
13
|
-
expect(isStringOrArrayType("array_number_string")).toBe(true);
|
|
13
|
+
expect(syncTable.TestKit.isStringOrArrayType("array_number_string")).toBe(true);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
test("isStringOrArrayType: array_number_text 不需要长度", () => {
|
|
17
|
-
expect(isStringOrArrayType("array_number_text")).toBe(false);
|
|
17
|
+
expect(syncTable.TestKit.isStringOrArrayType("array_number_text")).toBe(false);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
// ==================== SQL 类型映射测试 ====================
|
|
21
21
|
|
|
22
22
|
test("getSqlType: array_number_string 生成 VARCHAR(max)", () => {
|
|
23
|
-
const sqlType = getSqlType("array_number_string", 500);
|
|
23
|
+
const sqlType = syncTable.TestKit.getSqlType("mysql", "array_number_string", 500);
|
|
24
24
|
expect(sqlType).toMatch(/VARCHAR\(500\)/i);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
test("getSqlType: array_number_text 生成 TEXT/MEDIUMTEXT", () => {
|
|
28
|
-
const sqlType = getSqlType("array_number_text", null);
|
|
28
|
+
const sqlType = syncTable.TestKit.getSqlType("mysql", "array_number_text", null);
|
|
29
29
|
expect(sqlType).toMatch(/TEXT/i);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test("getSqlType: array_number_string 使用 max 参数", () => {
|
|
33
|
-
const sqlType1 = getSqlType("array_number_string", 200);
|
|
34
|
-
const sqlType2 = getSqlType("array_number_string", 1000);
|
|
33
|
+
const sqlType1 = syncTable.TestKit.getSqlType("mysql", "array_number_string", 200);
|
|
34
|
+
const sqlType2 = syncTable.TestKit.getSqlType("mysql", "array_number_string", 1000);
|
|
35
35
|
|
|
36
36
|
expect(sqlType1).toMatch(/VARCHAR\(200\)/i);
|
|
37
37
|
expect(sqlType2).toMatch(/VARCHAR\(1000\)/i);
|
|
@@ -40,28 +40,28 @@ describe("syncDb - array_number 类型支持", () => {
|
|
|
40
40
|
// ==================== 默认值处理测试 ====================
|
|
41
41
|
|
|
42
42
|
test('resolveDefaultValue: array_number_string null 时返回 "[]"', () => {
|
|
43
|
-
const result = resolveDefaultValue(null, "array_number_string");
|
|
43
|
+
const result = syncTable.TestKit.resolveDefaultValue(null, "array_number_string");
|
|
44
44
|
expect(result).toBe("[]");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test("resolveDefaultValue: array_number_string 有默认值时保留", () => {
|
|
48
|
-
const result = resolveDefaultValue("[1,2,3]", "array_number_string");
|
|
48
|
+
const result = syncTable.TestKit.resolveDefaultValue("[1,2,3]", "array_number_string");
|
|
49
49
|
expect(result).toBe("[1,2,3]");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test('resolveDefaultValue: array_number_text null 时返回 "null"', () => {
|
|
53
|
-
const result = resolveDefaultValue(null, "array_number_text");
|
|
53
|
+
const result = syncTable.TestKit.resolveDefaultValue(null, "array_number_text");
|
|
54
54
|
expect(result).toBe("null");
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test("resolveDefaultValue: array_number_text 有默认值时保留", () => {
|
|
58
|
-
const result = resolveDefaultValue("[100,200]", "array_number_text");
|
|
58
|
+
const result = syncTable.TestKit.resolveDefaultValue("[100,200]", "array_number_text");
|
|
59
59
|
expect(result).toBe("[100,200]");
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
test('resolveDefaultValue: 字符串 "null" 也视为 null', () => {
|
|
63
|
-
const result1 = resolveDefaultValue("null", "array_number_string");
|
|
64
|
-
const result2 = resolveDefaultValue("null", "array_number_text");
|
|
63
|
+
const result1 = syncTable.TestKit.resolveDefaultValue("null", "array_number_string");
|
|
64
|
+
const result2 = syncTable.TestKit.resolveDefaultValue("null", "array_number_text");
|
|
65
65
|
|
|
66
66
|
expect(result1).toBe("[]");
|
|
67
67
|
expect(result2).toBe("null");
|
|
@@ -70,29 +70,29 @@ describe("syncDb - array_number 类型支持", () => {
|
|
|
70
70
|
// ==================== SQL DEFAULT 子句测试 ====================
|
|
71
71
|
|
|
72
72
|
test("generateDefaultSql: array_number_string 生成 DEFAULT 子句", () => {
|
|
73
|
-
const sql = generateDefaultSql("[]", "array_number_string");
|
|
73
|
+
const sql = syncTable.TestKit.generateDefaultSql("[]", "array_number_string");
|
|
74
74
|
expect(sql).toBe(" DEFAULT '[]'");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
test("generateDefaultSql: array_number_string 自定义默认值", () => {
|
|
78
|
-
const sql = generateDefaultSql("[10,20,30]", "array_number_string");
|
|
78
|
+
const sql = syncTable.TestKit.generateDefaultSql("[10,20,30]", "array_number_string");
|
|
79
79
|
expect(sql).toBe(" DEFAULT '[10,20,30]'");
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
test("generateDefaultSql: array_number_text 不生成 DEFAULT", () => {
|
|
83
|
-
const sql = generateDefaultSql("[]", "array_number_text");
|
|
83
|
+
const sql = syncTable.TestKit.generateDefaultSql("[]", "array_number_text");
|
|
84
84
|
expect(sql).toBe("");
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
test("generateDefaultSql: array_number_text null 时不生成 DEFAULT", () => {
|
|
88
|
-
const sql = generateDefaultSql("null", "array_number_text");
|
|
88
|
+
const sql = syncTable.TestKit.generateDefaultSql("null", "array_number_text");
|
|
89
89
|
expect(sql).toBe("");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
// ==================== 单引号转义测试 ====================
|
|
93
93
|
|
|
94
94
|
test("generateDefaultSql: 默认值包含单引号时正确转义", () => {
|
|
95
|
-
const sql = generateDefaultSql("[1,'test',2]", "array_number_string");
|
|
95
|
+
const sql = syncTable.TestKit.generateDefaultSql("[1,'test',2]", "array_number_string");
|
|
96
96
|
expect(sql).toBe(" DEFAULT '[1,''test'',2]'");
|
|
97
97
|
});
|
|
98
98
|
|
|
@@ -105,18 +105,18 @@ describe("syncDb - array_number 类型支持", () => {
|
|
|
105
105
|
const fieldDefault = null;
|
|
106
106
|
|
|
107
107
|
// 1. 判断是否需要长度
|
|
108
|
-
expect(isStringOrArrayType(fieldType)).toBe(true);
|
|
108
|
+
expect(syncTable.TestKit.isStringOrArrayType(fieldType)).toBe(true);
|
|
109
109
|
|
|
110
110
|
// 2. 获取 SQL 类型
|
|
111
|
-
const sqlType = getSqlType(fieldType, fieldMax);
|
|
111
|
+
const sqlType = syncTable.TestKit.getSqlType("mysql", fieldType, fieldMax);
|
|
112
112
|
expect(sqlType).toMatch(/VARCHAR\(500\)/i);
|
|
113
113
|
|
|
114
114
|
// 3. 处理默认值
|
|
115
|
-
const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
|
|
115
|
+
const actualDefault = syncTable.TestKit.resolveDefaultValue(fieldDefault, fieldType);
|
|
116
116
|
expect(actualDefault).toBe("[]");
|
|
117
117
|
|
|
118
118
|
// 4. 生成 DEFAULT 子句
|
|
119
|
-
const defaultSql = generateDefaultSql(actualDefault, fieldType);
|
|
119
|
+
const defaultSql = syncTable.TestKit.generateDefaultSql(actualDefault, fieldType);
|
|
120
120
|
expect(defaultSql).toBe(" DEFAULT '[]'");
|
|
121
121
|
});
|
|
122
122
|
|
|
@@ -127,18 +127,18 @@ describe("syncDb - array_number 类型支持", () => {
|
|
|
127
127
|
const fieldDefault = null;
|
|
128
128
|
|
|
129
129
|
// 1. 判断是否需要长度
|
|
130
|
-
expect(isStringOrArrayType(fieldType)).toBe(false);
|
|
130
|
+
expect(syncTable.TestKit.isStringOrArrayType(fieldType)).toBe(false);
|
|
131
131
|
|
|
132
132
|
// 2. 获取 SQL 类型
|
|
133
|
-
const sqlType = getSqlType(fieldType, fieldMax);
|
|
133
|
+
const sqlType = syncTable.TestKit.getSqlType("mysql", fieldType, fieldMax);
|
|
134
134
|
expect(sqlType).toMatch(/TEXT/i);
|
|
135
135
|
|
|
136
136
|
// 3. 处理默认值
|
|
137
|
-
const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
|
|
137
|
+
const actualDefault = syncTable.TestKit.resolveDefaultValue(fieldDefault, fieldType);
|
|
138
138
|
expect(actualDefault).toBe("null");
|
|
139
139
|
|
|
140
140
|
// 4. 生成 DEFAULT 子句(TEXT 类型不支持)
|
|
141
|
-
const defaultSql = generateDefaultSql(actualDefault, fieldType);
|
|
141
|
+
const defaultSql = syncTable.TestKit.generateDefaultSql(actualDefault, fieldType);
|
|
142
142
|
expect(defaultSql).toBe("");
|
|
143
143
|
});
|
|
144
144
|
|
|
@@ -148,13 +148,13 @@ describe("syncDb - array_number 类型支持", () => {
|
|
|
148
148
|
const fieldMax = 10;
|
|
149
149
|
const fieldDefault = "[60,70,80]";
|
|
150
150
|
|
|
151
|
-
const sqlType = getSqlType(fieldType, fieldMax);
|
|
151
|
+
const sqlType = syncTable.TestKit.getSqlType("mysql", fieldType, fieldMax);
|
|
152
152
|
expect(sqlType).toMatch(/VARCHAR\(10\)/i);
|
|
153
153
|
|
|
154
|
-
const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
|
|
154
|
+
const actualDefault = syncTable.TestKit.resolveDefaultValue(fieldDefault, fieldType);
|
|
155
155
|
expect(actualDefault).toBe("[60,70,80]");
|
|
156
156
|
|
|
157
|
-
const defaultSql = generateDefaultSql(actualDefault, fieldType);
|
|
157
|
+
const defaultSql = syncTable.TestKit.generateDefaultSql(actualDefault, fieldType);
|
|
158
158
|
expect(defaultSql).toBe(" DEFAULT '[60,70,80]'");
|
|
159
159
|
});
|
|
160
160
|
});
|