befly 3.9.39 → 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.
Files changed (141) hide show
  1. package/README.md +39 -8
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +1 -1
  9. package/docs/api/api.md +1 -1
  10. package/docs/guide/quickstart.md +16 -9
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +7 -7
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +16 -9
  16. package/docs/reference/addon.md +12 -1
  17. package/docs/reference/config.md +13 -30
  18. package/docs/reference/sync.md +62 -193
  19. package/docs/reference/table.md +27 -29
  20. package/hooks/auth.ts +3 -4
  21. package/hooks/cors.ts +4 -6
  22. package/hooks/parser.ts +3 -4
  23. package/hooks/permission.ts +3 -4
  24. package/hooks/validator.ts +4 -5
  25. package/lib/cacheHelper.ts +89 -153
  26. package/lib/cacheKeys.ts +1 -1
  27. package/lib/connect.ts +9 -13
  28. package/lib/dbDialect.ts +285 -0
  29. package/lib/dbHelper.ts +179 -507
  30. package/lib/dbUtils.ts +450 -0
  31. package/lib/logger.ts +41 -5
  32. package/lib/redisHelper.ts +1 -0
  33. package/lib/sqlBuilder.ts +358 -58
  34. package/lib/sqlCheck.ts +136 -0
  35. package/lib/validator.ts +1 -1
  36. package/loader/loadApis.ts +23 -126
  37. package/loader/loadHooks.ts +31 -46
  38. package/loader/loadPlugins.ts +37 -52
  39. package/main.ts +58 -19
  40. package/package.json +24 -25
  41. package/paths.ts +14 -14
  42. package/plugins/cache.ts +12 -6
  43. package/plugins/cipher.ts +2 -2
  44. package/plugins/config.ts +6 -8
  45. package/plugins/db.ts +14 -19
  46. package/plugins/jwt.ts +6 -7
  47. package/plugins/logger.ts +7 -9
  48. package/plugins/redis.ts +8 -10
  49. package/plugins/tool.ts +3 -4
  50. package/router/api.ts +3 -2
  51. package/router/static.ts +7 -5
  52. package/sync/syncApi.ts +80 -235
  53. package/sync/syncCache.ts +16 -0
  54. package/sync/syncDev.ts +167 -202
  55. package/sync/syncMenu.ts +230 -444
  56. package/sync/syncTable.ts +1247 -0
  57. package/tests/_mocks/mockSqliteDb.ts +204 -0
  58. package/tests/addonHelper-cache.test.ts +32 -0
  59. package/tests/apiHandler-routePath-only.test.ts +32 -0
  60. package/tests/cacheHelper.test.ts +16 -51
  61. package/tests/checkApi-routePath-strict.test.ts +166 -0
  62. package/tests/checkMenu.test.ts +346 -0
  63. package/tests/checkTable-smoke.test.ts +157 -0
  64. package/tests/dbDialect-cache.test.ts +23 -0
  65. package/tests/dbDialect.test.ts +46 -0
  66. package/tests/dbHelper-advanced.test.ts +1 -1
  67. package/tests/dbHelper-all-array-types.test.ts +15 -15
  68. package/tests/dbHelper-batch-write.test.ts +90 -0
  69. package/tests/dbHelper-columns.test.ts +36 -54
  70. package/tests/dbHelper-execute.test.ts +26 -26
  71. package/tests/dbHelper-joins.test.ts +85 -176
  72. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  73. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  75. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  76. package/tests/logger.test.ts +6 -6
  77. package/tests/redisHelper.test.ts +6 -1
  78. package/tests/scanFiles-routePath.test.ts +46 -0
  79. package/tests/smoke-sql.test.ts +24 -0
  80. package/tests/sqlBuilder-advanced.test.ts +18 -5
  81. package/tests/sqlBuilder.test.ts +24 -0
  82. package/tests/sync-init-guard.test.ts +105 -0
  83. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  84. package/tests/syncApi-obsolete-records.test.ts +69 -0
  85. package/tests/syncApi-type-compat.test.ts +72 -0
  86. package/tests/syncDev-permissions.test.ts +81 -0
  87. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  88. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  89. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  90. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  91. package/tests/syncMenu-paths.test.ts +0 -9
  92. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  93. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  94. package/tests/syncTable-constants.test.ts +101 -0
  95. package/tests/syncTable-db-integration.test.ts +237 -0
  96. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  97. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  98. package/tests/syncTable-schema.test.ts +99 -0
  99. package/tests/syncTable-testkit.test.ts +25 -0
  100. package/tests/syncTable-types.test.ts +122 -0
  101. package/tests/tableRef-and-deserialize.test.ts +67 -0
  102. package/tsconfig.json +1 -1
  103. package/types/api.d.ts +1 -1
  104. package/types/befly.d.ts +13 -12
  105. package/types/cache.d.ts +2 -2
  106. package/types/context.d.ts +1 -1
  107. package/types/database.d.ts +0 -5
  108. package/types/hook.d.ts +1 -10
  109. package/types/plugin.d.ts +2 -96
  110. package/types/sync.d.ts +19 -25
  111. package/utils/convertBigIntFields.ts +38 -0
  112. package/utils/disableMenusGlob.ts +85 -0
  113. package/utils/importDefault.ts +21 -0
  114. package/utils/isDirentDirectory.ts +23 -0
  115. package/utils/loadMenuConfigs.ts +145 -0
  116. package/utils/processFields.ts +25 -0
  117. package/utils/scanAddons.ts +72 -0
  118. package/utils/scanFiles.ts +129 -21
  119. package/utils/scanSources.ts +64 -0
  120. package/utils/sortModules.ts +137 -0
  121. package/checks/checkApp.ts +0 -55
  122. package/hooks/rateLimit.ts +0 -276
  123. package/sync/syncAll.ts +0 -35
  124. package/sync/syncDb/apply.ts +0 -192
  125. package/sync/syncDb/constants.ts +0 -119
  126. package/sync/syncDb/ddl.ts +0 -251
  127. package/sync/syncDb/helpers.ts +0 -84
  128. package/sync/syncDb/schema.ts +0 -202
  129. package/sync/syncDb/sqlite.ts +0 -48
  130. package/sync/syncDb/table.ts +0 -207
  131. package/sync/syncDb/tableCreate.ts +0 -163
  132. package/sync/syncDb/types.ts +0 -132
  133. package/sync/syncDb/version.ts +0 -69
  134. package/sync/syncDb.ts +0 -168
  135. package/tests/rateLimit-hook.test.ts +0 -477
  136. package/tests/syncDb-constants.test.ts +0 -130
  137. package/tests/syncDb-schema.test.ts +0 -179
  138. package/tests/syncDb-types.test.ts +0 -139
  139. package/utils/addonHelper.ts +0 -90
  140. package/utils/modules.ts +0 -98
  141. package/utils/route.ts +0 -23
@@ -0,0 +1,99 @@
1
+ /**
2
+ * syncTable 表结构查询模块测试(纯 mock,不连接真实数据库)
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+
7
+ import { syncTable } from "../sync/syncTable.js";
8
+ import { createMockSqliteDb } from "./_mocks/mockSqliteDb.js";
9
+
10
+ describe("tableExistsRuntime", () => {
11
+ test("sql 执行器未初始化时抛出错误", async () => {
12
+ try {
13
+ await syncTable.TestKit.tableExistsRuntime(syncTable.TestKit.createRuntime("sqlite", null as any, ""), "user");
14
+ expect(true).toBe(false);
15
+ } catch (error: any) {
16
+ expect(error.message).toBe("SQL 执行器未初始化");
17
+ }
18
+ });
19
+
20
+ test("mock sqlite:表存在返回 true;表不存在返回 false", async () => {
21
+ const db = createMockSqliteDb({
22
+ executedSql: [],
23
+ tables: {
24
+ test_sync_table_exists: {
25
+ columns: {},
26
+ indexes: {}
27
+ }
28
+ }
29
+ });
30
+
31
+ const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
32
+ const exist = await syncTable.TestKit.tableExistsRuntime(runtime, "test_sync_table_exists");
33
+ expect(exist).toBe(true);
34
+
35
+ const notExist = await syncTable.TestKit.tableExistsRuntime(runtime, "test_sync_table_not_exist_12345");
36
+ expect(notExist).toBe(false);
37
+ });
38
+ });
39
+
40
+ describe("getTableColumnsRuntime", () => {
41
+ test("mock sqlite:返回列信息结构(至少包含我们定义的列)", async () => {
42
+ const db = createMockSqliteDb({
43
+ executedSql: [],
44
+ tables: {
45
+ test_sync_table_columns: {
46
+ columns: {
47
+ id: { name: "id", type: "INTEGER", notnull: 1, dflt_value: null },
48
+ user_name: { name: "user_name", type: "TEXT", notnull: 1, dflt_value: "''" },
49
+ user_id: { name: "user_id", type: "INTEGER", notnull: 1, dflt_value: "0" },
50
+ age: { name: "age", type: "INTEGER", notnull: 0, dflt_value: "0" },
51
+ created_at: { name: "created_at", type: "INTEGER", notnull: 1, dflt_value: "0" }
52
+ },
53
+ indexes: {}
54
+ }
55
+ }
56
+ });
57
+
58
+ const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
59
+ const columns = await syncTable.TestKit.getTableColumnsRuntime(runtime, "test_sync_table_columns");
60
+
61
+ expect(columns.id).toBeDefined();
62
+ expect(columns.user_name).toBeDefined();
63
+ expect(columns.user_id).toBeDefined();
64
+ expect(columns.age).toBeDefined();
65
+ expect(columns.created_at).toBeDefined();
66
+
67
+ expect(columns.user_name.nullable).toBe(false);
68
+ });
69
+ });
70
+
71
+ describe("getTableIndexesRuntime", () => {
72
+ test("mock sqlite:返回索引信息结构(仅单列索引;复合索引会被忽略)", async () => {
73
+ const db = createMockSqliteDb({
74
+ executedSql: [],
75
+ tables: {
76
+ test_sync_table_indexes: {
77
+ columns: {},
78
+ indexes: {
79
+ idx_created_at: ["created_at"],
80
+ idx_user_name: ["user_name"],
81
+ idx_composite: ["user_id", "created_at"]
82
+ }
83
+ }
84
+ }
85
+ });
86
+
87
+ const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
88
+ const indexes = await syncTable.TestKit.getTableIndexesRuntime(runtime, "test_sync_table_indexes");
89
+
90
+ expect(indexes.idx_created_at).toBeDefined();
91
+ expect(indexes.idx_created_at).toContain("created_at");
92
+
93
+ expect(indexes.idx_user_name).toBeDefined();
94
+ expect(indexes.idx_user_name).toContain("user_name");
95
+
96
+ // sqlite 路径下为了避免多列索引误判,仅收集单列索引
97
+ expect(indexes.idx_composite).toBeUndefined();
98
+ });
99
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { syncTable } from "../sync/syncTable.js";
4
+
5
+ describe("syncTable - TestKit 挂载", () => {
6
+ it("should expose TestKit as a stable object", () => {
7
+ expect(typeof syncTable).toBe("function");
8
+ expect(syncTable.TestKit).toBeDefined();
9
+
10
+ const descriptor = Object.getOwnPropertyDescriptor(syncTable, "TestKit");
11
+ expect(descriptor).toBeDefined();
12
+ expect(descriptor?.writable).toBe(false);
13
+ expect(descriptor?.configurable).toBe(false);
14
+ expect(descriptor?.enumerable).toBe(true);
15
+
16
+ expect(typeof syncTable.TestKit.quoteIdentifier).toBe("function");
17
+ expect(typeof syncTable.TestKit.getTypeMapping).toBe("function");
18
+
19
+ expect(syncTable.TestKit.DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR).toBe(8);
20
+
21
+ const firstRef = syncTable.TestKit;
22
+ const secondRef = syncTable.TestKit;
23
+ expect(firstRef).toBe(secondRef);
24
+ });
25
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * syncTable 类型处理模块测试
3
+ *
4
+ * 测试 types.ts 中的函数:
5
+ * - isStringOrArrayType
6
+ * - getSqlType
7
+ * - resolveDefaultValue
8
+ * - generateDefaultSql
9
+ */
10
+
11
+ import { describe, test, expect } from "bun:test";
12
+
13
+ import { syncTable } from "../sync/syncTable.js";
14
+
15
+ describe("isStringOrArrayType", () => {
16
+ test("string 类型返回 true", () => {
17
+ expect(syncTable.TestKit.isStringOrArrayType("string")).toBe(true);
18
+ });
19
+
20
+ test("array_string 类型返回 true", () => {
21
+ expect(syncTable.TestKit.isStringOrArrayType("array_string")).toBe(true);
22
+ });
23
+
24
+ test("number 类型返回 false", () => {
25
+ expect(syncTable.TestKit.isStringOrArrayType("number")).toBe(false);
26
+ });
27
+
28
+ test("text 类型返回 false", () => {
29
+ expect(syncTable.TestKit.isStringOrArrayType("text")).toBe(false);
30
+ });
31
+
32
+ test("array_text 类型返回 false", () => {
33
+ expect(syncTable.TestKit.isStringOrArrayType("array_text")).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe("resolveDefaultValue", () => {
38
+ test("null 值 + string 类型 => 空字符串", () => {
39
+ expect(syncTable.TestKit.resolveDefaultValue(null, "string")).toBe("");
40
+ });
41
+
42
+ test("null 值 + number 类型 => 0", () => {
43
+ expect(syncTable.TestKit.resolveDefaultValue(null, "number")).toBe(0);
44
+ });
45
+
46
+ test('"null" 字符串 + number 类型 => 0', () => {
47
+ expect(syncTable.TestKit.resolveDefaultValue("null", "number")).toBe(0);
48
+ });
49
+
50
+ test('null 值 + array_string 类型 => "[]"', () => {
51
+ expect(syncTable.TestKit.resolveDefaultValue(null, "array_string")).toBe("[]");
52
+ });
53
+
54
+ test('null 值 + text 类型 => "null"', () => {
55
+ expect(syncTable.TestKit.resolveDefaultValue(null, "text")).toBe("null");
56
+ });
57
+
58
+ test('null 值 + array_text 类型 => "null"(TEXT 不支持默认值)', () => {
59
+ expect(syncTable.TestKit.resolveDefaultValue(null, "array_text")).toBe("null");
60
+ });
61
+
62
+ test("有实际值时直接返回", () => {
63
+ expect(syncTable.TestKit.resolveDefaultValue("admin", "string")).toBe("admin");
64
+ expect(syncTable.TestKit.resolveDefaultValue(100, "number")).toBe(100);
65
+ expect(syncTable.TestKit.resolveDefaultValue(0, "number")).toBe(0);
66
+ });
67
+ });
68
+
69
+ describe("generateDefaultSql", () => {
70
+ test("number 类型生成数字默认值", () => {
71
+ expect(syncTable.TestKit.generateDefaultSql(0, "number")).toBe(" DEFAULT 0");
72
+ expect(syncTable.TestKit.generateDefaultSql(100, "number")).toBe(" DEFAULT 100");
73
+ });
74
+
75
+ test("string 类型生成带引号默认值", () => {
76
+ expect(syncTable.TestKit.generateDefaultSql("admin", "string")).toBe(" DEFAULT 'admin'");
77
+ expect(syncTable.TestKit.generateDefaultSql("", "string")).toBe(" DEFAULT ''");
78
+ });
79
+
80
+ test("text 类型不生成默认值", () => {
81
+ expect(syncTable.TestKit.generateDefaultSql("null", "text")).toBe("");
82
+ });
83
+
84
+ test("array_string 类型生成 JSON 数组默认值", () => {
85
+ expect(syncTable.TestKit.generateDefaultSql("[]", "array_string")).toBe(" DEFAULT '[]'");
86
+ });
87
+
88
+ test("array_text 类型不生成默认值(MySQL TEXT 不支持)", () => {
89
+ expect(syncTable.TestKit.generateDefaultSql("[]", "array_text")).toBe("");
90
+ });
91
+
92
+ test("单引号被正确转义", () => {
93
+ expect(syncTable.TestKit.generateDefaultSql("it's", "string")).toBe(" DEFAULT 'it''s'");
94
+ });
95
+ });
96
+
97
+ describe("getSqlType", () => {
98
+ test("string 类型带长度", () => {
99
+ const result = syncTable.TestKit.getSqlType("mysql", "string", 100);
100
+ expect(result).toBe("VARCHAR(100)");
101
+ });
102
+
103
+ test("array_string 类型带长度", () => {
104
+ const result = syncTable.TestKit.getSqlType("mysql", "array_string", 500);
105
+ expect(result).toBe("VARCHAR(500)");
106
+ });
107
+
108
+ test("number 类型无符号", () => {
109
+ const result = syncTable.TestKit.getSqlType("mysql", "number", null, true);
110
+ expect(result).toBe("BIGINT UNSIGNED");
111
+ });
112
+
113
+ test("number 类型有符号", () => {
114
+ const result = syncTable.TestKit.getSqlType("mysql", "number", null, false);
115
+ expect(result).toBe("BIGINT");
116
+ });
117
+
118
+ test("text 类型", () => {
119
+ const result = syncTable.TestKit.getSqlType("mysql", "text", null);
120
+ expect(result).toBe("MEDIUMTEXT");
121
+ });
122
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+
3
+ import { MySqlDialect } from "../lib/dbDialect.js";
4
+ import { DbHelper } from "../lib/dbHelper.js";
5
+ import { DbUtils } from "../lib/dbUtils.js";
6
+ import { SqlBuilder } from "../lib/sqlBuilder.js";
7
+
8
+ describe("tableRef normalize + escape", () => {
9
+ it("DbUtils.normalizeTableRef: 保留 alias,仅 snakeCase 表名", () => {
10
+ expect(DbUtils.normalizeTableRef("UserProfile up")).toBe("user_profile up");
11
+ expect(DbUtils.normalizeTableRef("order o")).toBe("order o");
12
+ });
13
+
14
+ it("SqlBuilder.from: 支持 table alias", () => {
15
+ const sql = new SqlBuilder().select(["*"]).from("order o").toSelectSql().sql;
16
+ expect(sql).toContain("FROM `order` o");
17
+ });
18
+
19
+ it("SqlBuilder.from: 支持 schema.table alias", () => {
20
+ const sql = new SqlBuilder().select(["*"]).from("my_db.users u").toSelectSql().sql;
21
+ expect(sql).toContain("FROM `my_db`.`users` u");
22
+ });
23
+
24
+ it("SqlBuilder.from: 复杂表引用要求显式 fromRaw", () => {
25
+ expect(() => new SqlBuilder().select(["*"]).from("users u FORCE INDEX (idx_user)")).toThrow();
26
+ });
27
+ });
28
+
29
+ describe("DbHelper.getList deserialize", () => {
30
+ it("getList: 数组字段应被 DbUtils.deserializeArrayFields 反序列化", async () => {
31
+ const sqlMock = {
32
+ unsafe: mock(async (sql: string, _params?: any[]) => {
33
+ if (sql.includes("COUNT(*) as total")) {
34
+ return [{ total: 1 }];
35
+ }
36
+ return [
37
+ {
38
+ id: "1",
39
+ tags: '["a","b"]',
40
+ state: 1,
41
+ created_at: 0,
42
+ updated_at: 0
43
+ }
44
+ ];
45
+ })
46
+ };
47
+
48
+ const redisMock = {
49
+ getObject: mock(async () => null),
50
+ setObject: mock(async () => "OK"),
51
+ genTimeID: mock(async () => 1)
52
+ };
53
+
54
+ const dbHelper = new DbHelper({ redis: redisMock as any, sql: sqlMock as any, dialect: new MySqlDialect() });
55
+
56
+ const result = await dbHelper.getList<{ id: number; tags: string[] }>({
57
+ table: "users",
58
+ fields: ["id", "tags"],
59
+ where: {}
60
+ });
61
+
62
+ expect(result.total).toBe(1);
63
+ expect(result.lists.length).toBe(1);
64
+ expect(Array.isArray(result.lists[0].tags)).toBe(true);
65
+ expect(result.lists[0].tags).toEqual(["a", "b"]);
66
+ });
67
+ });
package/tsconfig.json CHANGED
@@ -50,5 +50,5 @@
50
50
  }
51
51
  },
52
52
  "include": ["**/*.ts"],
53
- "exclude": ["node_modules", "dist", "logs", "temp", "tests", "main.single.ts"]
53
+ "exclude": ["node_modules", "dist", "logs", "temp", "tests"]
54
54
  }
package/types/api.d.ts CHANGED
@@ -11,7 +11,7 @@ import type { TableDefinition } from "./validate.js";
11
11
  * HTTP 方法类型
12
12
  * 支持 GET、POST 或逗号分隔的组合
13
13
  */
14
- export type HttpMethod = "GET" | "POST" | "GET,POST" | "POST,GET";
14
+ export type HttpMethod = "GET" | "POST" | "GET,POST";
15
15
 
16
16
  /**
17
17
  * 用户信息类型
package/types/befly.d.ts CHANGED
@@ -93,8 +93,8 @@ export interface CorsConfig {
93
93
  export interface RateLimitRule {
94
94
  /**
95
95
  * 路由匹配串
96
- * - 精确:"POST/api/auth/login"
97
- * - 前缀:"POST/api/auth/*" 或 "/api/auth/*"
96
+ * - 精确:"/api/auth/login"
97
+ * - 前缀:"/api/auth/*"
98
98
  * - 全量:"*"
99
99
  */
100
100
  route: string;
@@ -117,8 +117,8 @@ export interface RateLimitConfig {
117
117
  key?: "ip" | "user" | "ip_user";
118
118
  /**
119
119
  * 直接跳过限流的路由列表(优先级最高)
120
- * - 精确:"POST/api/health" 或 "/api/health"
121
- * - 前缀:"POST/api/health/*" 或 "/api/health/*"
120
+ * - 精确:"/api/health"
121
+ * - 前缀:"/api/health/*"
122
122
  */
123
123
  skipRoutes?: string[];
124
124
  /** 路由规则列表 */
@@ -165,14 +165,15 @@ export interface BeflyOptions {
165
165
  disableHooks?: string[];
166
166
  /** 禁用的插件列表 */
167
167
  disablePlugins?: string[];
168
-
169
- /** 是否启用组件钩子扫描(默认 false,仅加载 core hooks) */
170
- enableAddonHooks?: boolean;
171
-
172
- /** 是否启用项目钩子扫描(默认 false,仅加载 core hooks) */
173
- enableAppHooks?: boolean;
174
- /** 隐藏的菜单路径列表(不同步到数据库) */
175
- hiddenMenus?: string[];
168
+ /**
169
+ * 禁用的菜单 path 规则(用于菜单同步与加载前过滤)
170
+ *
171
+ * 仅支持 Bun.Glob 的 glob pattern 语法与 API(即把每条规则当作 glob 模式匹配菜单 path)。
172
+ * 示例:
173
+ * - 精确:"/addon/admin/login"
174
+ * - 通配:"/addon/admin/log/*"、"/addon/admin/**"、"**\/login"
175
+ */
176
+ disableMenus?: string[];
176
177
  /**
177
178
  * Addon 运行时配置
178
179
  * 按 addon 名称分组,如 addons.admin.email
package/types/cache.d.ts CHANGED
@@ -27,7 +27,7 @@ export interface CacheHelper {
27
27
  /**
28
28
  * 增量刷新单个角色的接口权限缓存
29
29
  */
30
- refreshRoleApiPermissions(roleCode: string, apiIds: number[]): Promise<void>;
30
+ refreshRoleApiPermissions(roleCode: string, apiPaths: string[]): Promise<void>;
31
31
 
32
32
  /**
33
33
  * 缓存所有数据(接口、菜单、角色权限)
@@ -56,7 +56,7 @@ export interface CacheHelper {
56
56
  /**
57
57
  * 检查角色是否有指定接口权限
58
58
  * @param roleCode - 角色代码
59
- * @param apiPath - 接口路径(格式:METHOD/path)
59
+ * @param apiPath - 接口路径(url.pathname,例如 /api/user/login;与 method 无关)
60
60
  * @returns 是否有权限
61
61
  */
62
62
  checkRolePermission(roleCode: string, apiPath: string): Promise<boolean>;
@@ -22,7 +22,7 @@ export interface RequestContext {
22
22
  ip: string;
23
23
  /** 请求头 */
24
24
  headers: Headers;
25
- /** API 路由路径(如 POST/api/user/login */
25
+ /** API 路由路径(url.pathname,例如 /api/user/login;与 method 无关) */
26
26
  route: string;
27
27
  /** 请求唯一 ID */
28
28
  requestId: string;
@@ -400,11 +400,6 @@ export interface DbHelper {
400
400
  * 查询单个字段值
401
401
  */
402
402
  getFieldValue<T = any>(options: Omit<QueryOptions, "fields"> & { field: string }): Promise<T | null>;
403
-
404
- /**
405
- * 清理数据或 where 条件(默认排除 null 和 undefined)
406
- */
407
- cleanFields<T extends Record<string, any>>(data: T, excludeValues?: any[], keepValues?: Record<string, any>): Partial<T>;
408
403
  }
409
404
 
410
405
  /**
package/types/hook.d.ts CHANGED
@@ -18,17 +18,8 @@ export interface Hook {
18
18
  name?: string;
19
19
 
20
20
  /** 依赖的钩子列表(在这些钩子之后执行) */
21
- after?: string[];
22
-
23
- /** 执行顺序(数字越小越先执行) */
24
- order?: number;
21
+ deps: string[];
25
22
 
26
23
  /** 钩子处理函数 */
27
24
  handler: HookHandler;
28
-
29
- /** 钩子配置 */
30
- config?: Record<string, any>;
31
-
32
- /** 钩子描述 */
33
- description?: string;
34
25
  }
package/types/plugin.d.ts CHANGED
@@ -3,22 +3,6 @@
3
3
  */
4
4
 
5
5
  import type { BeflyContext } from "./befly.js";
6
- import type { RequestContext } from "./context.js";
7
-
8
- /**
9
- * 插件初始化函数类型
10
- */
11
- export type PluginInitFunction = (befly: BeflyContext) => Promise<any> | any;
12
-
13
- /**
14
- * 插件请求处理钩子函数类型
15
- */
16
- export type Next = () => Promise<void>;
17
-
18
- /**
19
- * 插件请求钩子类型
20
- */
21
- export type PluginRequestHook = (ctx: RequestContext, next: Next) => Promise<void> | void;
22
6
 
23
7
  /**
24
8
  * 插件配置类型
@@ -26,88 +10,10 @@ export type PluginRequestHook = (ctx: RequestContext, next: Next) => Promise<voi
26
10
  export interface Plugin {
27
11
  /** 插件名称(运行时动态添加,由文件名生成) */
28
12
  name?: string;
29
- /** @deprecated use name instead */
30
- pluginName?: string;
31
13
 
32
14
  /** 依赖的插件列表(在这些插件之后执行) */
33
- after?: string[];
15
+ deps: string[];
34
16
 
35
17
  /** 插件初始化函数 */
36
- handler?: (context: BeflyContext) => any | Promise<any>;
37
-
38
- /** @deprecated use handler instead */
39
- onInit?: PluginInitFunction;
40
-
41
- /** 插件描述 */
42
- description?: string;
43
-
44
- /** 插件版本 */
45
- version?: string;
46
-
47
- /** 插件作者 */
48
- author?: string;
49
- }
50
-
51
- /**
52
- * 插件导出格式
53
- */
54
- export interface PluginExport {
55
- default: Plugin;
56
- }
57
-
58
- /**
59
- * 插件加载选项
60
- */
61
- export interface PluginLoadOptions {
62
- /** 插件目录路径 */
63
- pluginDir: string;
64
-
65
- /** 是否跳过错误 */
66
- skipErrors?: boolean;
67
-
68
- /** 插件过滤函数 */
69
- filter?: (filename: string) => boolean;
70
- }
71
-
72
- /**
73
- * 插件执行上下文
74
- */
75
- export interface PluginContext {
76
- /** 插件名称 */
77
- name: string;
78
-
79
- /** 执行开始时间 */
80
- startTime: number;
81
-
82
- /** 执行结束时间 */
83
- endTime?: number;
84
-
85
- /** 执行状态 */
86
- status: "pending" | "running" | "success" | "error";
87
-
88
- /** 错误信息 */
89
- error?: Error;
90
- }
91
-
92
- /**
93
- * 插件管理器接口
94
- */
95
- export interface PluginManager {
96
- /** 已加载的插件列表 */
97
- plugins: Plugin[];
98
-
99
- /** 注册插件 */
100
- register(plugin: Plugin): void;
101
-
102
- /** 加载插件目录 */
103
- loadPlugins(options: PluginLoadOptions): Promise<void>;
104
-
105
- /** 执行所有插件 */
106
- executeAll(befly: BeflyContext): Promise<void>;
107
-
108
- /** 获取插件 */
109
- getPlugin(name: string): Plugin | undefined;
110
-
111
- /** 移除插件 */
112
- removePlugin(name: string): boolean;
18
+ handler: (context: BeflyContext) => any | Promise<any>;
113
19
  }
package/types/sync.d.ts CHANGED
@@ -5,30 +5,10 @@
5
5
 
6
6
  // ==================== 命令选项类型 ====================
7
7
 
8
- /**
9
- * SyncDb 命令选项
10
- */
11
- export interface SyncDbOptions {
12
- table?: string;
13
- dryRun?: boolean;
14
- force?: boolean;
15
- }
16
-
17
- /**
18
- * Sync 命令选项
19
- */
20
- export interface SyncOptions {
21
- table?: string;
22
- force?: boolean;
23
- dryRun?: boolean;
24
- drop?: boolean;
25
- }
26
-
27
8
  /**
28
9
  * SyncMenu 命令选项
29
10
  */
30
11
  export interface SyncMenuOptions {
31
- plan?: boolean;
32
12
  }
33
13
 
34
14
  /**
@@ -42,27 +22,41 @@ export interface MenuConfig {
42
22
  children?: MenuConfig[];
43
23
  }
44
24
 
25
+ /**
26
+ * 菜单配置来源(三值约束)
27
+ */
28
+ export type MenuConfigSource = "core" | "app" | "addon";
29
+
45
30
  /**
46
31
  * SyncDev 命令选项
47
32
  */
48
33
  export interface SyncDevOptions {
49
- plan?: boolean;
50
34
  }
51
35
 
52
36
  /**
53
37
  * SyncApi 命令选项
54
38
  */
55
39
  export interface SyncApiOptions {
56
- plan?: boolean;
57
40
  }
58
41
 
42
+ /**
43
+ * syncApi 入参条目(来自 scanFiles 扫描结果的最小子集)
44
+ * - addonName 必须为 string(app/core 为 "",addon 为真实 addonName)
45
+ * - type 可选:历史/测试数据可能缺失;存在且不为 "api" 时在 syncApi 中会被跳过
46
+ */
47
+ export type SyncApiItem = {
48
+ type?: string;
49
+ routePath: string;
50
+ name: string;
51
+ addonName: string;
52
+ } & Record<string, any>;
53
+
59
54
  /**
60
55
  * API 信息
61
56
  */
62
57
  export interface ApiInfo {
63
58
  name: string;
64
59
  path: string;
65
- method: string;
66
60
  description: string;
67
61
  addonName: string;
68
62
  addonTitle: string;
@@ -281,7 +275,7 @@ export interface IndexDetail {
281
275
  export interface ApiReport {
282
276
  stats: {
283
277
  totalApis: number;
284
- projectApis: number;
278
+ appApis: number;
285
279
  addonApis: number;
286
280
  created: number;
287
281
  updated: number;
@@ -289,7 +283,7 @@ export interface ApiReport {
289
283
  };
290
284
  details: {
291
285
  bySource: {
292
- project: ApiDetail[];
286
+ app: ApiDetail[];
293
287
  addons: Record<string, ApiDetail[]>;
294
288
  };
295
289
  byAction: {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 转换数据库 BIGINT 字段为数字类型
3
+ *
4
+ * 当 bigint: false 时,Bun SQL 会将大于 u32 的 BIGINT 返回为字符串,此方法将其转换为 number。
5
+ *
6
+ * 转换规则:
7
+ * 1. 白名单中的字段会被转换
8
+ * 2. 所有以 'Id' 或 '_id' 结尾的字段会被自动转换
9
+ * 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
10
+ */
11
+ export function convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = ["id", "pid", "sort"]): T[] {
12
+ if (!arr || !Array.isArray(arr)) {
13
+ return arr as T[];
14
+ }
15
+
16
+ return arr.map((item) => {
17
+ const converted: Record<string, any> = {};
18
+ for (const [key, value] of Object.entries(item)) {
19
+ converted[key] = value;
20
+ }
21
+
22
+ for (const [key, value] of Object.entries(converted)) {
23
+ if (value === undefined || value === null) {
24
+ continue;
25
+ }
26
+
27
+ const shouldConvert = fields.includes(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
28
+ if (shouldConvert && typeof value === "string") {
29
+ const num = Number(value);
30
+ if (!isNaN(num)) {
31
+ converted[key] = num;
32
+ }
33
+ }
34
+ }
35
+
36
+ return converted as T;
37
+ }) as T[];
38
+ }