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,238 +1,147 @@
1
1
  /**
2
2
  * DbHelper JOIN 功能测试
3
- * 测试多表联查相关功能(不支持表别名)
3
+ * 测试多表联查相关功能(推荐使用表别名)
4
4
  */
5
5
 
6
6
  import { describe, test, expect } from "bun:test";
7
7
 
8
- import { snakeCase } from "es-toolkit/string";
8
+ import { DbUtils } from "../lib/dbUtils.js";
9
9
 
10
- // ============================================
11
- // 辅助函数单元测试(模拟 DbHelper 私有方法)
12
- // ============================================
13
-
14
- /**
15
- * 处理表名(转下划线格式)
16
- */
17
- function processTableName(table: string): string {
18
- return snakeCase(table.trim());
19
- }
20
-
21
- /**
22
- * 处理联查字段(支持表名.字段名格式)
23
- */
24
- function processJoinField(field: string): string {
25
- if (field.includes("(") || field === "*" || field.startsWith("`")) {
26
- return field;
27
- }
28
-
29
- if (field.toUpperCase().includes(" AS ")) {
30
- const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
31
- return `${processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
32
- }
33
-
34
- if (field.includes(".")) {
35
- const [tableName, fieldName] = field.split(".");
36
- return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
37
- }
38
-
39
- return snakeCase(field);
40
- }
41
-
42
- /**
43
- * 处理联查 where 条件键名
44
- */
45
- function processJoinWhereKey(key: string): string {
46
- if (key === "$or" || key === "$and") {
47
- return key;
48
- }
49
-
50
- if (key.includes("$")) {
51
- const lastDollarIndex = key.lastIndexOf("$");
52
- const fieldPart = key.substring(0, lastDollarIndex);
53
- const operator = key.substring(lastDollarIndex);
54
-
55
- if (fieldPart.includes(".")) {
56
- const [tableName, fieldName] = fieldPart.split(".");
57
- return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
58
- }
59
- return `${snakeCase(fieldPart)}${operator}`;
60
- }
61
-
62
- if (key.includes(".")) {
63
- const [tableName, fieldName] = key.split(".");
64
- return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
65
- }
66
-
67
- return snakeCase(key);
68
- }
69
-
70
- /**
71
- * 递归处理联查 where 条件
72
- */
73
- function processJoinWhere(where: any): any {
74
- if (!where || typeof where !== "object") return where;
75
-
76
- if (Array.isArray(where)) {
77
- return where.map((item) => processJoinWhere(item));
78
- }
79
-
80
- const result: any = {};
81
- for (const [key, value] of Object.entries(where)) {
82
- const newKey = processJoinWhereKey(key);
83
-
84
- if (key === "$or" || key === "$and") {
85
- result[newKey] = (value as any[]).map((item) => processJoinWhere(item));
86
- } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
87
- result[newKey] = processJoinWhere(value);
88
- } else {
89
- result[newKey] = value;
90
- }
91
- }
92
- return result;
93
- }
10
+ describe("DbUtils tableRef - normalizeTableRef/getJoinMainQualifier", () => {
11
+ test("普通表名转下划线(无 alias)", () => {
12
+ expect(DbUtils.normalizeTableRef("userProfile")).toBe("user_profile");
13
+ expect(DbUtils.normalizeTableRef("orderDetail")).toBe("order_detail");
14
+ expect(DbUtils.normalizeTableRef("user")).toBe("user");
15
+ expect(DbUtils.normalizeTableRef("order")).toBe("order");
16
+ });
94
17
 
95
- /**
96
- * 处理联查 orderBy
97
- */
98
- function processJoinOrderBy(orderBy: string[]): string[] {
99
- if (!orderBy || !Array.isArray(orderBy)) return orderBy;
100
- return orderBy.map((item) => {
101
- if (typeof item !== "string" || !item.includes("#")) return item;
102
- const [field, direction] = item.split("#");
103
- return `${processJoinField(field.trim())}#${direction.trim()}`;
18
+ test("带 alias 的 tableRef:只转换 schema/table,不改 alias", () => {
19
+ expect(DbUtils.normalizeTableRef("UserProfile up")).toBe("user_profile up");
20
+ expect(DbUtils.normalizeTableRef("myDb.UserProfile up")).toBe("my_db.user_profile up");
104
21
  });
105
- }
106
-
107
- // ============================================
108
- // 测试用例
109
- // ============================================
110
-
111
- describe("DbHelper JOIN - processTableName", () => {
112
- test("普通表名转下划线", () => {
113
- expect(processTableName("userProfile")).toBe("user_profile");
114
- expect(processTableName("orderDetail")).toBe("order_detail");
115
- expect(processTableName("user")).toBe("user");
116
- expect(processTableName("order")).toBe("order");
22
+
23
+ test("JOIN 主限定符:优先 alias", () => {
24
+ expect(DbUtils.getJoinMainQualifier("order o")).toBe("o");
25
+ expect(DbUtils.getJoinMainQualifier("userProfile")).toBe("user_profile");
117
26
  });
118
27
  });
119
28
 
120
29
  describe("DbHelper JOIN - processJoinField", () => {
121
30
  test("带表名的字段", () => {
122
- expect(processJoinField("order.userId")).toBe("order.user_id");
123
- expect(processJoinField("user.userName")).toBe("user.user_name");
124
- expect(processJoinField("order.createdAt")).toBe("order.created_at");
31
+ expect(DbUtils.processJoinField("o.userId")).toBe("o.user_id");
32
+ expect(DbUtils.processJoinField("u.userName")).toBe("u.user_name");
33
+ expect(DbUtils.processJoinField("o.createdAt")).toBe("o.created_at");
125
34
  });
126
35
 
127
- test("表名也转下划线", () => {
128
- expect(processJoinField("orderDetail.productId")).toBe("order_detail.product_id");
129
- expect(processJoinField("userProfile.avatarUrl")).toBe("user_profile.avatar_url");
36
+ test("表/别名部分保持原样(JOIN 场景点号前通常是别名)", () => {
37
+ expect(DbUtils.processJoinField("orderDetail.productId")).toBe("orderDetail.product_id");
38
+ expect(DbUtils.processJoinField("userProfile.avatarUrl")).toBe("userProfile.avatar_url");
130
39
  });
131
40
 
132
41
  test("普通字段(无表名)", () => {
133
- expect(processJoinField("userName")).toBe("user_name");
134
- expect(processJoinField("createdAt")).toBe("created_at");
42
+ expect(DbUtils.processJoinField("userName")).toBe("user_name");
43
+ expect(DbUtils.processJoinField("createdAt")).toBe("created_at");
135
44
  });
136
45
 
137
46
  test("带 AS 别名的字段", () => {
138
- expect(processJoinField("order.totalAmount AS total")).toBe("order.total_amount AS total");
139
- expect(processJoinField("user.userName AS name")).toBe("user.user_name AS name");
140
- expect(processJoinField("product.name AS productName")).toBe("product.name AS productName");
47
+ expect(DbUtils.processJoinField("o.totalAmount AS total")).toBe("o.total_amount AS total");
48
+ expect(DbUtils.processJoinField("u.userName AS name")).toBe("u.user_name AS name");
49
+ expect(DbUtils.processJoinField("p.name AS productName")).toBe("p.name AS productName");
141
50
  });
142
51
 
143
52
  test("函数字段保持原样", () => {
144
- expect(processJoinField("COUNT(*)")).toBe("COUNT(*)");
145
- expect(processJoinField("SUM(order.amount)")).toBe("SUM(order.amount)");
53
+ expect(DbUtils.processJoinField("COUNT(*)")).toBe("COUNT(*)");
54
+ expect(DbUtils.processJoinField("SUM(o.amount)")).toBe("SUM(o.amount)");
146
55
  });
147
56
 
148
57
  test("星号保持原样", () => {
149
- expect(processJoinField("*")).toBe("*");
58
+ expect(DbUtils.processJoinField("*")).toBe("*");
150
59
  });
151
60
 
152
61
  test("已转义字段保持原样", () => {
153
- expect(processJoinField("`order`")).toBe("`order`");
62
+ expect(DbUtils.processJoinField("`order`")).toBe("`order`");
154
63
  });
155
64
  });
156
65
 
157
66
  describe("DbHelper JOIN - processJoinWhereKey", () => {
158
67
  test("带表名的字段名", () => {
159
- expect(processJoinWhereKey("order.userId")).toBe("order.user_id");
160
- expect(processJoinWhereKey("user.userName")).toBe("user.user_name");
68
+ expect(DbUtils.processJoinWhereKey("o.userId")).toBe("o.user_id");
69
+ expect(DbUtils.processJoinWhereKey("u.userName")).toBe("u.user_name");
161
70
  });
162
71
 
163
72
  test("带表名和操作符的字段名", () => {
164
- expect(processJoinWhereKey("order.createdAt$gt")).toBe("order.created_at$gt");
165
- expect(processJoinWhereKey("user.status$in")).toBe("user.status$in");
166
- expect(processJoinWhereKey("order.amount$gte")).toBe("order.amount$gte");
73
+ expect(DbUtils.processJoinWhereKey("o.createdAt$gt")).toBe("o.created_at$gt");
74
+ expect(DbUtils.processJoinWhereKey("u.status$in")).toBe("u.status$in");
75
+ expect(DbUtils.processJoinWhereKey("o.amount$gte")).toBe("o.amount$gte");
167
76
  });
168
77
 
169
78
  test("普通字段带操作符", () => {
170
- expect(processJoinWhereKey("createdAt$gt")).toBe("created_at$gt");
171
- expect(processJoinWhereKey("userId$ne")).toBe("user_id$ne");
79
+ expect(DbUtils.processJoinWhereKey("createdAt$gt")).toBe("created_at$gt");
80
+ expect(DbUtils.processJoinWhereKey("userId$ne")).toBe("user_id$ne");
172
81
  });
173
82
 
174
83
  test("逻辑操作符保持原样", () => {
175
- expect(processJoinWhereKey("$or")).toBe("$or");
176
- expect(processJoinWhereKey("$and")).toBe("$and");
84
+ expect(DbUtils.processJoinWhereKey("$or")).toBe("$or");
85
+ expect(DbUtils.processJoinWhereKey("$and")).toBe("$and");
177
86
  });
178
87
  });
179
88
 
180
89
  describe("DbHelper JOIN - processJoinWhere", () => {
181
90
  test("简单条件", () => {
182
- const where = { "order.userId": 1, "order.state": 1 };
183
- const result = processJoinWhere(where);
184
- expect(result).toEqual({ "order.user_id": 1, "order.state": 1 });
91
+ const where = { "o.userId": 1, "o.state": 1 };
92
+ const result = DbUtils.processJoinWhere(where);
93
+ expect(result).toEqual({ "o.user_id": 1, "o.state": 1 });
185
94
  });
186
95
 
187
96
  test("带操作符的条件", () => {
188
- const where = { "order.createdAt$gt": 1000, "user.state$ne": 0 };
189
- const result = processJoinWhere(where);
190
- expect(result).toEqual({ "order.created_at$gt": 1000, "user.state$ne": 0 });
97
+ const where = { "o.createdAt$gt": 1000, "u.state$ne": 0 };
98
+ const result = DbUtils.processJoinWhere(where);
99
+ expect(result).toEqual({ "o.created_at$gt": 1000, "u.state$ne": 0 });
191
100
  });
192
101
 
193
102
  test("$or 条件", () => {
194
103
  const where = {
195
- $or: [{ "user.userName$like": "%test%" }, { "user.email$like": "%test%" }]
104
+ $or: [{ "u.userName$like": "%test%" }, { "u.email$like": "%test%" }]
196
105
  };
197
- const result = processJoinWhere(where);
106
+ const result = DbUtils.processJoinWhere(where);
198
107
  expect(result).toEqual({
199
- $or: [{ "user.user_name$like": "%test%" }, { "user.email$like": "%test%" }]
108
+ $or: [{ "u.user_name$like": "%test%" }, { "u.email$like": "%test%" }]
200
109
  });
201
110
  });
202
111
 
203
112
  test("复杂嵌套条件", () => {
204
113
  const where = {
205
- "order.state": 1,
206
- "user.state": 1,
207
- $or: [{ "user.userName$like": "%test%" }, { "product.name$like": "%test%" }],
208
- "order.createdAt$gte": 1000
114
+ "o.state": 1,
115
+ "u.state": 1,
116
+ $or: [{ "u.userName$like": "%test%" }, { "p.name$like": "%test%" }],
117
+ "o.createdAt$gte": 1000
209
118
  };
210
- const result = processJoinWhere(where);
119
+ const result = DbUtils.processJoinWhere(where);
211
120
  expect(result).toEqual({
212
- "order.state": 1,
213
- "user.state": 1,
214
- $or: [{ "user.user_name$like": "%test%" }, { "product.name$like": "%test%" }],
215
- "order.created_at$gte": 1000
121
+ "o.state": 1,
122
+ "u.state": 1,
123
+ $or: [{ "u.user_name$like": "%test%" }, { "p.name$like": "%test%" }],
124
+ "o.created_at$gte": 1000
216
125
  });
217
126
  });
218
127
  });
219
128
 
220
129
  describe("DbHelper JOIN - processJoinOrderBy", () => {
221
130
  test("带表名的排序", () => {
222
- const orderBy = ["order.createdAt#DESC", "user.userName#ASC"];
223
- const result = processJoinOrderBy(orderBy);
224
- expect(result).toEqual(["order.created_at#DESC", "user.user_name#ASC"]);
131
+ const orderBy = ["o.createdAt#DESC", "u.userName#ASC"];
132
+ const result = DbUtils.processJoinOrderBy(orderBy);
133
+ expect(result).toEqual(["o.created_at#DESC", "u.user_name#ASC"]);
225
134
  });
226
135
 
227
136
  test("普通排序", () => {
228
137
  const orderBy = ["createdAt#DESC"];
229
- const result = processJoinOrderBy(orderBy);
138
+ const result = DbUtils.processJoinOrderBy(orderBy);
230
139
  expect(result).toEqual(["created_at#DESC"]);
231
140
  });
232
141
 
233
142
  test("无排序方向的保持原样", () => {
234
143
  const orderBy = ["id"];
235
- const result = processJoinOrderBy(orderBy);
144
+ const result = DbUtils.processJoinOrderBy(orderBy);
236
145
  expect(result).toEqual(["id"]);
237
146
  });
238
147
  });
@@ -264,49 +173,49 @@ describe("DbHelper JOIN - 完整场景模拟", () => {
264
173
  test("订单列表联查参数处理", () => {
265
174
  // 模拟输入
266
175
  const options = {
267
- table: "order",
176
+ table: "order o",
268
177
  joins: [
269
- { table: "user", on: "order.userId = user.id" },
270
- { table: "product", on: "order.productId = product.id" }
178
+ { table: "user u", on: "o.user_id = u.id" },
179
+ { table: "product p", on: "o.product_id = p.id" }
271
180
  ],
272
- fields: ["order.id", "order.totalAmount", "user.userName", "product.name AS productName"],
181
+ fields: ["o.id", "o.totalAmount", "u.userName", "p.name AS productName"],
273
182
  where: {
274
- "order.state": 1,
275
- "user.state": 1,
276
- "order.createdAt$gte": 1701388800000
183
+ "o.state": 1,
184
+ "u.state": 1,
185
+ "o.createdAt$gte": 1701388800000
277
186
  },
278
- orderBy: ["order.createdAt#DESC"]
187
+ orderBy: ["o.createdAt#DESC"]
279
188
  };
280
189
 
281
- // 处理表名
282
- const processedTable = processTableName(options.table);
283
- expect(processedTable).toBe("order");
190
+ // 处理表名(tableRef 规范化)
191
+ const processedTable = DbUtils.normalizeTableRef(options.table);
192
+ expect(processedTable).toBe("order o");
284
193
 
285
194
  // 处理字段
286
- const processedFields = options.fields.map((f) => processJoinField(f));
287
- expect(processedFields).toEqual(["order.id", "order.total_amount", "user.user_name", "product.name AS productName"]);
195
+ const processedFields = options.fields.map((f) => DbUtils.processJoinField(f));
196
+ expect(processedFields).toEqual(["o.id", "o.total_amount", "u.user_name", "p.name AS productName"]);
288
197
 
289
198
  // 处理 where
290
- const processedWhere = processJoinWhere(options.where);
199
+ const processedWhere = DbUtils.processJoinWhere(options.where);
291
200
  expect(processedWhere).toEqual({
292
- "order.state": 1,
293
- "user.state": 1,
294
- "order.created_at$gte": 1701388800000
201
+ "o.state": 1,
202
+ "u.state": 1,
203
+ "o.created_at$gte": 1701388800000
295
204
  });
296
205
 
297
206
  // 处理 orderBy
298
- const processedOrderBy = processJoinOrderBy(options.orderBy);
299
- expect(processedOrderBy).toEqual(["order.created_at#DESC"]);
207
+ const processedOrderBy = DbUtils.processJoinOrderBy(options.orderBy);
208
+ expect(processedOrderBy).toEqual(["o.created_at#DESC"]);
300
209
 
301
210
  // 处理 joins
302
211
  const processedJoins = options.joins.map((j) => ({
303
212
  type: (j as any).type || "left",
304
- table: processTableName(j.table),
213
+ table: DbUtils.normalizeTableRef(j.table),
305
214
  on: j.on
306
215
  }));
307
216
  expect(processedJoins).toEqual([
308
- { type: "left", table: "user", on: "order.userId = user.id" },
309
- { type: "left", table: "product", on: "order.productId = product.id" }
217
+ { type: "left", table: "user u", on: "o.user_id = u.id" },
218
+ { type: "left", table: "product p", on: "o.product_id = p.id" }
310
219
  ]);
311
220
  });
312
221
  });
@@ -0,0 +1,3 @@
1
+ export default {
2
+ name: "B"
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ name: "A"
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ name: "B"
3
+ };
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { loadPlugins } from "../loader/loadPlugins.js";
4
+
5
+ describe("loadPlugins - order smoke", () => {
6
+ test("deps 应确保 redis 在 db/cache 之前初始化", async () => {
7
+ const executed: string[] = [];
8
+ const ctx: any = {};
9
+
10
+ const plugins: any[] = [
11
+ {
12
+ moduleName: "db",
13
+ deps: ["logger", "redis"],
14
+ handler: async (befly: any) => {
15
+ executed.push("db");
16
+
17
+ if (!befly.redis) {
18
+ throw new Error("db handler called before redis");
19
+ }
20
+
21
+ return { ok: true };
22
+ }
23
+ },
24
+ {
25
+ moduleName: "cache",
26
+ deps: ["logger", "redis", "db"],
27
+ handler: async (befly: any) => {
28
+ executed.push("cache");
29
+
30
+ if (!befly.redis) {
31
+ throw new Error("cache handler called before redis");
32
+ }
33
+
34
+ if (!befly.db) {
35
+ throw new Error("cache handler called before db");
36
+ }
37
+
38
+ return { ok: true };
39
+ }
40
+ },
41
+ {
42
+ moduleName: "redis",
43
+ deps: ["logger"],
44
+ handler: async (_befly: any) => {
45
+ executed.push("redis");
46
+ return { ok: true };
47
+ }
48
+ },
49
+ {
50
+ moduleName: "logger",
51
+ deps: [],
52
+ handler: async (_befly: any) => {
53
+ executed.push("logger");
54
+ return { ok: true };
55
+ }
56
+ }
57
+ ];
58
+
59
+ await loadPlugins(plugins as any, ctx as any, []);
60
+
61
+ const loggerIndex = executed.indexOf("logger");
62
+ const redisIndex = executed.indexOf("redis");
63
+ const dbIndex = executed.indexOf("db");
64
+ const cacheIndex = executed.indexOf("cache");
65
+
66
+ expect(loggerIndex).toBeGreaterThanOrEqual(0);
67
+ expect(redisIndex).toBeGreaterThanOrEqual(0);
68
+ expect(dbIndex).toBeGreaterThanOrEqual(0);
69
+ expect(cacheIndex).toBeGreaterThanOrEqual(0);
70
+
71
+ expect(loggerIndex).toBeLessThan(redisIndex);
72
+ expect(redisIndex).toBeLessThan(dbIndex);
73
+ expect(dbIndex).toBeLessThan(cacheIndex);
74
+ });
75
+ });
@@ -145,7 +145,7 @@ describe("Logger - AsyncLocalStorage 注入", () => {
145
145
  {
146
146
  requestId: "rid_1",
147
147
  method: "POST",
148
- route: "POST/api/test",
148
+ route: "/api/test",
149
149
  ip: "127.0.0.1",
150
150
  now: 123,
151
151
  userId: 9,
@@ -160,7 +160,7 @@ describe("Logger - AsyncLocalStorage 注入", () => {
160
160
  expect(calls.length).toBe(1);
161
161
  expect(calls[0].args[0].requestId).toBe("rid_1");
162
162
  expect(calls[0].args[0].method).toBe("POST");
163
- expect(calls[0].args[0].route).toBe("POST/api/test");
163
+ expect(calls[0].args[0].route).toBe("/api/test");
164
164
  expect(calls[0].args[0].userId).toBe(9);
165
165
  expect(typeof calls[0].args[0].durationSinceNowMs).toBe("number");
166
166
  expect(calls[0].args[0].durationSinceNowMs).toBeGreaterThanOrEqual(0);
@@ -190,7 +190,7 @@ describe("Logger - AsyncLocalStorage 注入", () => {
190
190
  {
191
191
  requestId: "rid_2",
192
192
  method: "POST",
193
- route: "POST/api/test",
193
+ route: "/api/test",
194
194
  ip: "127.0.0.1",
195
195
  now: 456
196
196
  },
@@ -202,7 +202,7 @@ describe("Logger - AsyncLocalStorage 注入", () => {
202
202
 
203
203
  expect(calls.length).toBe(1);
204
204
  expect(calls[0].args[0].requestId).toBe("explicit");
205
- expect(calls[0].args[0].route).toBe("POST/api/test");
205
+ expect(calls[0].args[0].route).toBe("/api/test");
206
206
  expect(calls[0].args[0].foo).toBe(1);
207
207
  expect(calls[0].args[1]).toBe("m");
208
208
  });
@@ -230,7 +230,7 @@ describe("Logger - AsyncLocalStorage 注入", () => {
230
230
  {
231
231
  requestId: "rid_3",
232
232
  method: "POST",
233
- route: "POST/api/test",
233
+ route: "/api/test",
234
234
  ip: "127.0.0.1",
235
235
  now: 789
236
236
  },
@@ -276,7 +276,7 @@ describe("Logger - AsyncLocalStorage 注入", () => {
276
276
  {
277
277
  requestId: "rid_trim",
278
278
  method: "POST",
279
- route: "POST/api/test",
279
+ route: "/api/test",
280
280
  ip: "127.0.0.1",
281
281
  now: 1
282
282
  },
@@ -12,7 +12,12 @@ let redis: RedisHelper;
12
12
 
13
13
  beforeAll(async () => {
14
14
  // 连接 Redis
15
- await Connect.connectRedis();
15
+ await Connect.connectRedis({
16
+ host: "127.0.0.1",
17
+ port: 6379,
18
+ db: 0,
19
+ prefix: "befly:"
20
+ });
16
21
  redis = new RedisHelper();
17
22
  });
18
23
 
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { scanFiles } from "../utils/scanFiles.js";
5
+
6
+ describe("scanFiles - api routePath formatting", () => {
7
+ test("routePrefix 应为 /core|/app|/addon 且 routePath 不应出现 /api//", async () => {
8
+ const fixturesDir = fileURLToPath(new URL("./fixtures/scanFilesApis", import.meta.url));
9
+ const addonApisDir = fileURLToPath(new URL("./fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis", import.meta.url));
10
+
11
+ const coreApis = await scanFiles(fixturesDir, "core", "api", "**/*.ts");
12
+ const appApis = await scanFiles(fixturesDir, "app", "api", "**/*.ts");
13
+ const addonApis = await scanFiles(addonApisDir, "addon", "api", "**/*.ts");
14
+
15
+ const all = ([] as any[]).concat(coreApis as any, appApis as any, addonApis as any);
16
+ expect(all.length).toBeGreaterThan(0);
17
+
18
+ for (const api of all) {
19
+ expect(typeof api.routePrefix).toBe("string");
20
+ expect(typeof api.routePath).toBe("string");
21
+
22
+ if (api.source === "addon") {
23
+ expect(api.routePrefix.startsWith("/addon/")).toBe(true);
24
+ expect(typeof api.addonName).toBe("string");
25
+ expect(api.addonName.length > 0).toBe(true);
26
+ expect(api.routePrefix).toBe(`/addon/${api.addonName}`);
27
+ } else {
28
+ expect(["/core", "/app"].includes(api.routePrefix)).toBe(true);
29
+ }
30
+ expect(api.routePath.includes("/api//")).toBe(false);
31
+ }
32
+
33
+ const coreB = (coreApis as any[]).find((item) => item.relativePath === "sub/b");
34
+ expect(coreB.routePrefix).toBe("/core");
35
+ expect(coreB.routePath).toBe("/api/core/sub/b");
36
+
37
+ const appB = (appApis as any[]).find((item) => item.relativePath === "sub/b");
38
+ expect(appB.routePrefix).toBe("/app");
39
+ expect(appB.routePath).toBe("/api/app/sub/b");
40
+
41
+ const addonB = (addonApis as any[]).find((item) => item.relativePath === "sub/b");
42
+ expect(addonB.addonName).toBe("demo");
43
+ expect(addonB.routePrefix).toBe("/addon/demo");
44
+ expect(addonB.routePath).toBe("/api/addon/demo/sub/b");
45
+ });
46
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { SqlBuilder } from "../lib/sqlBuilder.js";
4
+
5
+ describe("smoke - sql", () => {
6
+ test("SqlBuilder: should build a simple SELECT with params", () => {
7
+ const qb = new SqlBuilder();
8
+
9
+ const result = qb.select(["id", "name"]).from("users").where({ id: 123 }).limit(10).offset(20).toSelectSql();
10
+
11
+ expect(result).toBeDefined();
12
+ expect(typeof result.sql).toBe("string");
13
+ expect(Array.isArray(result.params)).toBe(true);
14
+
15
+ // 关键特征:有 SELECT/FROM/WHERE/LIMIT,且参数化生效
16
+ expect(result.sql).toContain("SELECT `id`, `name` FROM `users`");
17
+ expect(result.sql).toContain("WHERE `id` = ?");
18
+ expect(result.sql).toContain("LIMIT 10 OFFSET 20");
19
+ expect(result.params).toEqual([123]);
20
+
21
+ // 基础安全性:确保 where 使用占位符,而不是把值直接拼进 SQL
22
+ expect(result.sql).not.toContain("123");
23
+ });
24
+ });