befly 3.10.1 → 3.10.3
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/.gitignore +0 -0
- package/configs/presetFields.ts +10 -0
- package/configs/presetRegexp.ts +225 -0
- package/package.json +15 -16
- package/tests/_mocks/mockSqliteDb.ts +0 -204
- package/tests/addonHelper-cache.test.ts +0 -32
- package/tests/api-integration-array-number.test.ts +0 -282
- package/tests/apiHandler-routePath-only.test.ts +0 -32
- package/tests/befly-config-env.test.ts +0 -78
- package/tests/cacheHelper.test.ts +0 -323
- package/tests/cacheKeys.test.ts +0 -41
- package/tests/checkApi-routePath-strict.test.ts +0 -166
- package/tests/checkMenu.test.ts +0 -346
- package/tests/checkTable-smoke.test.ts +0 -157
- package/tests/cipher.test.ts +0 -249
- package/tests/dbDialect-cache.test.ts +0 -23
- package/tests/dbDialect.test.ts +0 -46
- package/tests/dbHelper-advanced.test.ts +0 -723
- package/tests/dbHelper-all-array-types.test.ts +0 -316
- package/tests/dbHelper-array-serialization.test.ts +0 -258
- package/tests/dbHelper-batch-write.test.ts +0 -90
- package/tests/dbHelper-columns.test.ts +0 -234
- package/tests/dbHelper-execute.test.ts +0 -187
- package/tests/dbHelper-joins.test.ts +0 -221
- package/tests/fields-redis-cache.test.ts +0 -127
- package/tests/fields-validate.test.ts +0 -99
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +0 -3
- package/tests/fixtures/scanFilesApis/a.ts +0 -3
- package/tests/fixtures/scanFilesApis/sub/b.ts +0 -3
- package/tests/getClientIp.test.ts +0 -54
- package/tests/integration.test.ts +0 -189
- package/tests/jwt.test.ts +0 -65
- package/tests/loadPlugins-order-smoke.test.ts +0 -75
- package/tests/logger.test.ts +0 -325
- package/tests/redisHelper.test.ts +0 -495
- package/tests/redisKeys.test.ts +0 -9
- package/tests/scanConfig.test.ts +0 -144
- package/tests/scanFiles-routePath.test.ts +0 -46
- package/tests/smoke-sql.test.ts +0 -24
- package/tests/sqlBuilder-advanced.test.ts +0 -608
- package/tests/sqlBuilder.test.ts +0 -209
- package/tests/sync-connection.test.ts +0 -183
- package/tests/sync-init-guard.test.ts +0 -105
- package/tests/syncApi-insBatch-fields-consistent.test.ts +0 -61
- package/tests/syncApi-obsolete-records.test.ts +0 -69
- package/tests/syncApi-type-compat.test.ts +0 -72
- package/tests/syncDev-permissions.test.ts +0 -81
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +0 -88
- package/tests/syncMenu-duplicate-path.test.ts +0 -122
- package/tests/syncMenu-obsolete-records.test.ts +0 -161
- package/tests/syncMenu-parentPath-from-tree.test.ts +0 -75
- package/tests/syncMenu-paths.test.ts +0 -59
- package/tests/syncTable-apply.test.ts +0 -279
- package/tests/syncTable-array-number.test.ts +0 -160
- package/tests/syncTable-constants.test.ts +0 -101
- package/tests/syncTable-db-integration.test.ts +0 -237
- package/tests/syncTable-ddl.test.ts +0 -245
- package/tests/syncTable-helpers.test.ts +0 -99
- package/tests/syncTable-schema.test.ts +0 -99
- package/tests/syncTable-testkit.test.ts +0 -25
- package/tests/syncTable-types.test.ts +0 -122
- package/tests/tableRef-and-deserialize.test.ts +0 -67
- package/tests/util.test.ts +0 -100
- package/tests/validator-array-number.test.ts +0 -310
- package/tests/validator-default.test.ts +0 -373
- package/tests/validator.test.ts +0 -679
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,161 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { __test__ } from "../sync/syncMenu.js";
|
|
6
|
-
|
|
7
|
-
const testRootDir = join(process.cwd(), "temp", "test-sync-menu-views");
|
|
8
|
-
const viewsDir = join(testRootDir, "views");
|
|
9
|
-
|
|
10
|
-
function writeVueIndex(dir: string, title: string, order?: number): void {
|
|
11
|
-
if (!existsSync(dir)) {
|
|
12
|
-
mkdirSync(dir, { recursive: true });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const metaOrder = typeof order === "number" ? `, order: ${order}` : "";
|
|
16
|
-
|
|
17
|
-
const content = `<script setup>\ndefinePage({ meta: { title: "${title}"${metaOrder} } });\n</script>\n\n<template>\n <div />\n</template>\n`;
|
|
18
|
-
writeFileSync(join(dir, "index.vue"), content, { encoding: "utf8" });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
beforeAll(() => {
|
|
22
|
-
if (existsSync(testRootDir)) {
|
|
23
|
-
rmSync(testRootDir, { recursive: true, force: true });
|
|
24
|
-
}
|
|
25
|
-
mkdirSync(viewsDir, { recursive: true });
|
|
26
|
-
|
|
27
|
-
// 根级 index:应映射为 prefix 本身(不带尾随 /)
|
|
28
|
-
writeVueIndex(join(viewsDir, "index"), "Root", 2);
|
|
29
|
-
|
|
30
|
-
// 带数字后缀目录:应清理后缀
|
|
31
|
-
writeVueIndex(join(viewsDir, "user_1"), "User", 1);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterAll(() => {
|
|
35
|
-
if (existsSync(testRootDir)) {
|
|
36
|
-
rmSync(testRootDir, { recursive: true, force: true });
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("syncMenu - scanViewsDir paths", () => {
|
|
41
|
-
test("根级 index 不应生成尾随斜杠", async () => {
|
|
42
|
-
const prefix = "/addon/addonAdmin";
|
|
43
|
-
const menus = await __test__.scanViewsDir(viewsDir, prefix);
|
|
44
|
-
|
|
45
|
-
const root = menus.find((m) => m.path === prefix);
|
|
46
|
-
expect(root).toBeDefined();
|
|
47
|
-
expect(root?.path).toBe(prefix);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("目录名 _数字 后缀应被清理", async () => {
|
|
51
|
-
const prefix = "/addon/addonAdmin";
|
|
52
|
-
const menus = await __test__.scanViewsDir(viewsDir, prefix);
|
|
53
|
-
|
|
54
|
-
const user = menus.find((m) => m.path === "/addon/addonAdmin/user");
|
|
55
|
-
expect(user).toBeDefined();
|
|
56
|
-
expect(user?.name).toBe("User");
|
|
57
|
-
expect(user?.sort).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
});
|