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
|
@@ -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 {
|
|
8
|
+
import { DbUtils } from "../lib/dbUtils.js";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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("
|
|
123
|
-
expect(processJoinField("
|
|
124
|
-
expect(processJoinField("
|
|
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("
|
|
129
|
-
expect(processJoinField("userProfile.avatarUrl")).toBe("
|
|
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("
|
|
139
|
-
expect(processJoinField("
|
|
140
|
-
expect(processJoinField("
|
|
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(
|
|
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("
|
|
160
|
-
expect(processJoinWhereKey("
|
|
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("
|
|
165
|
-
expect(processJoinWhereKey("
|
|
166
|
-
expect(processJoinWhereKey("
|
|
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 = { "
|
|
183
|
-
const result = processJoinWhere(where);
|
|
184
|
-
expect(result).toEqual({ "
|
|
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 = { "
|
|
189
|
-
const result = processJoinWhere(where);
|
|
190
|
-
expect(result).toEqual({ "
|
|
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: [{ "
|
|
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: [{ "
|
|
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
|
-
"
|
|
206
|
-
"
|
|
207
|
-
$or: [{ "
|
|
208
|
-
"
|
|
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
|
-
"
|
|
213
|
-
"
|
|
214
|
-
$or: [{ "
|
|
215
|
-
"
|
|
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 = ["
|
|
223
|
-
const result = processJoinOrderBy(orderBy);
|
|
224
|
-
expect(result).toEqual(["
|
|
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: "
|
|
270
|
-
{ table: "product", on: "
|
|
178
|
+
{ table: "user u", on: "o.user_id = u.id" },
|
|
179
|
+
{ table: "product p", on: "o.product_id = p.id" }
|
|
271
180
|
],
|
|
272
|
-
fields: ["
|
|
181
|
+
fields: ["o.id", "o.totalAmount", "u.userName", "p.name AS productName"],
|
|
273
182
|
where: {
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
183
|
+
"o.state": 1,
|
|
184
|
+
"u.state": 1,
|
|
185
|
+
"o.createdAt$gte": 1701388800000
|
|
277
186
|
},
|
|
278
|
-
orderBy: ["
|
|
187
|
+
orderBy: ["o.createdAt#DESC"]
|
|
279
188
|
};
|
|
280
189
|
|
|
281
|
-
//
|
|
282
|
-
const processedTable =
|
|
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(["
|
|
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
|
-
"
|
|
293
|
-
"
|
|
294
|
-
"
|
|
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(["
|
|
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:
|
|
213
|
+
table: DbUtils.normalizeTableRef(j.table),
|
|
305
214
|
on: j.on
|
|
306
215
|
}));
|
|
307
216
|
expect(processedJoins).toEqual([
|
|
308
|
-
{ type: "left", table: "user", on: "
|
|
309
|
-
{ type: "left", table: "product", on: "
|
|
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,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
|
+
});
|
package/tests/logger.test.ts
CHANGED
|
@@ -145,7 +145,7 @@ describe("Logger - AsyncLocalStorage 注入", () => {
|
|
|
145
145
|
{
|
|
146
146
|
requestId: "rid_1",
|
|
147
147
|
method: "POST",
|
|
148
|
-
route: "
|
|
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("
|
|
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: "
|
|
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("
|
|
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: "
|
|
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: "
|
|
279
|
+
route: "/api/test",
|
|
280
280
|
ip: "127.0.0.1",
|
|
281
281
|
now: 1
|
|
282
282
|
},
|
|
@@ -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
|
+
});
|