befly 3.10.1 → 3.10.3
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/.gitignore +0 -0
- package/configs/presetFields.ts +10 -0
- package/configs/presetRegexp.ts +225 -0
- package/package.json +15 -16
- package/tests/_mocks/mockSqliteDb.ts +0 -204
- package/tests/addonHelper-cache.test.ts +0 -32
- package/tests/api-integration-array-number.test.ts +0 -282
- package/tests/apiHandler-routePath-only.test.ts +0 -32
- package/tests/befly-config-env.test.ts +0 -78
- package/tests/cacheHelper.test.ts +0 -323
- package/tests/cacheKeys.test.ts +0 -41
- package/tests/checkApi-routePath-strict.test.ts +0 -166
- package/tests/checkMenu.test.ts +0 -346
- package/tests/checkTable-smoke.test.ts +0 -157
- package/tests/cipher.test.ts +0 -249
- package/tests/dbDialect-cache.test.ts +0 -23
- package/tests/dbDialect.test.ts +0 -46
- package/tests/dbHelper-advanced.test.ts +0 -723
- package/tests/dbHelper-all-array-types.test.ts +0 -316
- package/tests/dbHelper-array-serialization.test.ts +0 -258
- package/tests/dbHelper-batch-write.test.ts +0 -90
- package/tests/dbHelper-columns.test.ts +0 -234
- package/tests/dbHelper-execute.test.ts +0 -187
- package/tests/dbHelper-joins.test.ts +0 -221
- package/tests/fields-redis-cache.test.ts +0 -127
- package/tests/fields-validate.test.ts +0 -99
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +0 -3
- package/tests/fixtures/scanFilesApis/a.ts +0 -3
- package/tests/fixtures/scanFilesApis/sub/b.ts +0 -3
- package/tests/getClientIp.test.ts +0 -54
- package/tests/integration.test.ts +0 -189
- package/tests/jwt.test.ts +0 -65
- package/tests/loadPlugins-order-smoke.test.ts +0 -75
- package/tests/logger.test.ts +0 -325
- package/tests/redisHelper.test.ts +0 -495
- package/tests/redisKeys.test.ts +0 -9
- package/tests/scanConfig.test.ts +0 -144
- package/tests/scanFiles-routePath.test.ts +0 -46
- package/tests/smoke-sql.test.ts +0 -24
- package/tests/sqlBuilder-advanced.test.ts +0 -608
- package/tests/sqlBuilder.test.ts +0 -209
- package/tests/sync-connection.test.ts +0 -183
- package/tests/sync-init-guard.test.ts +0 -105
- package/tests/syncApi-insBatch-fields-consistent.test.ts +0 -61
- package/tests/syncApi-obsolete-records.test.ts +0 -69
- package/tests/syncApi-type-compat.test.ts +0 -72
- package/tests/syncDev-permissions.test.ts +0 -81
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +0 -88
- package/tests/syncMenu-duplicate-path.test.ts +0 -122
- package/tests/syncMenu-obsolete-records.test.ts +0 -161
- package/tests/syncMenu-parentPath-from-tree.test.ts +0 -75
- package/tests/syncMenu-paths.test.ts +0 -59
- package/tests/syncTable-apply.test.ts +0 -279
- package/tests/syncTable-array-number.test.ts +0 -160
- package/tests/syncTable-constants.test.ts +0 -101
- package/tests/syncTable-db-integration.test.ts +0 -237
- package/tests/syncTable-ddl.test.ts +0 -245
- package/tests/syncTable-helpers.test.ts +0 -99
- package/tests/syncTable-schema.test.ts +0 -99
- package/tests/syncTable-testkit.test.ts +0 -25
- package/tests/syncTable-types.test.ts +0 -122
- package/tests/tableRef-and-deserialize.test.ts +0 -67
- package/tests/util.test.ts +0 -100
- package/tests/validator-array-number.test.ts +0 -310
- package/tests/validator-default.test.ts +0 -373
- package/tests/validator.test.ts +0 -679
package/tests/smoke-sql.test.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,608 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SqlBuilder 高级测试用例
|
|
3
|
-
* 测试复杂 WHERE 条件、边界条件、SQL 注入防护
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, test, expect } from "bun:test";
|
|
7
|
-
|
|
8
|
-
import { SqlBuilder } from "../lib/sqlBuilder";
|
|
9
|
-
|
|
10
|
-
describe("SqlBuilder - WHERE 操作符完整测试", () => {
|
|
11
|
-
test("$in - 空数组应抛出错误", () => {
|
|
12
|
-
// **已修复**:$in 传入空数组 [] 会抛出错误
|
|
13
|
-
// 空数组会导致查询永远不匹配任何记录
|
|
14
|
-
|
|
15
|
-
const builder = new SqlBuilder();
|
|
16
|
-
|
|
17
|
-
expect(() => {
|
|
18
|
-
builder.select(["*"]).from("users").where({ id$in: [] }).toSelectSql();
|
|
19
|
-
}).toThrow("$in 操作符的数组不能为空");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("$in - 单个值应生成正确的 SQL", () => {
|
|
23
|
-
const builder = new SqlBuilder();
|
|
24
|
-
const result = builder
|
|
25
|
-
.select(["*"])
|
|
26
|
-
.from("users")
|
|
27
|
-
.where({ id$in: [1] })
|
|
28
|
-
.toSelectSql();
|
|
29
|
-
|
|
30
|
-
expect(result.sql).toContain("IN (?)");
|
|
31
|
-
expect(result.params).toEqual([1]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("$in - 多个值应生成多个占位符", () => {
|
|
35
|
-
const builder = new SqlBuilder();
|
|
36
|
-
const result = builder
|
|
37
|
-
.select(["*"])
|
|
38
|
-
.from("users")
|
|
39
|
-
.where({ id$in: [1, 2, 3, 4, 5] })
|
|
40
|
-
.toSelectSql();
|
|
41
|
-
|
|
42
|
-
expect(result.sql).toContain("IN (?,?,?,?,?)");
|
|
43
|
-
expect(result.params).toEqual([1, 2, 3, 4, 5]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("$notIn - 应生成 NOT IN 语句", () => {
|
|
47
|
-
const builder = new SqlBuilder();
|
|
48
|
-
const result = builder
|
|
49
|
-
.select(["*"])
|
|
50
|
-
.from("users")
|
|
51
|
-
.where({ status$notIn: ["banned", "deleted"] })
|
|
52
|
-
.toSelectSql();
|
|
53
|
-
|
|
54
|
-
expect(result.sql).toContain("NOT IN");
|
|
55
|
-
expect(result.params).toEqual(["banned", "deleted"]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("$between - 应生成 BETWEEN AND 语句", () => {
|
|
59
|
-
const builder = new SqlBuilder();
|
|
60
|
-
const result = builder
|
|
61
|
-
.select(["*"])
|
|
62
|
-
.from("users")
|
|
63
|
-
.where({ age$between: [18, 65] })
|
|
64
|
-
.toSelectSql();
|
|
65
|
-
|
|
66
|
-
expect(result.sql).toContain("BETWEEN ? AND ?");
|
|
67
|
-
expect(result.params).toEqual([18, 65]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("$between - 非数组值应如何处理", () => {
|
|
71
|
-
// **问题**:$between 应该接收 [min, max]
|
|
72
|
-
// 如果传入非数组,应该报错还是忽略?
|
|
73
|
-
|
|
74
|
-
const builder = new SqlBuilder();
|
|
75
|
-
const result = builder
|
|
76
|
-
.select(["*"])
|
|
77
|
-
.from("users")
|
|
78
|
-
.where({ age$between: 18 as any })
|
|
79
|
-
.toSelectSql();
|
|
80
|
-
|
|
81
|
-
// 当前实现:非数组不会生成 BETWEEN 条件(保持为其它 where 逻辑输出)
|
|
82
|
-
expect(typeof result.sql).toBe("string");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("$between - 数组长度不为 2 应如何处理", () => {
|
|
86
|
-
const builder = new SqlBuilder();
|
|
87
|
-
const result = builder
|
|
88
|
-
.select(["*"])
|
|
89
|
-
.from("users")
|
|
90
|
-
.where({ age$between: [18] as any })
|
|
91
|
-
.toSelectSql();
|
|
92
|
-
|
|
93
|
-
// **建议**:未来可验证数组长度必须为 2
|
|
94
|
-
expect(typeof result.sql).toBe("string");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("$null - 应生成 IS NULL 语句", () => {
|
|
98
|
-
const builder = new SqlBuilder();
|
|
99
|
-
const result = builder.select(["*"]).from("users").where({ deletedAt$null: true }).toSelectSql();
|
|
100
|
-
|
|
101
|
-
expect(result.sql).toContain("IS NULL");
|
|
102
|
-
expect(result.params.length).toBe(0); // IS NULL 不需要参数
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("$notNull - 应生成 IS NOT NULL 语句", () => {
|
|
106
|
-
const builder = new SqlBuilder();
|
|
107
|
-
const result = builder.select(["*"]).from("users").where({ email$notNull: true }).toSelectSql();
|
|
108
|
-
|
|
109
|
-
expect(result.sql).toContain("IS NOT NULL");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("$like - 应生成 LIKE 语句", () => {
|
|
113
|
-
const builder = new SqlBuilder();
|
|
114
|
-
const result = builder.select(["*"]).from("users").where({ name$like: "%john%" }).toSelectSql();
|
|
115
|
-
|
|
116
|
-
expect(result.sql).toContain("LIKE ?");
|
|
117
|
-
expect(result.params).toEqual(["%john%"]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("$like - 通配符应由用户控制", () => {
|
|
121
|
-
// 用户应自己添加 %
|
|
122
|
-
const builder = new SqlBuilder();
|
|
123
|
-
const result = builder.select(["*"]).from("users").where({ name$like: "john" }).toSelectSql();
|
|
124
|
-
|
|
125
|
-
// 不应自动添加 %
|
|
126
|
-
expect(result.params).toEqual(["john"]);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("比较操作符 - 应生成正确的符号", () => {
|
|
130
|
-
const cases = [
|
|
131
|
-
{ op: "gt", symbol: ">", value: 18 },
|
|
132
|
-
{ op: "gte", symbol: ">=", value: 18 },
|
|
133
|
-
{ op: "lt", symbol: "<", value: 65 },
|
|
134
|
-
{ op: "lte", symbol: "<=", value: 65 },
|
|
135
|
-
{ op: "ne", symbol: "!=", value: 0 }
|
|
136
|
-
];
|
|
137
|
-
|
|
138
|
-
cases.forEach(({ op, symbol, value }) => {
|
|
139
|
-
const builder = new SqlBuilder();
|
|
140
|
-
const where: any = {};
|
|
141
|
-
where[`age$${op}`] = value;
|
|
142
|
-
|
|
143
|
-
const result = builder.select(["*"]).from("users").where(where).toSelectSql();
|
|
144
|
-
|
|
145
|
-
expect(result.sql).toContain(symbol);
|
|
146
|
-
expect(result.params).toEqual([value]);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("SqlBuilder - 复杂 WHERE 条件", () => {
|
|
152
|
-
test("$or - 单个条件应正确处理", () => {
|
|
153
|
-
const builder = new SqlBuilder();
|
|
154
|
-
const result = builder
|
|
155
|
-
.select(["*"])
|
|
156
|
-
.from("users")
|
|
157
|
-
.where({
|
|
158
|
-
$or: [{ name: "john" }]
|
|
159
|
-
})
|
|
160
|
-
.toSelectSql();
|
|
161
|
-
|
|
162
|
-
// 单个条件不应该显示OR关键字
|
|
163
|
-
expect(result.sql).not.toContain(" OR ");
|
|
164
|
-
expect(result.params).toEqual(["john"]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("$or - 多个条件应用 OR 连接", () => {
|
|
168
|
-
const builder = new SqlBuilder();
|
|
169
|
-
const result = builder
|
|
170
|
-
.select(["*"])
|
|
171
|
-
.from("users")
|
|
172
|
-
.where({
|
|
173
|
-
$or: [{ name: "john" }, { email: "john@example.com" }]
|
|
174
|
-
})
|
|
175
|
-
.toSelectSql();
|
|
176
|
-
|
|
177
|
-
expect(result.sql).toContain("OR");
|
|
178
|
-
expect(result.params).toEqual(["john", "john@example.com"]);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("$or - 空数组应如何处理", () => {
|
|
182
|
-
const builder = new SqlBuilder();
|
|
183
|
-
const result = builder.select(["*"]).from("users").where({ $or: [] }).toSelectSql();
|
|
184
|
-
|
|
185
|
-
// **问题**:空 $or 应该跳过还是报错?
|
|
186
|
-
expect(typeof result.sql).toBe("string");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("$and + $or 嵌套", () => {
|
|
190
|
-
const builder = new SqlBuilder();
|
|
191
|
-
const result = builder
|
|
192
|
-
.select(["*"])
|
|
193
|
-
.from("users")
|
|
194
|
-
.where({
|
|
195
|
-
age$gte: 18,
|
|
196
|
-
$or: [{ role: "admin" }, { role: "moderator" }]
|
|
197
|
-
})
|
|
198
|
-
.toSelectSql();
|
|
199
|
-
|
|
200
|
-
expect(result.sql).toContain("AND");
|
|
201
|
-
expect(result.sql).toContain("OR");
|
|
202
|
-
expect(result.params).toEqual([18, "admin", "moderator"]);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("$or 内嵌套多个操作符", () => {
|
|
206
|
-
const builder = new SqlBuilder();
|
|
207
|
-
const result = builder
|
|
208
|
-
.select(["*"])
|
|
209
|
-
.from("users")
|
|
210
|
-
.where({
|
|
211
|
-
$or: [{ age$gt: 60 }, { age$lt: 18 }]
|
|
212
|
-
})
|
|
213
|
-
.toSelectSql();
|
|
214
|
-
|
|
215
|
-
expect(result.sql).toContain("OR");
|
|
216
|
-
expect(result.sql).toContain(">");
|
|
217
|
-
expect(result.sql).toContain("<");
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("深层嵌套的 $or 和 $and", () => {
|
|
221
|
-
const builder = new SqlBuilder();
|
|
222
|
-
const result = builder
|
|
223
|
-
.select(["*"])
|
|
224
|
-
.from("users")
|
|
225
|
-
.where({
|
|
226
|
-
status: "active",
|
|
227
|
-
$or: [
|
|
228
|
-
{
|
|
229
|
-
$and: [{ age$gte: 18 }, { age$lte: 65 }]
|
|
230
|
-
},
|
|
231
|
-
{ vip: true }
|
|
232
|
-
]
|
|
233
|
-
})
|
|
234
|
-
.toSelectSql();
|
|
235
|
-
|
|
236
|
-
expect(result.sql).toBeDefined();
|
|
237
|
-
expect(typeof result.sql).toBe("string");
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe("SqlBuilder - 字段名转义", () => {
|
|
242
|
-
test("普通字段名应添加反引号", () => {
|
|
243
|
-
const builder = new SqlBuilder();
|
|
244
|
-
const result = builder.select(["name", "email"]).from("users").toSelectSql();
|
|
245
|
-
|
|
246
|
-
expect(result.sql).toContain("`name`");
|
|
247
|
-
expect(result.sql).toContain("`email`");
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("星号不应添加反引号", () => {
|
|
251
|
-
const builder = new SqlBuilder();
|
|
252
|
-
const result = builder.select(["*"]).from("users").toSelectSql();
|
|
253
|
-
|
|
254
|
-
expect(result.sql).toContain("SELECT *");
|
|
255
|
-
expect(result.sql).not.toContain("`*`");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("已有反引号的字段不应重复添加", () => {
|
|
259
|
-
const builder = new SqlBuilder();
|
|
260
|
-
const result = builder.select(["`user_id`", "`user_name`"]).from("users").toSelectSql();
|
|
261
|
-
|
|
262
|
-
// 不应变成 `\`user_id\``
|
|
263
|
-
expect(result.sql).toContain("`user_id`");
|
|
264
|
-
expect(result.sql).not.toContain("``");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test("表名.字段名应分别转义", () => {
|
|
268
|
-
const builder = new SqlBuilder();
|
|
269
|
-
const result = builder.select(["users.id", "profiles.bio"]).from("users").toSelectSql();
|
|
270
|
-
|
|
271
|
-
expect(result.sql).toContain("`users`.`id`");
|
|
272
|
-
expect(result.sql).toContain("`profiles`.`bio`");
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test("函数调用必须使用 selectRaw", () => {
|
|
276
|
-
const builder = new SqlBuilder();
|
|
277
|
-
|
|
278
|
-
expect(() => {
|
|
279
|
-
builder.select(["COUNT(*)", "MAX(age)"]).from("users").toSelectSql();
|
|
280
|
-
}).toThrow("selectRaw");
|
|
281
|
-
|
|
282
|
-
const builder2 = new SqlBuilder();
|
|
283
|
-
const result = builder2.selectRaw("COUNT(*)").selectRaw("MAX(age)").from("users").toSelectSql();
|
|
284
|
-
|
|
285
|
-
expect(result.sql).toContain("COUNT(*)");
|
|
286
|
-
expect(result.sql).toContain("MAX(age)");
|
|
287
|
-
expect(result.sql).not.toContain("`COUNT(*)`");
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("AS 别名应正确处理", () => {
|
|
291
|
-
const builder = new SqlBuilder();
|
|
292
|
-
const result = builder.select(["name AS userName"]).selectRaw("COUNT(*) AS total").from("users").toSelectSql();
|
|
293
|
-
|
|
294
|
-
expect(result.sql).toContain("`name` AS userName");
|
|
295
|
-
expect(result.sql).toContain("COUNT(*) AS total");
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test("特殊字符字段名应转义", () => {
|
|
299
|
-
// 包含空格、横杠等特殊字符的字段名
|
|
300
|
-
const builder = new SqlBuilder();
|
|
301
|
-
const result = builder.select(["user-name", "created at"]).from("users").toSelectSql();
|
|
302
|
-
|
|
303
|
-
// 应该添加反引号保护
|
|
304
|
-
expect(result.sql).toContain("`user-name`");
|
|
305
|
-
expect(result.sql).toContain("`created at`");
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
describe("SqlBuilder - 表名转义", () => {
|
|
310
|
-
test("普通表名应添加反引号", () => {
|
|
311
|
-
const builder = new SqlBuilder();
|
|
312
|
-
const result = builder.select(["*"]).from("users").toSelectSql();
|
|
313
|
-
|
|
314
|
-
expect(result.sql).toContain("FROM `users`");
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("表别名应正确处理", () => {
|
|
318
|
-
const builder = new SqlBuilder();
|
|
319
|
-
const result = builder.select(["*"]).from("users u").toSelectSql();
|
|
320
|
-
|
|
321
|
-
expect(result.sql).toContain("FROM `users` u");
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
test("已有反引号的表名不应重复添加", () => {
|
|
325
|
-
const builder = new SqlBuilder();
|
|
326
|
-
const result = builder.select(["*"]).from("`user_profiles`").toSelectSql();
|
|
327
|
-
|
|
328
|
-
expect(result.sql).toContain("FROM `user_profiles`");
|
|
329
|
-
expect(result.sql).not.toContain("``");
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("已有反引号的表名带别名应正确处理", () => {
|
|
333
|
-
const builder = new SqlBuilder();
|
|
334
|
-
const result = builder.select(["*"]).from("`user_profiles` up").toSelectSql();
|
|
335
|
-
|
|
336
|
-
expect(result.sql).toContain("FROM `user_profiles` up");
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
describe("SqlBuilder - ORDER BY", () => {
|
|
341
|
-
test("单字段升序排序", () => {
|
|
342
|
-
const builder = new SqlBuilder();
|
|
343
|
-
const result = builder.select(["*"]).from("users").orderBy(["createdAt#ASC"]).toSelectSql();
|
|
344
|
-
|
|
345
|
-
// ORDER BY目前不转换驼峰,保持原样
|
|
346
|
-
expect(result.sql).toContain("ORDER BY `createdAt` ASC");
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
test("单字段降序排序", () => {
|
|
350
|
-
const builder = new SqlBuilder();
|
|
351
|
-
const result = builder.select(["*"]).from("users").orderBy(["createdAt#DESC"]).toSelectSql();
|
|
352
|
-
|
|
353
|
-
// ORDER BY目前不转换驼峰,保持原样
|
|
354
|
-
expect(result.sql).toContain("ORDER BY `createdAt` DESC");
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("多字段排序", () => {
|
|
358
|
-
const builder = new SqlBuilder();
|
|
359
|
-
const result = builder.select(["*"]).from("users").orderBy(["priority#DESC", "createdAt#ASC"]).toSelectSql();
|
|
360
|
-
|
|
361
|
-
expect(result.sql).toContain("ORDER BY");
|
|
362
|
-
expect(result.sql).toContain("`priority` DESC");
|
|
363
|
-
expect(result.sql).toContain("`createdAt` ASC");
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
test("无方向标识应抛错", () => {
|
|
367
|
-
// **当前实现**:要求必须有 #ASC 或 #DESC
|
|
368
|
-
|
|
369
|
-
const builder = new SqlBuilder();
|
|
370
|
-
|
|
371
|
-
expect(() => {
|
|
372
|
-
builder.select(["*"]).from("users").orderBy(["createdAt"]).toSelectSql();
|
|
373
|
-
}).toThrow('orderBy 字段必须是 "字段#方向" 格式的字符串');
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("字段名当前不转换为下划线", () => {
|
|
377
|
-
const builder = new SqlBuilder();
|
|
378
|
-
const result = builder.select(["*"]).from("users").orderBy(["createdAt#DESC", "userId#ASC"]).toSelectSql();
|
|
379
|
-
|
|
380
|
-
// 当前实现不转换驼峰
|
|
381
|
-
expect(result.sql).toContain("createdAt");
|
|
382
|
-
expect(result.sql).toContain("userId");
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
describe("SqlBuilder - LIMIT 和 OFFSET", () => {
|
|
387
|
-
test("LIMIT 应生成正确的 SQL", () => {
|
|
388
|
-
const builder = new SqlBuilder();
|
|
389
|
-
const result = builder.select(["*"]).from("users").limit(10).toSelectSql();
|
|
390
|
-
|
|
391
|
-
expect(result.sql).toContain("LIMIT 10");
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
test("OFFSET 单独使用时无效(需要配合LIMIT)", () => {
|
|
395
|
-
const builder = new SqlBuilder();
|
|
396
|
-
const result = builder.select(["*"]).from("users").offset(20).toSelectSql();
|
|
397
|
-
|
|
398
|
-
// 当前实现:单独使用OFFSET不生效
|
|
399
|
-
expect(result.sql).not.toContain("OFFSET");
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test("LIMIT + OFFSET 组合", () => {
|
|
403
|
-
const builder = new SqlBuilder();
|
|
404
|
-
const result = builder.select(["*"]).from("users").limit(10).offset(20).toSelectSql();
|
|
405
|
-
|
|
406
|
-
expect(result.sql).toContain("LIMIT 10");
|
|
407
|
-
expect(result.sql).toContain("OFFSET 20");
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test("LIMIT 0 应允许(查询结构不查数据)", () => {
|
|
411
|
-
const builder = new SqlBuilder();
|
|
412
|
-
const result = builder.select(["*"]).from("users").limit(0).toSelectSql();
|
|
413
|
-
|
|
414
|
-
expect(result.sql).toContain("LIMIT 0");
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("LIMIT 负数应抛错", () => {
|
|
418
|
-
const builder = new SqlBuilder();
|
|
419
|
-
|
|
420
|
-
expect(() => {
|
|
421
|
-
builder.select(["*"]).from("users").limit(-10).toSelectSql();
|
|
422
|
-
}).toThrow("LIMIT 数量必须是非负数");
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test("OFFSET 负数应抛错", () => {
|
|
426
|
-
const builder = new SqlBuilder();
|
|
427
|
-
|
|
428
|
-
expect(() => {
|
|
429
|
-
builder.select(["*"]).from("users").offset(-5).toSelectSql();
|
|
430
|
-
}).toThrow("OFFSET 必须是非负数");
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
describe("SqlBuilder - INSERT/UPDATE/DELETE 语句", () => {
|
|
435
|
-
test("当前 SqlBuilder 不直接支持 INSERT/UPDATE/DELETE", () => {
|
|
436
|
-
// **说明**:当前 SqlBuilder 主要用于 SELECT 查询
|
|
437
|
-
// INSERT/UPDATE/DELETE 由 DbHelper 的 insData/updData/delData 方法处理
|
|
438
|
-
|
|
439
|
-
const builder = new SqlBuilder();
|
|
440
|
-
|
|
441
|
-
// 验证没有这些方法
|
|
442
|
-
expect(typeof (builder as any).insert).toBe("undefined");
|
|
443
|
-
expect(typeof (builder as any).update).toBe("undefined");
|
|
444
|
-
expect(typeof (builder as any).delete).toBe("undefined");
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
describe("SqlBuilder - SQL 注入防护", () => {
|
|
449
|
-
test("参数化查询应防止 SQL 注入", () => {
|
|
450
|
-
const builder = new SqlBuilder();
|
|
451
|
-
const maliciousInput = "' OR '1'='1";
|
|
452
|
-
|
|
453
|
-
const result = builder.select(["*"]).from("users").where({ name: maliciousInput }).toSelectSql();
|
|
454
|
-
|
|
455
|
-
// 参数应该被正确转义
|
|
456
|
-
expect(result.params).toEqual([maliciousInput]);
|
|
457
|
-
expect(result.sql).not.toContain("' OR '1'='1");
|
|
458
|
-
expect(result.sql).toContain("?"); // 使用占位符
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("字段名不应允许用户输入", () => {
|
|
462
|
-
// **潜在风险**:如果字段名来自用户输入
|
|
463
|
-
const userInput = "name; DROP TABLE users; --";
|
|
464
|
-
expect(userInput).toContain("DROP TABLE");
|
|
465
|
-
|
|
466
|
-
// 字段名应该从白名单选择,不应直接使用用户输入
|
|
467
|
-
// const result = builder.select([userInput]).from('users').toSelectSql();
|
|
468
|
-
|
|
469
|
-
// **建议**:字段名应该验证是否在允许的字段列表中
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
test("表名不应允许用户输入", () => {
|
|
473
|
-
// **潜在风险**:表名来自用户输入
|
|
474
|
-
const userInput = "users; DROP TABLE sensitive_data; --";
|
|
475
|
-
expect(userInput).toContain("DROP TABLE");
|
|
476
|
-
|
|
477
|
-
// **建议**:表名应该从白名单选择
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
describe("SqlBuilder - reset 功能", () => {
|
|
482
|
-
test("reset 应清空所有状态", () => {
|
|
483
|
-
const builder = new SqlBuilder();
|
|
484
|
-
|
|
485
|
-
builder.select(["name"]).from("users").where({ id: 1 }).orderBy(["name#ASC"]).limit(10);
|
|
486
|
-
|
|
487
|
-
const beforeReset = builder.toSelectSql();
|
|
488
|
-
expect(beforeReset.sql).toBeDefined();
|
|
489
|
-
|
|
490
|
-
builder.reset();
|
|
491
|
-
|
|
492
|
-
// reset 后应该是空状态
|
|
493
|
-
builder.select(["email"]).from("profiles");
|
|
494
|
-
const afterReset = builder.toSelectSql();
|
|
495
|
-
|
|
496
|
-
expect(afterReset.sql).not.toContain("users");
|
|
497
|
-
expect(afterReset.sql).toContain("profiles");
|
|
498
|
-
expect(afterReset.sql).not.toContain("LIMIT");
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
test("reset 应支持链式调用", () => {
|
|
502
|
-
const builder = new SqlBuilder();
|
|
503
|
-
|
|
504
|
-
const result = builder.select(["*"]).from("users").reset().select(["id"]).from("posts").toSelectSql();
|
|
505
|
-
|
|
506
|
-
expect(result.sql).toContain("posts");
|
|
507
|
-
expect(result.sql).not.toContain("users");
|
|
508
|
-
});
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
describe("SqlBuilder - 代码逻辑问题分析", () => {
|
|
512
|
-
test("问题1:$in 空数组会生成错误的 SQL", () => {
|
|
513
|
-
// **问题**:WHERE id IN () 是非法的 SQL 语法
|
|
514
|
-
// **建议**:检测到空数组应跳过条件或抛出错误
|
|
515
|
-
|
|
516
|
-
const mockWhere = (values: any[]) => {
|
|
517
|
-
if (values.length === 0) {
|
|
518
|
-
throw new Error("$in 操作符不能使用空数组");
|
|
519
|
-
}
|
|
520
|
-
return `id IN (${values.map(() => "?").join(", ")})`;
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
expect(() => mockWhere([])).toThrow("$in 操作符不能使用空数组");
|
|
524
|
-
expect(mockWhere([1, 2])).toBe("id IN (?, ?)");
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
test("问题2:LIMIT 和 OFFSET 没有验证", () => {
|
|
528
|
-
// **问题**:负数会导致 SQL 错误
|
|
529
|
-
// **建议**:添加参数验证
|
|
530
|
-
|
|
531
|
-
const mockLimit = (value: number) => {
|
|
532
|
-
if (value < 0) {
|
|
533
|
-
throw new Error("LIMIT 必须 >= 0");
|
|
534
|
-
}
|
|
535
|
-
return `LIMIT ${value}`;
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
expect(() => mockLimit(-10)).toThrow("LIMIT 必须 >= 0");
|
|
539
|
-
expect(mockLimit(10)).toBe("LIMIT 10");
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
test("问题3:无 WHERE 的 UPDATE 和 DELETE 很危险", () => {
|
|
543
|
-
// **问题**:容易误操作导致数据丢失
|
|
544
|
-
// **建议**:要求必须有 WHERE 条件或明确的标志
|
|
545
|
-
|
|
546
|
-
const mockUpdate = (where: any, allowNoWhere: boolean = false) => {
|
|
547
|
-
const hasWhere = where && Object.keys(where).length > 0;
|
|
548
|
-
|
|
549
|
-
if (!hasWhere && !allowNoWhere) {
|
|
550
|
-
throw new Error("UPDATE 必须有 WHERE 条件。如果确实要更新所有行,请设置 allowNoWhere: true");
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return "UPDATE users SET status = ?";
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
expect(() => mockUpdate({})).toThrow("UPDATE 必须有 WHERE 条件");
|
|
557
|
-
expect(mockUpdate({}, true)).toBeDefined(); // 明确允许
|
|
558
|
-
expect(mockUpdate({ id: 1 })).toBeDefined(); // 有 WHERE
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test("问题4:$between 参数验证不足", () => {
|
|
562
|
-
// **问题**:应该验证是否为数组且长度为 2
|
|
563
|
-
// **建议**:添加严格的参数检查
|
|
564
|
-
|
|
565
|
-
const mockBetween = (value: any) => {
|
|
566
|
-
if (!Array.isArray(value)) {
|
|
567
|
-
throw new Error("$between 必须是数组");
|
|
568
|
-
}
|
|
569
|
-
if (value.length !== 2) {
|
|
570
|
-
throw new Error("$between 数组长度必须为 2");
|
|
571
|
-
}
|
|
572
|
-
return `age BETWEEN ? AND ?`;
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
expect(() => mockBetween(18)).toThrow("$between 必须是数组");
|
|
576
|
-
expect(() => mockBetween([18])).toThrow("$between 数组长度必须为 2");
|
|
577
|
-
expect(mockBetween([18, 65])).toBeDefined();
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
test("问题5:字段名转义可能影响性能", () => {
|
|
581
|
-
// **问题**:每次查询都要转义字段名
|
|
582
|
-
// **建议**:可以缓存转义结果
|
|
583
|
-
|
|
584
|
-
const escapeCache = new Map<string, string>();
|
|
585
|
-
|
|
586
|
-
const mockEscape = (field: string) => {
|
|
587
|
-
if (escapeCache.has(field)) {
|
|
588
|
-
return escapeCache.get(field)!;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const escaped = field === "*" ? field : `\`${field}\``;
|
|
592
|
-
escapeCache.set(field, escaped);
|
|
593
|
-
return escaped;
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
// 第一次转义
|
|
597
|
-
const t1 = performance.now();
|
|
598
|
-
mockEscape("user_name");
|
|
599
|
-
const time1 = performance.now() - t1;
|
|
600
|
-
|
|
601
|
-
// 第二次应该更快(从缓存读取)
|
|
602
|
-
const t2 = performance.now();
|
|
603
|
-
mockEscape("user_name");
|
|
604
|
-
const time2 = performance.now() - t2;
|
|
605
|
-
|
|
606
|
-
console.log(`首次转义: ${time1}ms, 缓存命中: ${time2}ms`);
|
|
607
|
-
});
|
|
608
|
-
});
|