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.
Files changed (144) hide show
  1. package/README.md +47 -19
  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 +17 -11
  9. package/docs/api/api.md +16 -2
  10. package/docs/guide/quickstart.md +31 -10
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +26 -14
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +5 -328
  16. package/docs/reference/addon.md +0 -4
  17. package/docs/reference/config.md +14 -31
  18. package/docs/reference/logger.md +3 -3
  19. package/docs/reference/sync.md +132 -237
  20. package/docs/reference/table.md +28 -30
  21. package/hooks/auth.ts +3 -4
  22. package/hooks/cors.ts +4 -6
  23. package/hooks/parser.ts +3 -4
  24. package/hooks/permission.ts +3 -4
  25. package/hooks/validator.ts +3 -4
  26. package/lib/cacheHelper.ts +89 -153
  27. package/lib/cacheKeys.ts +1 -1
  28. package/lib/connect.ts +9 -13
  29. package/lib/dbDialect.ts +285 -0
  30. package/lib/dbHelper.ts +179 -507
  31. package/lib/dbUtils.ts +450 -0
  32. package/lib/logger.ts +41 -5
  33. package/lib/redisHelper.ts +1 -0
  34. package/lib/sqlBuilder.ts +358 -58
  35. package/lib/sqlCheck.ts +136 -0
  36. package/lib/validator.ts +1 -1
  37. package/loader/loadApis.ts +23 -126
  38. package/loader/loadHooks.ts +31 -46
  39. package/loader/loadPlugins.ts +37 -52
  40. package/main.ts +58 -19
  41. package/package.json +24 -25
  42. package/paths.ts +14 -14
  43. package/plugins/cache.ts +12 -6
  44. package/plugins/cipher.ts +2 -2
  45. package/plugins/config.ts +6 -8
  46. package/plugins/db.ts +14 -19
  47. package/plugins/jwt.ts +6 -7
  48. package/plugins/logger.ts +7 -9
  49. package/plugins/redis.ts +8 -10
  50. package/plugins/tool.ts +3 -4
  51. package/router/api.ts +3 -2
  52. package/router/static.ts +7 -5
  53. package/sync/syncApi.ts +80 -235
  54. package/sync/syncCache.ts +16 -0
  55. package/sync/syncDev.ts +167 -202
  56. package/sync/syncMenu.ts +230 -444
  57. package/sync/syncTable.ts +1247 -0
  58. package/tests/_mocks/mockSqliteDb.ts +204 -0
  59. package/tests/addonHelper-cache.test.ts +32 -0
  60. package/tests/apiHandler-routePath-only.test.ts +32 -0
  61. package/tests/cacheHelper.test.ts +16 -51
  62. package/tests/checkApi-routePath-strict.test.ts +166 -0
  63. package/tests/checkMenu.test.ts +346 -0
  64. package/tests/checkTable-smoke.test.ts +157 -0
  65. package/tests/dbDialect-cache.test.ts +23 -0
  66. package/tests/dbDialect.test.ts +46 -0
  67. package/tests/dbHelper-advanced.test.ts +1 -1
  68. package/tests/dbHelper-all-array-types.test.ts +15 -15
  69. package/tests/dbHelper-batch-write.test.ts +90 -0
  70. package/tests/dbHelper-columns.test.ts +36 -54
  71. package/tests/dbHelper-execute.test.ts +26 -26
  72. package/tests/dbHelper-joins.test.ts +85 -176
  73. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  75. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  76. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  77. package/tests/logger.test.ts +6 -6
  78. package/tests/redisHelper.test.ts +6 -1
  79. package/tests/scanFiles-routePath.test.ts +46 -0
  80. package/tests/smoke-sql.test.ts +24 -0
  81. package/tests/sqlBuilder-advanced.test.ts +18 -5
  82. package/tests/sqlBuilder.test.ts +24 -0
  83. package/tests/sync-init-guard.test.ts +105 -0
  84. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  85. package/tests/syncApi-obsolete-records.test.ts +69 -0
  86. package/tests/syncApi-type-compat.test.ts +72 -0
  87. package/tests/syncDev-permissions.test.ts +81 -0
  88. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  89. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  90. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  91. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  92. package/tests/syncMenu-paths.test.ts +0 -9
  93. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  94. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  95. package/tests/syncTable-constants.test.ts +101 -0
  96. package/tests/syncTable-db-integration.test.ts +237 -0
  97. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  98. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  99. package/tests/syncTable-schema.test.ts +99 -0
  100. package/tests/syncTable-testkit.test.ts +25 -0
  101. package/tests/syncTable-types.test.ts +122 -0
  102. package/tests/tableRef-and-deserialize.test.ts +67 -0
  103. package/tsconfig.json +1 -1
  104. package/types/api.d.ts +1 -1
  105. package/types/befly.d.ts +13 -12
  106. package/types/cache.d.ts +2 -2
  107. package/types/context.d.ts +1 -1
  108. package/types/database.d.ts +0 -5
  109. package/types/hook.d.ts +1 -10
  110. package/types/plugin.d.ts +2 -96
  111. package/types/sync.d.ts +19 -25
  112. package/utils/convertBigIntFields.ts +38 -0
  113. package/utils/disableMenusGlob.ts +85 -0
  114. package/utils/importDefault.ts +21 -0
  115. package/utils/isDirentDirectory.ts +23 -0
  116. package/utils/loadMenuConfigs.ts +145 -0
  117. package/utils/processFields.ts +25 -0
  118. package/utils/scanAddons.ts +72 -0
  119. package/utils/scanFiles.ts +129 -21
  120. package/utils/scanSources.ts +64 -0
  121. package/utils/sortModules.ts +137 -0
  122. package/checks/checkApp.ts +0 -55
  123. package/docs/cipher.md +0 -582
  124. package/docs/database.md +0 -1176
  125. package/hooks/rateLimit.ts +0 -276
  126. package/sync/syncAll.ts +0 -35
  127. package/sync/syncDb/apply.ts +0 -192
  128. package/sync/syncDb/constants.ts +0 -119
  129. package/sync/syncDb/ddl.ts +0 -251
  130. package/sync/syncDb/helpers.ts +0 -84
  131. package/sync/syncDb/schema.ts +0 -202
  132. package/sync/syncDb/sqlite.ts +0 -48
  133. package/sync/syncDb/table.ts +0 -207
  134. package/sync/syncDb/tableCreate.ts +0 -163
  135. package/sync/syncDb/types.ts +0 -132
  136. package/sync/syncDb/version.ts +0 -69
  137. package/sync/syncDb.ts +0 -168
  138. package/tests/rateLimit-hook.test.ts +0 -477
  139. package/tests/syncDb-constants.test.ts +0 -130
  140. package/tests/syncDb-schema.test.ts +0 -179
  141. package/tests/syncDb-types.test.ts +0 -139
  142. package/utils/addonHelper.ts +0 -90
  143. package/utils/modules.ts +0 -98
  144. package/utils/route.ts +0 -23
@@ -1,130 +0,0 @@
1
- /**
2
- * syncDb 常量模块测试
3
- *
4
- * 测试 constants.ts 中的常量:
5
- * - DB_VERSION_REQUIREMENTS
6
- * - SYSTEM_INDEX_FIELDS
7
- * - SYSTEM_INDEX_FIELDS
8
- * - CHANGE_TYPE_LABELS
9
- * - MYSQL_TABLE_CONFIG
10
- * - typeMapping
11
- */
12
-
13
- import { describe, test, expect, beforeAll } from "bun:test";
14
-
15
- import { setDbType } from "../sync/syncDb/constants.js";
16
-
17
- // 设置数据库类型为 MySQL
18
- setDbType("mysql");
19
-
20
- let constants: any;
21
-
22
- beforeAll(async () => {
23
- constants = await import("../sync/syncDb/constants.js");
24
- });
25
-
26
- describe("DB_VERSION_REQUIREMENTS", () => {
27
- test("MySQL 最低版本为 8", () => {
28
- expect(constants.DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR).toBe(8);
29
- });
30
-
31
- test("PostgreSQL 最低版本为 17", () => {
32
- expect(constants.DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR).toBe(17);
33
- });
34
-
35
- test("SQLite 最低版本为 3.50.0", () => {
36
- expect(constants.DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION).toBe("3.50.0");
37
- });
38
- });
39
-
40
- describe("SYSTEM_INDEX_FIELDS", () => {
41
- test("包含 created_at", () => {
42
- expect(constants.SYSTEM_INDEX_FIELDS).toContain("created_at");
43
- });
44
-
45
- test("包含 updated_at", () => {
46
- expect(constants.SYSTEM_INDEX_FIELDS).toContain("updated_at");
47
- });
48
-
49
- test("包含 state", () => {
50
- expect(constants.SYSTEM_INDEX_FIELDS).toContain("state");
51
- });
52
-
53
- test("共 3 个系统索引字段", () => {
54
- expect(constants.SYSTEM_INDEX_FIELDS.length).toBe(3);
55
- });
56
- });
57
-
58
- describe("CHANGE_TYPE_LABELS", () => {
59
- test('length 对应 "长度"', () => {
60
- expect(constants.CHANGE_TYPE_LABELS.length).toBe("长度");
61
- });
62
-
63
- test('datatype 对应 "类型"', () => {
64
- expect(constants.CHANGE_TYPE_LABELS.datatype).toBe("类型");
65
- });
66
-
67
- test('comment 对应 "注释"', () => {
68
- expect(constants.CHANGE_TYPE_LABELS.comment).toBe("注释");
69
- });
70
-
71
- test('default 对应 "默认值"', () => {
72
- expect(constants.CHANGE_TYPE_LABELS.default).toBe("默认值");
73
- });
74
- });
75
-
76
- describe("MYSQL_TABLE_CONFIG", () => {
77
- test("ENGINE 为 InnoDB", () => {
78
- expect(constants.MYSQL_TABLE_CONFIG.ENGINE).toBe("InnoDB");
79
- });
80
-
81
- test("CHARSET 为 utf8mb4", () => {
82
- expect(constants.MYSQL_TABLE_CONFIG.CHARSET).toBe("utf8mb4");
83
- });
84
-
85
- test("COLLATE 为 utf8mb4_0900_ai_ci", () => {
86
- expect(constants.MYSQL_TABLE_CONFIG.COLLATE).toBe("utf8mb4_0900_ai_ci");
87
- });
88
- });
89
-
90
- describe("getTypeMapping (MySQL)", () => {
91
- test("number 映射为 BIGINT", () => {
92
- expect(constants.getTypeMapping().number).toBe("BIGINT");
93
- });
94
-
95
- test("string 映射为 VARCHAR", () => {
96
- expect(constants.getTypeMapping().string).toBe("VARCHAR");
97
- });
98
-
99
- test("text 映射为 MEDIUMTEXT", () => {
100
- expect(constants.getTypeMapping().text).toBe("MEDIUMTEXT");
101
- });
102
-
103
- test("array_string 映射为 VARCHAR", () => {
104
- expect(constants.getTypeMapping().array_string).toBe("VARCHAR");
105
- });
106
-
107
- test("array_text 映射为 MEDIUMTEXT", () => {
108
- expect(constants.getTypeMapping().array_text).toBe("MEDIUMTEXT");
109
- });
110
- });
111
-
112
- describe("IS_PLAN", () => {
113
- test("IS_PLAN 为 boolean 类型", () => {
114
- expect(typeof constants.IS_PLAN).toBe("boolean");
115
- });
116
- });
117
-
118
- describe("数据库类型判断 (MySQL)", () => {
119
- test("isMySQL 为 true", () => {
120
- expect(constants.isMySQL()).toBe(true);
121
- });
122
-
123
- test("isPG 为 false", () => {
124
- expect(constants.isPG()).toBe(false);
125
- });
126
-
127
- test("isSQLite 为 false", () => {
128
- expect(constants.isSQLite()).toBe(false);
129
- });
130
- });
@@ -1,179 +0,0 @@
1
- /**
2
- * syncDb 表结构查询模块测试
3
- *
4
- * 测试 schema.ts 中的函数(纯逻辑测试,不需要数据库连接):
5
- * - tableExists
6
- * - getTableColumns
7
- * - getTableIndexes
8
- *
9
- * 注意:这些是模拟测试,实际数据库操作需要集成测试
10
- */
11
-
12
- import { describe, test, expect, beforeAll } from "bun:test";
13
-
14
- import { setDbType } from "../sync/syncDb/constants.js";
15
-
16
- // 设置数据库类型为 MySQL
17
- setDbType("mysql");
18
-
19
- let tableExists: any;
20
- let getTableColumns: any;
21
- let getTableIndexes: any;
22
-
23
- beforeAll(async () => {
24
- const schema = await import("../sync/syncDb/schema.js");
25
- tableExists = schema.tableExists;
26
- getTableColumns = schema.getTableColumns;
27
- getTableIndexes = schema.getTableIndexes;
28
- });
29
-
30
- describe("tableExists", () => {
31
- test("sql 客户端未初始化时抛出错误", async () => {
32
- try {
33
- await tableExists(null, "user");
34
- expect(true).toBe(false); // 不应该到这里
35
- } catch (error: any) {
36
- expect(error.message).toBe("SQL 客户端未初始化");
37
- }
38
- });
39
-
40
- test("传入有效 sql 客户端时正常执行", async () => {
41
- // 创建模拟 SQL 客户端
42
- const mockSql = Object.assign(
43
- async function (_strings: TemplateStringsArray, ..._values: any[]) {
44
- // 模拟 MySQL 查询返回
45
- return [{ count: 1 }];
46
- },
47
- {
48
- unsafe: async (_query: string) => []
49
- }
50
- );
51
-
52
- const result = await tableExists(mockSql, "user", "test_db");
53
- expect(result).toBe(true);
54
- });
55
-
56
- test("表不存在时返回 false", async () => {
57
- const mockSql = Object.assign(
58
- async function (_strings: TemplateStringsArray, ..._values: any[]) {
59
- return [{ count: 0 }];
60
- },
61
- {
62
- unsafe: async (_query: string) => []
63
- }
64
- );
65
-
66
- const result = await tableExists(mockSql, "nonexistent", "test_db");
67
- expect(result).toBe(false);
68
- });
69
- });
70
-
71
- describe("getTableColumns", () => {
72
- test("返回正确的列信息结构", async () => {
73
- const mockSql = Object.assign(
74
- async function (_strings: TemplateStringsArray, ..._values: any[]) {
75
- // 模拟 MySQL information_schema 返回
76
- return [
77
- {
78
- COLUMN_NAME: "id",
79
- DATA_TYPE: "bigint",
80
- CHARACTER_MAXIMUM_LENGTH: null,
81
- IS_NULLABLE: "NO",
82
- COLUMN_DEFAULT: null,
83
- COLUMN_COMMENT: "主键ID",
84
- COLUMN_TYPE: "bigint unsigned"
85
- },
86
- {
87
- COLUMN_NAME: "user_name",
88
- DATA_TYPE: "varchar",
89
- CHARACTER_MAXIMUM_LENGTH: 50,
90
- IS_NULLABLE: "NO",
91
- COLUMN_DEFAULT: "",
92
- COLUMN_COMMENT: "用户名",
93
- COLUMN_TYPE: "varchar(50)"
94
- },
95
- {
96
- COLUMN_NAME: "age",
97
- DATA_TYPE: "bigint",
98
- CHARACTER_MAXIMUM_LENGTH: null,
99
- IS_NULLABLE: "YES",
100
- COLUMN_DEFAULT: "0",
101
- COLUMN_COMMENT: "年龄",
102
- COLUMN_TYPE: "bigint"
103
- }
104
- ];
105
- },
106
- {
107
- unsafe: async (_query: string) => []
108
- }
109
- );
110
-
111
- const columns = await getTableColumns(mockSql, "user", "test_db");
112
-
113
- expect(columns.id).toBeDefined();
114
- expect(columns.id.type).toBe("bigint");
115
- expect(columns.id.nullable).toBe(false);
116
- expect(columns.id.comment).toBe("主键ID");
117
-
118
- expect(columns.user_name).toBeDefined();
119
- expect(columns.user_name.type).toBe("varchar");
120
- expect(columns.user_name.max).toBe(50);
121
- expect(columns.user_name.nullable).toBe(false);
122
- expect(columns.user_name.defaultValue).toBe("");
123
-
124
- expect(columns.age).toBeDefined();
125
- expect(columns.age.nullable).toBe(true);
126
- expect(columns.age.defaultValue).toBe("0");
127
- });
128
- });
129
-
130
- describe("getTableIndexes", () => {
131
- test("返回正确的索引信息结构", async () => {
132
- const mockSql = Object.assign(
133
- async function (_strings: TemplateStringsArray, ..._values: any[]) {
134
- // 模拟 MySQL information_schema.STATISTICS 返回
135
- // 注意:PRIMARY 索引被排除
136
- return [
137
- { INDEX_NAME: "idx_created_at", COLUMN_NAME: "created_at" },
138
- { INDEX_NAME: "idx_user_name", COLUMN_NAME: "user_name" }
139
- ];
140
- },
141
- {
142
- unsafe: async (_query: string) => []
143
- }
144
- );
145
-
146
- const indexes = await getTableIndexes(mockSql, "user", "test_db");
147
-
148
- // PRIMARY 索引被排除,不应存在
149
- expect(indexes.PRIMARY).toBeUndefined();
150
-
151
- expect(indexes.idx_created_at).toBeDefined();
152
- expect(indexes.idx_created_at).toContain("created_at");
153
-
154
- expect(indexes.idx_user_name).toBeDefined();
155
- expect(indexes.idx_user_name).toContain("user_name");
156
- });
157
-
158
- test("复合索引包含多个列", async () => {
159
- const mockSql = Object.assign(
160
- async function (_strings: TemplateStringsArray, ..._values: any[]) {
161
- // 模拟复合索引,同一索引名包含多个列
162
- return [
163
- { INDEX_NAME: "idx_composite", COLUMN_NAME: "user_id" },
164
- { INDEX_NAME: "idx_composite", COLUMN_NAME: "created_at" }
165
- ];
166
- },
167
- {
168
- unsafe: async (_query: string) => []
169
- }
170
- );
171
-
172
- const indexes = await getTableIndexes(mockSql, "user", "test_db");
173
-
174
- expect(indexes.idx_composite).toBeDefined();
175
- expect(indexes.idx_composite.length).toBe(2);
176
- expect(indexes.idx_composite).toContain("user_id");
177
- expect(indexes.idx_composite).toContain("created_at");
178
- });
179
- });
@@ -1,139 +0,0 @@
1
- /**
2
- * syncDb 类型处理模块测试
3
- *
4
- * 测试 types.ts 中的函数:
5
- * - isStringOrArrayType
6
- * - getSqlType
7
- * - resolveDefaultValue
8
- * - generateDefaultSql
9
- */
10
-
11
- import { describe, test, expect, beforeAll } from "bun:test";
12
-
13
- import { setDbType } from "../sync/syncDb/constants.js";
14
-
15
- // 设置数据库类型为 MySQL
16
- setDbType("mysql");
17
-
18
- // 动态导入以确保环境变量生效
19
- let isStringOrArrayType: any;
20
- let getSqlType: any;
21
- let resolveDefaultValue: any;
22
- let generateDefaultSql: any;
23
-
24
- beforeAll(async () => {
25
- const types = await import("../sync/syncDb/types.js");
26
- isStringOrArrayType = types.isStringOrArrayType;
27
- getSqlType = types.getSqlType;
28
- resolveDefaultValue = types.resolveDefaultValue;
29
- generateDefaultSql = types.generateDefaultSql;
30
- });
31
-
32
- describe("isStringOrArrayType", () => {
33
- test("string 类型返回 true", () => {
34
- expect(isStringOrArrayType("string")).toBe(true);
35
- });
36
-
37
- test("array_string 类型返回 true", () => {
38
- expect(isStringOrArrayType("array_string")).toBe(true);
39
- });
40
-
41
- test("number 类型返回 false", () => {
42
- expect(isStringOrArrayType("number")).toBe(false);
43
- });
44
-
45
- test("text 类型返回 false", () => {
46
- expect(isStringOrArrayType("text")).toBe(false);
47
- });
48
-
49
- test("array_text 类型返回 false", () => {
50
- expect(isStringOrArrayType("array_text")).toBe(false);
51
- });
52
- });
53
-
54
- describe("resolveDefaultValue", () => {
55
- test("null 值 + string 类型 => 空字符串", () => {
56
- expect(resolveDefaultValue(null, "string")).toBe("");
57
- });
58
-
59
- test("null 值 + number 类型 => 0", () => {
60
- expect(resolveDefaultValue(null, "number")).toBe(0);
61
- });
62
-
63
- test('"null" 字符串 + number 类型 => 0', () => {
64
- expect(resolveDefaultValue("null", "number")).toBe(0);
65
- });
66
-
67
- test('null 值 + array_string 类型 => "[]"', () => {
68
- expect(resolveDefaultValue(null, "array_string")).toBe("[]");
69
- });
70
-
71
- test('null 值 + text 类型 => "null"', () => {
72
- expect(resolveDefaultValue(null, "text")).toBe("null");
73
- });
74
-
75
- test('null 值 + array_text 类型 => "null"(TEXT 不支持默认值)', () => {
76
- expect(resolveDefaultValue(null, "array_text")).toBe("null");
77
- });
78
-
79
- test("有实际值时直接返回", () => {
80
- expect(resolveDefaultValue("admin", "string")).toBe("admin");
81
- expect(resolveDefaultValue(100, "number")).toBe(100);
82
- expect(resolveDefaultValue(0, "number")).toBe(0);
83
- });
84
- });
85
-
86
- describe("generateDefaultSql", () => {
87
- test("number 类型生成数字默认值", () => {
88
- expect(generateDefaultSql(0, "number")).toBe(" DEFAULT 0");
89
- expect(generateDefaultSql(100, "number")).toBe(" DEFAULT 100");
90
- });
91
-
92
- test("string 类型生成带引号默认值", () => {
93
- expect(generateDefaultSql("admin", "string")).toBe(" DEFAULT 'admin'");
94
- expect(generateDefaultSql("", "string")).toBe(" DEFAULT ''");
95
- });
96
-
97
- test("text 类型不生成默认值", () => {
98
- expect(generateDefaultSql("null", "text")).toBe("");
99
- });
100
-
101
- test("array_string 类型生成 JSON 数组默认值", () => {
102
- expect(generateDefaultSql("[]", "array_string")).toBe(" DEFAULT '[]'");
103
- });
104
-
105
- test("array_text 类型不生成默认值(MySQL TEXT 不支持)", () => {
106
- expect(generateDefaultSql("[]", "array_text")).toBe("");
107
- });
108
-
109
- test("单引号被正确转义", () => {
110
- expect(generateDefaultSql("it's", "string")).toBe(" DEFAULT 'it''s'");
111
- });
112
- });
113
-
114
- describe("getSqlType", () => {
115
- test("string 类型带长度", () => {
116
- const result = getSqlType("string", 100);
117
- expect(result).toBe("VARCHAR(100)");
118
- });
119
-
120
- test("array_string 类型带长度", () => {
121
- const result = getSqlType("array_string", 500);
122
- expect(result).toBe("VARCHAR(500)");
123
- });
124
-
125
- test("number 类型无符号", () => {
126
- const result = getSqlType("number", null, true);
127
- expect(result).toBe("BIGINT UNSIGNED");
128
- });
129
-
130
- test("number 类型有符号", () => {
131
- const result = getSqlType("number", null, false);
132
- expect(result).toBe("BIGINT");
133
- });
134
-
135
- test("text 类型", () => {
136
- const result = getSqlType("text", null);
137
- expect(result).toBe("MEDIUMTEXT");
138
- });
139
- });
@@ -1,90 +0,0 @@
1
- import fs from "node:fs";
2
- import { existsSync, statSync } from "node:fs";
3
-
4
- import { join } from "pathe";
5
-
6
- import { projectAddonsDir } from "../paths.js";
7
-
8
- /**
9
- * 扫描所有可用的 addon
10
- * 优先从本地 addons/ 目录加载,其次从 node_modules/@befly-addon/ 加载
11
- * @param cwd - 项目根目录,默认为 process.cwd()
12
- * @returns addon 名称数组
13
- */
14
- export const scanAddons = (cwd: string = process.cwd()): string[] => {
15
- const addons = new Set<string>();
16
-
17
- // 1. 扫描本地 addons 目录(优先级高)
18
- if (existsSync(projectAddonsDir)) {
19
- try {
20
- const localAddons = fs.readdirSync(projectAddonsDir).filter((name) => {
21
- const fullPath = join(projectAddonsDir, name);
22
- try {
23
- const stat = statSync(fullPath);
24
- return stat.isDirectory() && !name.startsWith("_");
25
- } catch {
26
- return false;
27
- }
28
- });
29
- localAddons.forEach((name) => addons.add(name));
30
- } catch {
31
- // 忽略本地目录读取错误
32
- }
33
- }
34
-
35
- // 2. 扫描 node_modules/@befly-addon 目录
36
- const beflyDir = join(cwd, "node_modules", "@befly-addon");
37
- if (existsSync(beflyDir)) {
38
- try {
39
- const npmAddons = fs.readdirSync(beflyDir).filter((name) => {
40
- // 如果本地已存在,跳过 npm 包版本
41
- if (addons.has(name)) return false;
42
-
43
- const fullPath = join(beflyDir, name);
44
- try {
45
- const stat = statSync(fullPath);
46
- return stat.isDirectory();
47
- } catch {
48
- return false;
49
- }
50
- });
51
- npmAddons.forEach((name) => addons.add(name));
52
- } catch {
53
- // 忽略 npm 目录读取错误
54
- }
55
- }
56
-
57
- return Array.from(addons).sort();
58
- };
59
-
60
- /**
61
- * 获取 addon 的指定子目录路径
62
- * 优先返回本地 addons 目录,其次返回 node_modules 目录
63
- * @param name - addon 名称
64
- * @param subDir - 子目录名称
65
- * @param cwd - 项目根目录,默认为 process.cwd()
66
- * @returns 完整路径
67
- */
68
- export const getAddonDir = (name: string, subDir: string, cwd: string = process.cwd()): string => {
69
- // 优先使用本地 addons 目录
70
- // const projectAddonsDir = join(cwd, 'addons');
71
- // const localPath = join(projectAddonsDir, name, subDir);
72
- // if (existsSync(localPath)) {
73
- // return localPath;
74
- // }
75
-
76
- // 降级使用 node_modules 目录
77
- return join(cwd, "node_modules", "@befly-addon", name, subDir);
78
- };
79
-
80
- /**
81
- * 检查 addon 子目录是否存在
82
- * @param name - addon 名称
83
- * @param subDir - 子目录名称
84
- * @param cwd - 项目根目录,默认为 process.cwd()
85
- * @returns 是否存在
86
- */
87
- export const addonDirExists = (name: string, subDir: string, cwd: string = process.cwd()): boolean => {
88
- const dir = getAddonDir(name, subDir, cwd);
89
- return existsSync(dir) && statSync(dir).isDirectory();
90
- };
package/utils/modules.ts DELETED
@@ -1,98 +0,0 @@
1
- import type { Hook } from "../types/hook.js";
2
- import type { Plugin } from "../types/plugin.js";
3
-
4
- import { existsSync } from "node:fs";
5
-
6
- import { camelCase } from "es-toolkit/string";
7
-
8
- import { Logger } from "../lib/logger.js";
9
- import { scanFiles } from "./scanFiles.js";
10
-
11
- /**
12
- * 扫描模块(插件或钩子)
13
- * @param dir - 目录路径
14
- * @param type - 模块类型(core/addon/app)
15
- * @param moduleLabel - 模块标签(如"插件"、"钩子")
16
- * @param addonName - 组件名称(仅 type='addon' 时需要)
17
- * @returns 模块列表
18
- */
19
- export async function scanModules<T extends Plugin | Hook>(dir: string, type: "core" | "addon" | "app", moduleLabel: string, addonName?: string): Promise<T[]> {
20
- if (!existsSync(dir)) return [];
21
-
22
- const items: T[] = [];
23
- const files = await scanFiles(dir, "*.ts");
24
-
25
- for (const { filePath, fileName } of files) {
26
- // 生成模块名称
27
- const name = camelCase(fileName);
28
- const moduleName = type === "core" ? name : type === "addon" ? `addon_${camelCase(addonName!)}_${name}` : `app_${name}`;
29
-
30
- try {
31
- const normalizedFilePath = filePath.replace(/\\/g, "/");
32
- const moduleImport = await import(normalizedFilePath);
33
- const item = moduleImport.default;
34
-
35
- item.name = moduleName;
36
- // 为 addon 模块记录 addon 名称
37
- if (type === "addon" && addonName) {
38
- item.addonName = addonName;
39
- }
40
- items.push(item);
41
- } catch (err: any) {
42
- const typeLabel = type === "core" ? "核心" : type === "addon" ? `组件${addonName}` : "项目";
43
- Logger.error({ err: err, module: fileName }, `${typeLabel}${moduleLabel} 导入失败`);
44
- process.exit(1);
45
- }
46
- }
47
-
48
- return items;
49
- }
50
-
51
- /**
52
- * 排序模块(根据依赖关系)
53
- * @param modules - 待排序的模块列表
54
- * @returns 排序后的模块列表,如果存在循环依赖或依赖不存在则返回 false
55
- */
56
- export function sortModules<T extends { name?: string; after?: string[] }>(modules: T[]): T[] | false {
57
- const result: T[] = [];
58
- const visited = new Set<string>();
59
- const visiting = new Set<string>();
60
- const moduleMap: Record<string, T> = Object.fromEntries(modules.map((m) => [m.name!, m]));
61
- let isPass = true;
62
-
63
- // 检查依赖是否存在
64
- for (const module of modules) {
65
- if (module.after) {
66
- for (const dep of module.after) {
67
- if (!moduleMap[dep]) {
68
- Logger.error({ module: module.name, dependency: dep }, "依赖的模块未找到");
69
- isPass = false;
70
- }
71
- }
72
- }
73
- }
74
-
75
- if (!isPass) return false;
76
-
77
- const visit = (name: string): void => {
78
- if (visited.has(name)) return;
79
- if (visiting.has(name)) {
80
- Logger.error({ module: name }, "模块循环依赖");
81
- isPass = false;
82
- return;
83
- }
84
-
85
- const module = moduleMap[name];
86
- if (!module) return;
87
-
88
- visiting.add(name);
89
- (module.after || []).forEach(visit);
90
- visiting.delete(name);
91
- visited.add(name);
92
- result.push(module);
93
- };
94
-
95
- modules.forEach((m) => visit(m.name!));
96
-
97
- return isPass ? result : false;
98
- }
package/utils/route.ts DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * 标准化接口权限路径(用于写入/读取权限缓存时保持一致)
3
- * 规则(简化版):method 大写;path 为空则为 '/';确保以 '/' 开头
4
- * 说明:当前框架内 api.path 来源于目录结构生成、请求侧使用 URL.pathname,默认不包含 query/hash,且不期望出现尾部斜杠等异常输入。
5
- */
6
- export function normalizeApiPath(method: string, path: string): string {
7
- const normalizedMethod = (method || "").toUpperCase();
8
- let normalizedPath = path || "/";
9
-
10
- if (!normalizedPath.startsWith("/")) {
11
- normalizedPath = "/" + normalizedPath;
12
- }
13
-
14
- return `${normalizedMethod}${normalizedPath}`;
15
- }
16
-
17
- /**
18
- * 生成路由 Key(用于路由匹配 / 权限缓存 / 日志等场景统一使用)
19
- * 格式:METHOD/path
20
- */
21
- export function makeRouteKey(method: string, pathname: string): string {
22
- return normalizeApiPath(method, pathname);
23
- }