befly 3.9.38 → 3.9.40
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 +37 -38
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +1 -1
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +8 -8
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +48 -44
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -52
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -65
- package/sync/syncMenu.ts +190 -55
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
package/lib/sqlBuilder.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* SQL 构造器 - TypeScript 版本
|
|
3
3
|
* 提供链式 API 构建 SQL 查询
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { WhereConditions,
|
|
7
|
-
import type { SqlValue } from 'befly-shared/types';
|
|
6
|
+
import type { WhereConditions, WhereOperator, OrderDirection, SqlQuery, InsertData, UpdateData, SqlValue } from "../types/common.js";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* SQL 构建器类
|
|
11
10
|
*/
|
|
12
11
|
export class SqlBuilder {
|
|
13
12
|
private _select: string[] = [];
|
|
14
|
-
private _from: string =
|
|
13
|
+
private _from: string = "";
|
|
15
14
|
private _where: string[] = [];
|
|
16
15
|
private _joins: string[] = [];
|
|
17
16
|
private _orderBy: string[] = [];
|
|
@@ -26,7 +25,7 @@ export class SqlBuilder {
|
|
|
26
25
|
*/
|
|
27
26
|
reset(): this {
|
|
28
27
|
this._select = [];
|
|
29
|
-
this._from =
|
|
28
|
+
this._from = "";
|
|
30
29
|
this._where = [];
|
|
31
30
|
this._joins = [];
|
|
32
31
|
this._orderBy = [];
|
|
@@ -42,19 +41,19 @@ export class SqlBuilder {
|
|
|
42
41
|
* 转义字段名
|
|
43
42
|
*/
|
|
44
43
|
private _escapeField(field: string): string {
|
|
45
|
-
if (typeof field !==
|
|
44
|
+
if (typeof field !== "string") {
|
|
46
45
|
return field;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
field = field.trim();
|
|
50
49
|
|
|
51
50
|
// 如果是 * 或已经有着重号或包含函数,直接返回
|
|
52
|
-
if (field ===
|
|
51
|
+
if (field === "*" || field.startsWith("`") || field.includes("(")) {
|
|
53
52
|
return field;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
// 处理别名(AS关键字)
|
|
57
|
-
if (field.toUpperCase().includes(
|
|
56
|
+
if (field.toUpperCase().includes(" AS ")) {
|
|
58
57
|
const parts = field.split(/\s+AS\s+/i);
|
|
59
58
|
const fieldPart = parts[0].trim();
|
|
60
59
|
const aliasPart = parts[1].trim();
|
|
@@ -62,17 +61,17 @@ export class SqlBuilder {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
// 处理表名.字段名的情况(多表联查)
|
|
65
|
-
if (field.includes(
|
|
66
|
-
const parts = field.split(
|
|
64
|
+
if (field.includes(".")) {
|
|
65
|
+
const parts = field.split(".");
|
|
67
66
|
return parts
|
|
68
67
|
.map((part) => {
|
|
69
68
|
part = part.trim();
|
|
70
|
-
if (part ===
|
|
69
|
+
if (part === "*" || part.startsWith("`")) {
|
|
71
70
|
return part;
|
|
72
71
|
}
|
|
73
72
|
return `\`${part}\``;
|
|
74
73
|
})
|
|
75
|
-
.join(
|
|
74
|
+
.join(".");
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
// 处理单个字段名
|
|
@@ -83,18 +82,18 @@ export class SqlBuilder {
|
|
|
83
82
|
* 转义表名
|
|
84
83
|
*/
|
|
85
84
|
private _escapeTable(table: string): string {
|
|
86
|
-
if (typeof table !==
|
|
85
|
+
if (typeof table !== "string") {
|
|
87
86
|
return table;
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
table = table.trim();
|
|
91
90
|
|
|
92
|
-
if (table.startsWith(
|
|
91
|
+
if (table.startsWith("`")) {
|
|
93
92
|
return table;
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
// 处理表别名(表名 + 空格 + 别名)
|
|
97
|
-
if (table.includes(
|
|
96
|
+
if (table.includes(" ")) {
|
|
98
97
|
const parts = table.split(/\s+/);
|
|
99
98
|
if (parts.length === 2) {
|
|
100
99
|
const tableName = parts[0].trim();
|
|
@@ -124,75 +123,75 @@ export class SqlBuilder {
|
|
|
124
123
|
const escapedField = this._escapeField(fieldName);
|
|
125
124
|
|
|
126
125
|
switch (operator) {
|
|
127
|
-
case
|
|
128
|
-
case
|
|
126
|
+
case "$ne":
|
|
127
|
+
case "$not":
|
|
129
128
|
this._validateParam(value);
|
|
130
129
|
this._where.push(`${escapedField} != ?`);
|
|
131
130
|
this._params.push(value);
|
|
132
131
|
break;
|
|
133
132
|
|
|
134
|
-
case
|
|
133
|
+
case "$in":
|
|
135
134
|
if (!Array.isArray(value)) {
|
|
136
135
|
throw new Error(`$in 操作符的值必须是数组 (operator: ${operator})`);
|
|
137
136
|
}
|
|
138
137
|
if (value.length === 0) {
|
|
139
138
|
throw new Error(`$in 操作符的数组不能为空。提示:空数组会导致查询永远不匹配任何记录,这通常不是预期行为。请检查查询条件或移除该字段。`);
|
|
140
139
|
}
|
|
141
|
-
const placeholders = value.map(() =>
|
|
140
|
+
const placeholders = value.map(() => "?").join(",");
|
|
142
141
|
this._where.push(`${escapedField} IN (${placeholders})`);
|
|
143
142
|
this._params.push(...value);
|
|
144
143
|
break;
|
|
145
144
|
|
|
146
|
-
case
|
|
147
|
-
case
|
|
145
|
+
case "$nin":
|
|
146
|
+
case "$notIn":
|
|
148
147
|
if (!Array.isArray(value)) {
|
|
149
148
|
throw new Error(`$nin/$notIn 操作符的值必须是数组 (operator: ${operator})`);
|
|
150
149
|
}
|
|
151
150
|
if (value.length === 0) {
|
|
152
151
|
throw new Error(`$nin/$notIn 操作符的数组不能为空。提示:空数组会导致查询匹配所有记录,这通常不是预期行为。请检查查询条件或移除该字段。`);
|
|
153
152
|
}
|
|
154
|
-
const placeholders2 = value.map(() =>
|
|
153
|
+
const placeholders2 = value.map(() => "?").join(",");
|
|
155
154
|
this._where.push(`${escapedField} NOT IN (${placeholders2})`);
|
|
156
155
|
this._params.push(...value);
|
|
157
156
|
break;
|
|
158
157
|
|
|
159
|
-
case
|
|
158
|
+
case "$like":
|
|
160
159
|
this._validateParam(value);
|
|
161
160
|
this._where.push(`${escapedField} LIKE ?`);
|
|
162
161
|
this._params.push(value);
|
|
163
162
|
break;
|
|
164
163
|
|
|
165
|
-
case
|
|
164
|
+
case "$notLike":
|
|
166
165
|
this._validateParam(value);
|
|
167
166
|
this._where.push(`${escapedField} NOT LIKE ?`);
|
|
168
167
|
this._params.push(value);
|
|
169
168
|
break;
|
|
170
169
|
|
|
171
|
-
case
|
|
170
|
+
case "$gt":
|
|
172
171
|
this._validateParam(value);
|
|
173
172
|
this._where.push(`${escapedField} > ?`);
|
|
174
173
|
this._params.push(value);
|
|
175
174
|
break;
|
|
176
175
|
|
|
177
|
-
case
|
|
176
|
+
case "$gte":
|
|
178
177
|
this._validateParam(value);
|
|
179
178
|
this._where.push(`${escapedField} >= ?`);
|
|
180
179
|
this._params.push(value);
|
|
181
180
|
break;
|
|
182
181
|
|
|
183
|
-
case
|
|
182
|
+
case "$lt":
|
|
184
183
|
this._validateParam(value);
|
|
185
184
|
this._where.push(`${escapedField} < ?`);
|
|
186
185
|
this._params.push(value);
|
|
187
186
|
break;
|
|
188
187
|
|
|
189
|
-
case
|
|
188
|
+
case "$lte":
|
|
190
189
|
this._validateParam(value);
|
|
191
190
|
this._where.push(`${escapedField} <= ?`);
|
|
192
191
|
this._params.push(value);
|
|
193
192
|
break;
|
|
194
193
|
|
|
195
|
-
case
|
|
194
|
+
case "$between":
|
|
196
195
|
if (Array.isArray(value) && value.length === 2) {
|
|
197
196
|
this._validateParam(value[0]);
|
|
198
197
|
this._validateParam(value[1]);
|
|
@@ -201,7 +200,7 @@ export class SqlBuilder {
|
|
|
201
200
|
}
|
|
202
201
|
break;
|
|
203
202
|
|
|
204
|
-
case
|
|
203
|
+
case "$notBetween":
|
|
205
204
|
if (Array.isArray(value) && value.length === 2) {
|
|
206
205
|
this._validateParam(value[0]);
|
|
207
206
|
this._validateParam(value[1]);
|
|
@@ -210,13 +209,13 @@ export class SqlBuilder {
|
|
|
210
209
|
}
|
|
211
210
|
break;
|
|
212
211
|
|
|
213
|
-
case
|
|
212
|
+
case "$null":
|
|
214
213
|
if (value === true) {
|
|
215
214
|
this._where.push(`${escapedField} IS NULL`);
|
|
216
215
|
}
|
|
217
216
|
break;
|
|
218
217
|
|
|
219
|
-
case
|
|
218
|
+
case "$notNull":
|
|
220
219
|
if (value === true) {
|
|
221
220
|
this._where.push(`${escapedField} IS NOT NULL`);
|
|
222
221
|
}
|
|
@@ -234,7 +233,7 @@ export class SqlBuilder {
|
|
|
234
233
|
* 处理复杂的 WHERE 条件对象
|
|
235
234
|
*/
|
|
236
235
|
private _processWhereConditions(whereObj: WhereConditions): void {
|
|
237
|
-
if (!whereObj || typeof whereObj !==
|
|
236
|
+
if (!whereObj || typeof whereObj !== "object") {
|
|
238
237
|
return;
|
|
239
238
|
}
|
|
240
239
|
|
|
@@ -244,11 +243,11 @@ export class SqlBuilder {
|
|
|
244
243
|
return;
|
|
245
244
|
}
|
|
246
245
|
|
|
247
|
-
if (key ===
|
|
246
|
+
if (key === "$and") {
|
|
248
247
|
if (Array.isArray(value)) {
|
|
249
248
|
value.forEach((condition) => this._processWhereConditions(condition));
|
|
250
249
|
}
|
|
251
|
-
} else if (key ===
|
|
250
|
+
} else if (key === "$or") {
|
|
252
251
|
if (Array.isArray(value)) {
|
|
253
252
|
const orConditions: string[] = [];
|
|
254
253
|
const tempParams: SqlValue[] = [];
|
|
@@ -257,25 +256,25 @@ export class SqlBuilder {
|
|
|
257
256
|
const tempBuilder = new SqlBuilder();
|
|
258
257
|
tempBuilder._processWhereConditions(condition);
|
|
259
258
|
if (tempBuilder._where.length > 0) {
|
|
260
|
-
orConditions.push(`(${tempBuilder._where.join(
|
|
259
|
+
orConditions.push(`(${tempBuilder._where.join(" AND ")})`);
|
|
261
260
|
tempParams.push(...tempBuilder._params);
|
|
262
261
|
}
|
|
263
262
|
});
|
|
264
263
|
|
|
265
264
|
if (orConditions.length > 0) {
|
|
266
|
-
this._where.push(`(${orConditions.join(
|
|
265
|
+
this._where.push(`(${orConditions.join(" OR ")})`);
|
|
267
266
|
this._params.push(...tempParams);
|
|
268
267
|
}
|
|
269
268
|
}
|
|
270
|
-
} else if (key.includes(
|
|
269
|
+
} else if (key.includes("$")) {
|
|
271
270
|
// 一级属性格式:age$gt, role$in 等
|
|
272
|
-
const lastDollarIndex = key.lastIndexOf(
|
|
271
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
273
272
|
const fieldName = key.substring(0, lastDollarIndex);
|
|
274
|
-
const operator = (
|
|
273
|
+
const operator = ("$" + key.substring(lastDollarIndex + 1)) as WhereOperator;
|
|
275
274
|
this._applyOperator(fieldName, operator, value);
|
|
276
275
|
} else {
|
|
277
276
|
// 检查值是否为对象(嵌套条件)
|
|
278
|
-
if (typeof value ===
|
|
277
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
279
278
|
// 嵌套条件:如 { age: { $gt: 18 } }
|
|
280
279
|
for (const [op, val] of Object.entries(value)) {
|
|
281
280
|
this._applyOperator(key, op as WhereOperator, val);
|
|
@@ -296,7 +295,7 @@ export class SqlBuilder {
|
|
|
296
295
|
*/
|
|
297
296
|
getWhereConditions(): { sql: string; params: SqlValue[] } {
|
|
298
297
|
return {
|
|
299
|
-
sql: this._where.length > 0 ? this._where.join(
|
|
298
|
+
sql: this._where.length > 0 ? this._where.join(" AND ") : "",
|
|
300
299
|
params: [...this._params]
|
|
301
300
|
};
|
|
302
301
|
}
|
|
@@ -304,13 +303,13 @@ export class SqlBuilder {
|
|
|
304
303
|
/**
|
|
305
304
|
* SELECT 字段
|
|
306
305
|
*/
|
|
307
|
-
select(fields: string | string[] =
|
|
306
|
+
select(fields: string | string[] = "*"): this {
|
|
308
307
|
if (Array.isArray(fields)) {
|
|
309
308
|
this._select = [...this._select, ...fields.map((field) => this._escapeField(field))];
|
|
310
|
-
} else if (typeof fields ===
|
|
309
|
+
} else if (typeof fields === "string") {
|
|
311
310
|
this._select.push(this._escapeField(fields));
|
|
312
311
|
} else {
|
|
313
|
-
throw new Error(
|
|
312
|
+
throw new Error("SELECT 字段必须是字符串或数组");
|
|
314
313
|
}
|
|
315
314
|
return this;
|
|
316
315
|
}
|
|
@@ -319,7 +318,7 @@ export class SqlBuilder {
|
|
|
319
318
|
* FROM 表名
|
|
320
319
|
*/
|
|
321
320
|
from(table: string): this {
|
|
322
|
-
if (typeof table !==
|
|
321
|
+
if (typeof table !== "string" || !table.trim()) {
|
|
323
322
|
throw new Error(`FROM 表名必须是非空字符串 (table: ${table})`);
|
|
324
323
|
}
|
|
325
324
|
this._from = this._escapeTable(table.trim());
|
|
@@ -330,14 +329,14 @@ export class SqlBuilder {
|
|
|
330
329
|
* WHERE 条件
|
|
331
330
|
*/
|
|
332
331
|
where(condition: WhereConditions | string, value?: SqlValue): this {
|
|
333
|
-
if (typeof condition ===
|
|
332
|
+
if (typeof condition === "object" && condition !== null) {
|
|
334
333
|
this._processWhereConditions(condition);
|
|
335
334
|
} else if (value !== undefined && value !== null) {
|
|
336
335
|
this._validateParam(value);
|
|
337
336
|
const escapedCondition = this._escapeField(condition as string);
|
|
338
337
|
this._where.push(`${escapedCondition} = ?`);
|
|
339
338
|
this._params.push(value);
|
|
340
|
-
} else if (typeof condition ===
|
|
339
|
+
} else if (typeof condition === "string") {
|
|
341
340
|
this._where.push(condition);
|
|
342
341
|
}
|
|
343
342
|
return this;
|
|
@@ -347,7 +346,7 @@ export class SqlBuilder {
|
|
|
347
346
|
* LEFT JOIN
|
|
348
347
|
*/
|
|
349
348
|
leftJoin(table: string, on: string): this {
|
|
350
|
-
if (typeof table !==
|
|
349
|
+
if (typeof table !== "string" || typeof on !== "string") {
|
|
351
350
|
throw new Error(`JOIN 表名和条件必须是字符串 (table: ${table}, on: ${on})`);
|
|
352
351
|
}
|
|
353
352
|
const escapedTable = this._escapeTable(table);
|
|
@@ -359,7 +358,7 @@ export class SqlBuilder {
|
|
|
359
358
|
* RIGHT JOIN
|
|
360
359
|
*/
|
|
361
360
|
rightJoin(table: string, on: string): this {
|
|
362
|
-
if (typeof table !==
|
|
361
|
+
if (typeof table !== "string" || typeof on !== "string") {
|
|
363
362
|
throw new Error(`JOIN 表名和条件必须是字符串 (table: ${table}, on: ${on})`);
|
|
364
363
|
}
|
|
365
364
|
const escapedTable = this._escapeTable(table);
|
|
@@ -371,7 +370,7 @@ export class SqlBuilder {
|
|
|
371
370
|
* INNER JOIN
|
|
372
371
|
*/
|
|
373
372
|
innerJoin(table: string, on: string): this {
|
|
374
|
-
if (typeof table !==
|
|
373
|
+
if (typeof table !== "string" || typeof on !== "string") {
|
|
375
374
|
throw new Error(`JOIN 表名和条件必须是字符串 (table: ${table}, on: ${on})`);
|
|
376
375
|
}
|
|
377
376
|
const escapedTable = this._escapeTable(table);
|
|
@@ -389,11 +388,11 @@ export class SqlBuilder {
|
|
|
389
388
|
}
|
|
390
389
|
|
|
391
390
|
fields.forEach((item) => {
|
|
392
|
-
if (typeof item !==
|
|
391
|
+
if (typeof item !== "string" || !item.includes("#")) {
|
|
393
392
|
throw new Error(`orderBy 字段必须是 "字段#方向" 格式的字符串(例如:"name#ASC", "id#DESC") (item: ${item})`);
|
|
394
393
|
}
|
|
395
394
|
|
|
396
|
-
const [fieldName, direction] = item.split(
|
|
395
|
+
const [fieldName, direction] = item.split("#");
|
|
397
396
|
const cleanField = fieldName.trim();
|
|
398
397
|
const cleanDir = direction.trim().toUpperCase() as OrderDirection;
|
|
399
398
|
|
|
@@ -401,7 +400,7 @@ export class SqlBuilder {
|
|
|
401
400
|
throw new Error(`orderBy 中字段名不能为空 (item: ${item})`);
|
|
402
401
|
}
|
|
403
402
|
|
|
404
|
-
if (![
|
|
403
|
+
if (!["ASC", "DESC"].includes(cleanDir)) {
|
|
405
404
|
throw new Error(`ORDER BY 方向必须是 ASC 或 DESC (direction: ${cleanDir})`);
|
|
406
405
|
}
|
|
407
406
|
|
|
@@ -417,9 +416,9 @@ export class SqlBuilder {
|
|
|
417
416
|
*/
|
|
418
417
|
groupBy(field: string | string[]): this {
|
|
419
418
|
if (Array.isArray(field)) {
|
|
420
|
-
const escapedFields = field.filter((f) => typeof f ===
|
|
419
|
+
const escapedFields = field.filter((f) => typeof f === "string").map((f) => this._escapeField(f));
|
|
421
420
|
this._groupBy = [...this._groupBy, ...escapedFields];
|
|
422
|
-
} else if (typeof field ===
|
|
421
|
+
} else if (typeof field === "string") {
|
|
423
422
|
this._groupBy.push(this._escapeField(field));
|
|
424
423
|
}
|
|
425
424
|
return this;
|
|
@@ -429,7 +428,7 @@ export class SqlBuilder {
|
|
|
429
428
|
* HAVING
|
|
430
429
|
*/
|
|
431
430
|
having(condition: string): this {
|
|
432
|
-
if (typeof condition ===
|
|
431
|
+
if (typeof condition === "string") {
|
|
433
432
|
this._having.push(condition);
|
|
434
433
|
}
|
|
435
434
|
return this;
|
|
@@ -439,12 +438,12 @@ export class SqlBuilder {
|
|
|
439
438
|
* LIMIT
|
|
440
439
|
*/
|
|
441
440
|
limit(count: number, offset?: number): this {
|
|
442
|
-
if (typeof count !==
|
|
441
|
+
if (typeof count !== "number" || count < 0) {
|
|
443
442
|
throw new Error(`LIMIT 数量必须是非负数 (count: ${count})`);
|
|
444
443
|
}
|
|
445
444
|
this._limit = Math.floor(count);
|
|
446
445
|
if (offset !== undefined && offset !== null) {
|
|
447
|
-
if (typeof offset !==
|
|
446
|
+
if (typeof offset !== "number" || offset < 0) {
|
|
448
447
|
throw new Error(`OFFSET 必须是非负数 (offset: ${offset})`);
|
|
449
448
|
}
|
|
450
449
|
this._offset = Math.floor(offset);
|
|
@@ -456,7 +455,7 @@ export class SqlBuilder {
|
|
|
456
455
|
* OFFSET
|
|
457
456
|
*/
|
|
458
457
|
offset(count: number): this {
|
|
459
|
-
if (typeof count !==
|
|
458
|
+
if (typeof count !== "number" || count < 0) {
|
|
460
459
|
throw new Error(`OFFSET 必须是非负数 (count: ${count})`);
|
|
461
460
|
}
|
|
462
461
|
this._offset = Math.floor(count);
|
|
@@ -467,33 +466,33 @@ export class SqlBuilder {
|
|
|
467
466
|
* 构建 SELECT 查询
|
|
468
467
|
*/
|
|
469
468
|
toSelectSql(): SqlQuery {
|
|
470
|
-
let sql =
|
|
469
|
+
let sql = "SELECT ";
|
|
471
470
|
|
|
472
|
-
sql += this._select.length > 0 ? this._select.join(
|
|
471
|
+
sql += this._select.length > 0 ? this._select.join(", ") : "*";
|
|
473
472
|
|
|
474
473
|
if (!this._from) {
|
|
475
|
-
throw new Error(
|
|
474
|
+
throw new Error("FROM 表名是必需的");
|
|
476
475
|
}
|
|
477
476
|
sql += ` FROM ${this._from}`;
|
|
478
477
|
|
|
479
478
|
if (this._joins.length > 0) {
|
|
480
|
-
sql +=
|
|
479
|
+
sql += " " + this._joins.join(" ");
|
|
481
480
|
}
|
|
482
481
|
|
|
483
482
|
if (this._where.length > 0) {
|
|
484
|
-
sql +=
|
|
483
|
+
sql += " WHERE " + this._where.join(" AND ");
|
|
485
484
|
}
|
|
486
485
|
|
|
487
486
|
if (this._groupBy.length > 0) {
|
|
488
|
-
sql +=
|
|
487
|
+
sql += " GROUP BY " + this._groupBy.join(", ");
|
|
489
488
|
}
|
|
490
489
|
|
|
491
490
|
if (this._having.length > 0) {
|
|
492
|
-
sql +=
|
|
491
|
+
sql += " HAVING " + this._having.join(" AND ");
|
|
493
492
|
}
|
|
494
493
|
|
|
495
494
|
if (this._orderBy.length > 0) {
|
|
496
|
-
sql +=
|
|
495
|
+
sql += " ORDER BY " + this._orderBy.join(", ");
|
|
497
496
|
}
|
|
498
497
|
|
|
499
498
|
if (this._limit !== null) {
|
|
@@ -510,11 +509,11 @@ export class SqlBuilder {
|
|
|
510
509
|
* 构建 INSERT 查询
|
|
511
510
|
*/
|
|
512
511
|
toInsertSql(table: string, data: InsertData): SqlQuery {
|
|
513
|
-
if (!table || typeof table !==
|
|
512
|
+
if (!table || typeof table !== "string") {
|
|
514
513
|
throw new Error(`INSERT 需要表名 (table: ${table})`);
|
|
515
514
|
}
|
|
516
515
|
|
|
517
|
-
if (!data || typeof data !==
|
|
516
|
+
if (!data || typeof data !== "object") {
|
|
518
517
|
throw new Error(`INSERT 需要数据 (table: ${table}, data: ${JSON.stringify(data)})`);
|
|
519
518
|
}
|
|
520
519
|
|
|
@@ -531,10 +530,10 @@ export class SqlBuilder {
|
|
|
531
530
|
}
|
|
532
531
|
|
|
533
532
|
const escapedFields = fields.map((field) => this._escapeField(field));
|
|
534
|
-
const placeholders = fields.map(() =>
|
|
535
|
-
const values = data.map(() => `(${placeholders})`).join(
|
|
533
|
+
const placeholders = fields.map(() => "?").join(", ");
|
|
534
|
+
const values = data.map(() => `(${placeholders})`).join(", ");
|
|
536
535
|
|
|
537
|
-
const sql = `INSERT INTO ${escapedTable} (${escapedFields.join(
|
|
536
|
+
const sql = `INSERT INTO ${escapedTable} (${escapedFields.join(", ")}) VALUES ${values}`;
|
|
538
537
|
const params = data.flatMap((row) => fields.map((field) => row[field]));
|
|
539
538
|
|
|
540
539
|
return { sql, params };
|
|
@@ -545,8 +544,8 @@ export class SqlBuilder {
|
|
|
545
544
|
}
|
|
546
545
|
|
|
547
546
|
const escapedFields = fields.map((field) => this._escapeField(field));
|
|
548
|
-
const placeholders = fields.map(() =>
|
|
549
|
-
const sql = `INSERT INTO ${escapedTable} (${escapedFields.join(
|
|
547
|
+
const placeholders = fields.map(() => "?").join(", ");
|
|
548
|
+
const sql = `INSERT INTO ${escapedTable} (${escapedFields.join(", ")}) VALUES (${placeholders})`;
|
|
550
549
|
const params = fields.map((field) => data[field]);
|
|
551
550
|
|
|
552
551
|
return { sql, params };
|
|
@@ -557,29 +556,29 @@ export class SqlBuilder {
|
|
|
557
556
|
* 构建 UPDATE 查询
|
|
558
557
|
*/
|
|
559
558
|
toUpdateSql(table: string, data: UpdateData): SqlQuery {
|
|
560
|
-
if (!table || typeof table !==
|
|
561
|
-
throw new Error(
|
|
559
|
+
if (!table || typeof table !== "string") {
|
|
560
|
+
throw new Error("UPDATE 需要表名");
|
|
562
561
|
}
|
|
563
562
|
|
|
564
|
-
if (!data || typeof data !==
|
|
565
|
-
throw new Error(
|
|
563
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
564
|
+
throw new Error("UPDATE 需要数据对象");
|
|
566
565
|
}
|
|
567
566
|
|
|
568
567
|
const fields = Object.keys(data);
|
|
569
568
|
if (fields.length === 0) {
|
|
570
|
-
throw new Error(
|
|
569
|
+
throw new Error("更新数据必须至少有一个字段");
|
|
571
570
|
}
|
|
572
571
|
|
|
573
572
|
const escapedTable = this._escapeTable(table);
|
|
574
573
|
const setFields = fields.map((field) => `${this._escapeField(field)} = ?`);
|
|
575
574
|
const params: SqlValue[] = [...Object.values(data), ...this._params];
|
|
576
575
|
|
|
577
|
-
let sql = `UPDATE ${escapedTable} SET ${setFields.join(
|
|
576
|
+
let sql = `UPDATE ${escapedTable} SET ${setFields.join(", ")}`;
|
|
578
577
|
|
|
579
578
|
if (this._where.length > 0) {
|
|
580
|
-
sql +=
|
|
579
|
+
sql += " WHERE " + this._where.join(" AND ");
|
|
581
580
|
} else {
|
|
582
|
-
throw new Error(
|
|
581
|
+
throw new Error("为安全起见,UPDATE 需要 WHERE 条件");
|
|
583
582
|
}
|
|
584
583
|
|
|
585
584
|
return { sql, params };
|
|
@@ -589,17 +588,17 @@ export class SqlBuilder {
|
|
|
589
588
|
* 构建 DELETE 查询
|
|
590
589
|
*/
|
|
591
590
|
toDeleteSql(table: string): SqlQuery {
|
|
592
|
-
if (!table || typeof table !==
|
|
593
|
-
throw new Error(
|
|
591
|
+
if (!table || typeof table !== "string") {
|
|
592
|
+
throw new Error("DELETE 需要表名");
|
|
594
593
|
}
|
|
595
594
|
|
|
596
595
|
const escapedTable = this._escapeTable(table);
|
|
597
596
|
let sql = `DELETE FROM ${escapedTable}`;
|
|
598
597
|
|
|
599
598
|
if (this._where.length > 0) {
|
|
600
|
-
sql +=
|
|
599
|
+
sql += " WHERE " + this._where.join(" AND ");
|
|
601
600
|
} else {
|
|
602
|
-
throw new Error(
|
|
601
|
+
throw new Error("为安全起见,DELETE 需要 WHERE 条件");
|
|
603
602
|
}
|
|
604
603
|
|
|
605
604
|
return { sql, params: [...this._params] };
|
|
@@ -609,19 +608,19 @@ export class SqlBuilder {
|
|
|
609
608
|
* 构建 COUNT 查询
|
|
610
609
|
*/
|
|
611
610
|
toCountSql(): SqlQuery {
|
|
612
|
-
let sql =
|
|
611
|
+
let sql = "SELECT COUNT(*) as total";
|
|
613
612
|
|
|
614
613
|
if (!this._from) {
|
|
615
|
-
throw new Error(
|
|
614
|
+
throw new Error("COUNT 需要 FROM 表名");
|
|
616
615
|
}
|
|
617
616
|
sql += ` FROM ${this._from}`;
|
|
618
617
|
|
|
619
618
|
if (this._joins.length > 0) {
|
|
620
|
-
sql +=
|
|
619
|
+
sql += " " + this._joins.join(" ");
|
|
621
620
|
}
|
|
622
621
|
|
|
623
622
|
if (this._where.length > 0) {
|
|
624
|
-
sql +=
|
|
623
|
+
sql += " WHERE " + this._where.join(" AND ");
|
|
625
624
|
}
|
|
626
625
|
|
|
627
626
|
return { sql, params: [...this._params] };
|