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