befly 2.3.3 → 3.0.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/checks/conflict.ts +329 -0
- package/checks/table.ts +252 -0
- package/config/env.ts +218 -0
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +96 -0
- package/main.ts +47 -0
- package/package.json +26 -11
- package/plugins/db.ts +60 -0
- package/plugins/logger.ts +28 -0
- package/plugins/redis.ts +47 -0
- package/scripts/syncDb/apply.ts +171 -0
- package/scripts/syncDb/constants.ts +71 -0
- package/scripts/syncDb/ddl.ts +189 -0
- package/scripts/syncDb/helpers.ts +173 -0
- package/scripts/syncDb/index.ts +203 -0
- package/scripts/syncDb/schema.ts +199 -0
- package/scripts/syncDb/sqlite.ts +50 -0
- package/scripts/syncDb/state.ts +106 -0
- package/scripts/syncDb/table.ts +214 -0
- package/scripts/syncDb/tableCreate.ts +148 -0
- package/scripts/syncDb/tests/constants.test.ts +105 -0
- package/scripts/syncDb/tests/ddl.test.ts +134 -0
- package/scripts/syncDb/tests/helpers.test.ts +70 -0
- package/scripts/syncDb/types.ts +92 -0
- package/scripts/syncDb/version.ts +73 -0
- package/scripts/syncDb.ts +10 -0
- package/tsconfig.json +58 -0
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +249 -0
- package/types/befly.d.ts +230 -0
- package/types/common.d.ts +215 -0
- package/types/context.d.ts +7 -0
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +273 -0
- package/types/index.d.ts +450 -0
- package/types/index.ts +438 -0
- package/types/jwt.d.ts +99 -0
- package/types/logger.d.ts +43 -0
- package/types/plugin.d.ts +109 -0
- package/types/redis.d.ts +46 -0
- package/types/tool.d.ts +67 -0
- package/types/validator.d.ts +43 -0
- package/types/validator.ts +43 -0
- package/utils/colors.ts +221 -0
- package/utils/crypto.ts +308 -0
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +713 -0
- package/utils/helper.ts +812 -0
- package/utils/index.ts +33 -0
- package/utils/jwt.ts +493 -0
- package/utils/logger.ts +191 -0
- package/utils/redisHelper.ts +321 -0
- package/utils/requestContext.ts +167 -0
- package/utils/sqlBuilder.ts +611 -0
- package/utils/validate.ts +493 -0
- package/utils/{xml.js → xml.ts} +100 -74
- package/.npmrc +0 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -11
- package/apis/health/info.js +0 -49
- package/apis/tool/tokenCheck.js +0 -29
- package/bin/befly.js +0 -109
- package/bunfig.toml +0 -3
- package/checks/table.js +0 -206
- package/config/env.js +0 -64
- package/main.js +0 -579
- package/plugins/db.js +0 -46
- package/plugins/logger.js +0 -14
- package/plugins/redis.js +0 -32
- package/plugins/tool.js +0 -8
- package/scripts/syncDb.js +0 -752
- package/scripts/syncDev.js +0 -96
- package/system.js +0 -118
- package/tables/common.json +0 -16
- package/tables/tool.json +0 -6
- package/utils/api.js +0 -27
- package/utils/colors.js +0 -83
- package/utils/crypto.js +0 -260
- package/utils/index.js +0 -334
- package/utils/jwt.js +0 -387
- package/utils/logger.js +0 -143
- package/utils/redisHelper.js +0 -74
- package/utils/sqlBuilder.js +0 -498
- package/utils/sqlManager.js +0 -471
- package/utils/tool.js +0 -31
- package/utils/validate.js +0 -226
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL 构造器 - TypeScript 版本
|
|
3
|
+
* 提供链式 API 构建 SQL 查询
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WhereConditions, SqlValue, OrderByField } from '../types/common.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SQL 构建器类
|
|
10
|
+
*/
|
|
11
|
+
export class SqlBuilder {
|
|
12
|
+
private _select: string[] = [];
|
|
13
|
+
private _from: string = '';
|
|
14
|
+
private _where: string[] = [];
|
|
15
|
+
private _joins: string[] = [];
|
|
16
|
+
private _orderBy: string[] = [];
|
|
17
|
+
private _groupBy: string[] = [];
|
|
18
|
+
private _having: string[] = [];
|
|
19
|
+
private _limit: number | null = null;
|
|
20
|
+
private _offset: number | null = null;
|
|
21
|
+
private _params: SqlValue[] = [];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 重置构建器状态
|
|
25
|
+
*/
|
|
26
|
+
reset(): this {
|
|
27
|
+
this._select = [];
|
|
28
|
+
this._from = '';
|
|
29
|
+
this._where = [];
|
|
30
|
+
this._joins = [];
|
|
31
|
+
this._orderBy = [];
|
|
32
|
+
this._groupBy = [];
|
|
33
|
+
this._having = [];
|
|
34
|
+
this._limit = null;
|
|
35
|
+
this._offset = null;
|
|
36
|
+
this._params = [];
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 转义字段名
|
|
42
|
+
*/
|
|
43
|
+
private _escapeField(field: string): string {
|
|
44
|
+
if (typeof field !== 'string') {
|
|
45
|
+
return field;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
field = field.trim();
|
|
49
|
+
|
|
50
|
+
// 如果是 * 或已经有着重号或包含函数,直接返回
|
|
51
|
+
if (field === '*' || field.startsWith('`') || field.includes('(')) {
|
|
52
|
+
return field;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 处理别名(AS关键字)
|
|
56
|
+
if (field.toUpperCase().includes(' AS ')) {
|
|
57
|
+
const parts = field.split(/\s+AS\s+/i);
|
|
58
|
+
const fieldPart = parts[0].trim();
|
|
59
|
+
const aliasPart = parts[1].trim();
|
|
60
|
+
return `${this._escapeField(fieldPart)} AS ${aliasPart}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 处理表名.字段名的情况(多表联查)
|
|
64
|
+
if (field.includes('.')) {
|
|
65
|
+
const parts = field.split('.');
|
|
66
|
+
return parts
|
|
67
|
+
.map((part) => {
|
|
68
|
+
part = part.trim();
|
|
69
|
+
if (part === '*' || part.startsWith('`')) {
|
|
70
|
+
return part;
|
|
71
|
+
}
|
|
72
|
+
return `\`${part}\``;
|
|
73
|
+
})
|
|
74
|
+
.join('.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 处理单个字段名
|
|
78
|
+
return `\`${field}\``;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 转义表名
|
|
83
|
+
*/
|
|
84
|
+
private _escapeTable(table: string): string {
|
|
85
|
+
if (typeof table !== 'string') {
|
|
86
|
+
return table;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
table = table.trim();
|
|
90
|
+
|
|
91
|
+
if (table.startsWith('`')) {
|
|
92
|
+
return table;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 处理表别名(表名 + 空格 + 别名)
|
|
96
|
+
if (table.includes(' ')) {
|
|
97
|
+
const parts = table.split(/\s+/);
|
|
98
|
+
if (parts.length === 2) {
|
|
99
|
+
const tableName = parts[0].trim();
|
|
100
|
+
const alias = parts[1].trim();
|
|
101
|
+
return `\`${tableName}\` ${alias}`;
|
|
102
|
+
} else {
|
|
103
|
+
return table;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `\`${table}\``;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 验证参数
|
|
112
|
+
*/
|
|
113
|
+
private _validateParam(value: any): void {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
throw new Error(`参数值不能为 undefined`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 处理单个操作符条件
|
|
121
|
+
*/
|
|
122
|
+
private _applyOperator(fieldName: string, operator: WhereOperator, value: any): void {
|
|
123
|
+
const escapedField = this._escapeField(fieldName);
|
|
124
|
+
|
|
125
|
+
switch (operator) {
|
|
126
|
+
case '$ne':
|
|
127
|
+
case '$not':
|
|
128
|
+
this._validateParam(value);
|
|
129
|
+
this._where.push(`${escapedField} != ?`);
|
|
130
|
+
this._params.push(value);
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case '$in':
|
|
134
|
+
if (!Array.isArray(value)) {
|
|
135
|
+
throw new Error(`$in 操作符的值必须是数组 (operator: ${operator})`);
|
|
136
|
+
}
|
|
137
|
+
if (value.length === 0) {
|
|
138
|
+
throw new Error(`$in 操作符的数组不能为空。提示:空数组会导致查询永远不匹配任何记录,这通常不是预期行为。请检查查询条件或移除该字段。`);
|
|
139
|
+
}
|
|
140
|
+
const placeholders = value.map(() => '?').join(',');
|
|
141
|
+
this._where.push(`${escapedField} IN (${placeholders})`);
|
|
142
|
+
this._params.push(...value);
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case '$nin':
|
|
146
|
+
case '$notIn':
|
|
147
|
+
if (!Array.isArray(value)) {
|
|
148
|
+
throw new Error(`$nin/$notIn 操作符的值必须是数组 (operator: ${operator})`);
|
|
149
|
+
}
|
|
150
|
+
if (value.length === 0) {
|
|
151
|
+
throw new Error(`$nin/$notIn 操作符的数组不能为空。提示:空数组会导致查询匹配所有记录,这通常不是预期行为。请检查查询条件或移除该字段。`);
|
|
152
|
+
}
|
|
153
|
+
const placeholders2 = value.map(() => '?').join(',');
|
|
154
|
+
this._where.push(`${escapedField} NOT IN (${placeholders2})`);
|
|
155
|
+
this._params.push(...value);
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case '$like':
|
|
159
|
+
this._validateParam(value);
|
|
160
|
+
this._where.push(`${escapedField} LIKE ?`);
|
|
161
|
+
this._params.push(value);
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case '$notLike':
|
|
165
|
+
this._validateParam(value);
|
|
166
|
+
this._where.push(`${escapedField} NOT LIKE ?`);
|
|
167
|
+
this._params.push(value);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case '$gt':
|
|
171
|
+
this._validateParam(value);
|
|
172
|
+
this._where.push(`${escapedField} > ?`);
|
|
173
|
+
this._params.push(value);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case '$gte':
|
|
177
|
+
this._validateParam(value);
|
|
178
|
+
this._where.push(`${escapedField} >= ?`);
|
|
179
|
+
this._params.push(value);
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case '$lt':
|
|
183
|
+
this._validateParam(value);
|
|
184
|
+
this._where.push(`${escapedField} < ?`);
|
|
185
|
+
this._params.push(value);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case '$lte':
|
|
189
|
+
this._validateParam(value);
|
|
190
|
+
this._where.push(`${escapedField} <= ?`);
|
|
191
|
+
this._params.push(value);
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case '$between':
|
|
195
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
196
|
+
this._validateParam(value[0]);
|
|
197
|
+
this._validateParam(value[1]);
|
|
198
|
+
this._where.push(`${escapedField} BETWEEN ? AND ?`);
|
|
199
|
+
this._params.push(value[0], value[1]);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case '$notBetween':
|
|
204
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
205
|
+
this._validateParam(value[0]);
|
|
206
|
+
this._validateParam(value[1]);
|
|
207
|
+
this._where.push(`${escapedField} NOT BETWEEN ? AND ?`);
|
|
208
|
+
this._params.push(value[0], value[1]);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case '$null':
|
|
213
|
+
if (value === true) {
|
|
214
|
+
this._where.push(`${escapedField} IS NULL`);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case '$notNull':
|
|
219
|
+
if (value === true) {
|
|
220
|
+
this._where.push(`${escapedField} IS NOT NULL`);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
default:
|
|
225
|
+
// 等于条件
|
|
226
|
+
this._validateParam(value);
|
|
227
|
+
this._where.push(`${escapedField} = ?`);
|
|
228
|
+
this._params.push(value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 处理复杂的 WHERE 条件对象
|
|
234
|
+
*/
|
|
235
|
+
private _processWhereConditions(whereObj: WhereConditions): void {
|
|
236
|
+
if (!whereObj || typeof whereObj !== 'object') {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Object.entries(whereObj).forEach(([key, value]) => {
|
|
241
|
+
// 跳过undefined值
|
|
242
|
+
if (value === undefined) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (key === '$and') {
|
|
247
|
+
if (Array.isArray(value)) {
|
|
248
|
+
value.forEach((condition) => this._processWhereConditions(condition));
|
|
249
|
+
}
|
|
250
|
+
} else if (key === '$or') {
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
const orConditions: string[] = [];
|
|
253
|
+
const tempParams: SqlValue[] = [];
|
|
254
|
+
|
|
255
|
+
value.forEach((condition) => {
|
|
256
|
+
const tempBuilder = new SqlBuilder();
|
|
257
|
+
tempBuilder._processWhereConditions(condition);
|
|
258
|
+
if (tempBuilder._where.length > 0) {
|
|
259
|
+
orConditions.push(`(${tempBuilder._where.join(' AND ')})`);
|
|
260
|
+
tempParams.push(...tempBuilder._params);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (orConditions.length > 0) {
|
|
265
|
+
this._where.push(`(${orConditions.join(' OR ')})`);
|
|
266
|
+
this._params.push(...tempParams);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} else if (key.includes('$')) {
|
|
270
|
+
// 一级属性格式:age$gt, role$in 等
|
|
271
|
+
const lastDollarIndex = key.lastIndexOf('$');
|
|
272
|
+
const fieldName = key.substring(0, lastDollarIndex);
|
|
273
|
+
const operator = ('$' + key.substring(lastDollarIndex + 1)) as WhereOperator;
|
|
274
|
+
this._applyOperator(fieldName, operator, value);
|
|
275
|
+
} else {
|
|
276
|
+
// 检查值是否为对象(嵌套条件)
|
|
277
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
278
|
+
// 嵌套条件:如 { age: { $gt: 18 } }
|
|
279
|
+
for (const [op, val] of Object.entries(value)) {
|
|
280
|
+
this._applyOperator(key, op as WhereOperator, val);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
// 简单的等于条件
|
|
284
|
+
this._validateParam(value);
|
|
285
|
+
const escapedKey = this._escapeField(key);
|
|
286
|
+
this._where.push(`${escapedKey} = ?`);
|
|
287
|
+
this._params.push(value);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 获取 WHERE 条件(供 DbHelper 使用)
|
|
295
|
+
*/
|
|
296
|
+
getWhereConditions(): { sql: string; params: SqlValue[] } {
|
|
297
|
+
return {
|
|
298
|
+
sql: this._where.length > 0 ? this._where.join(' AND ') : '',
|
|
299
|
+
params: [...this._params]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* SELECT 字段
|
|
305
|
+
*/
|
|
306
|
+
select(fields: string | string[] = '*'): this {
|
|
307
|
+
if (Array.isArray(fields)) {
|
|
308
|
+
this._select = [...this._select, ...fields.map((field) => this._escapeField(field))];
|
|
309
|
+
} else if (typeof fields === 'string') {
|
|
310
|
+
this._select.push(this._escapeField(fields));
|
|
311
|
+
} else {
|
|
312
|
+
throw new Error('SELECT 字段必须是字符串或数组');
|
|
313
|
+
}
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* FROM 表名
|
|
319
|
+
*/
|
|
320
|
+
from(table: string): this {
|
|
321
|
+
if (typeof table !== 'string' || !table.trim()) {
|
|
322
|
+
throw new Error(`FROM 表名必须是非空字符串 (table: ${table})`);
|
|
323
|
+
}
|
|
324
|
+
this._from = this._escapeTable(table.trim());
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* WHERE 条件
|
|
330
|
+
*/
|
|
331
|
+
where(condition: WhereConditions | string, value?: SqlValue): this {
|
|
332
|
+
if (typeof condition === 'object' && condition !== null) {
|
|
333
|
+
this._processWhereConditions(condition);
|
|
334
|
+
} else if (value !== undefined && value !== null) {
|
|
335
|
+
this._validateParam(value);
|
|
336
|
+
const escapedCondition = this._escapeField(condition as string);
|
|
337
|
+
this._where.push(`${escapedCondition} = ?`);
|
|
338
|
+
this._params.push(value);
|
|
339
|
+
} else if (typeof condition === 'string') {
|
|
340
|
+
this._where.push(condition);
|
|
341
|
+
}
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* LEFT JOIN
|
|
347
|
+
*/
|
|
348
|
+
leftJoin(table: string, on: string): this {
|
|
349
|
+
if (typeof table !== 'string' || typeof on !== 'string') {
|
|
350
|
+
throw new Error(`JOIN 表名和条件必须是字符串 (table: ${table}, on: ${on})`);
|
|
351
|
+
}
|
|
352
|
+
const escapedTable = this._escapeTable(table);
|
|
353
|
+
this._joins.push(`LEFT JOIN ${escapedTable} ON ${on}`);
|
|
354
|
+
return this;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* ORDER BY
|
|
359
|
+
* @param fields - 格式为 ["field#ASC", "field2#DESC"]
|
|
360
|
+
*/
|
|
361
|
+
orderBy(fields: string[]): this {
|
|
362
|
+
if (!Array.isArray(fields)) {
|
|
363
|
+
throw new Error('orderBy 必须是字符串数组,格式为 "字段#方向"');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fields.forEach((item) => {
|
|
367
|
+
if (typeof item !== 'string' || !item.includes('#')) {
|
|
368
|
+
throw new Error(`orderBy 字段必须是 "字段#方向" 格式的字符串(例如:"name#ASC", "id#DESC") (item: ${item})`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const [fieldName, direction] = item.split('#');
|
|
372
|
+
const cleanField = fieldName.trim();
|
|
373
|
+
const cleanDir = direction.trim().toUpperCase() as OrderDirection;
|
|
374
|
+
|
|
375
|
+
if (!cleanField) {
|
|
376
|
+
throw new Error(`orderBy 中字段名不能为空 (item: ${item})`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!['ASC', 'DESC'].includes(cleanDir)) {
|
|
380
|
+
throw new Error(`ORDER BY 方向必须是 ASC 或 DESC (direction: ${cleanDir})`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const escapedField = this._escapeField(cleanField);
|
|
384
|
+
this._orderBy.push(`${escapedField} ${cleanDir}`);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return this;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* GROUP BY
|
|
392
|
+
*/
|
|
393
|
+
groupBy(field: string | string[]): this {
|
|
394
|
+
if (Array.isArray(field)) {
|
|
395
|
+
const escapedFields = field.filter((f) => typeof f === 'string').map((f) => this._escapeField(f));
|
|
396
|
+
this._groupBy = [...this._groupBy, ...escapedFields];
|
|
397
|
+
} else if (typeof field === 'string') {
|
|
398
|
+
this._groupBy.push(this._escapeField(field));
|
|
399
|
+
}
|
|
400
|
+
return this;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* HAVING
|
|
405
|
+
*/
|
|
406
|
+
having(condition: string): this {
|
|
407
|
+
if (typeof condition === 'string') {
|
|
408
|
+
this._having.push(condition);
|
|
409
|
+
}
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* LIMIT
|
|
415
|
+
*/
|
|
416
|
+
limit(count: number, offset?: number): this {
|
|
417
|
+
if (typeof count !== 'number' || count < 0) {
|
|
418
|
+
throw new Error(`LIMIT 数量必须是非负数 (count: ${count})`);
|
|
419
|
+
}
|
|
420
|
+
this._limit = Math.floor(count);
|
|
421
|
+
if (offset !== undefined && offset !== null) {
|
|
422
|
+
if (typeof offset !== 'number' || offset < 0) {
|
|
423
|
+
throw new Error(`OFFSET 必须是非负数 (offset: ${offset})`);
|
|
424
|
+
}
|
|
425
|
+
this._offset = Math.floor(offset);
|
|
426
|
+
}
|
|
427
|
+
return this;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* OFFSET
|
|
432
|
+
*/
|
|
433
|
+
offset(count: number): this {
|
|
434
|
+
if (typeof count !== 'number' || count < 0) {
|
|
435
|
+
throw new Error(`OFFSET 必须是非负数 (count: ${count})`);
|
|
436
|
+
}
|
|
437
|
+
this._offset = Math.floor(count);
|
|
438
|
+
return this;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 构建 SELECT 查询
|
|
443
|
+
*/
|
|
444
|
+
toSelectSql(): SqlQuery {
|
|
445
|
+
let sql = 'SELECT ';
|
|
446
|
+
|
|
447
|
+
sql += this._select.length > 0 ? this._select.join(', ') : '*';
|
|
448
|
+
|
|
449
|
+
if (!this._from) {
|
|
450
|
+
throw new Error('FROM 表名是必需的');
|
|
451
|
+
}
|
|
452
|
+
sql += ` FROM ${this._from}`;
|
|
453
|
+
|
|
454
|
+
if (this._joins.length > 0) {
|
|
455
|
+
sql += ' ' + this._joins.join(' ');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (this._where.length > 0) {
|
|
459
|
+
sql += ' WHERE ' + this._where.join(' AND ');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (this._groupBy.length > 0) {
|
|
463
|
+
sql += ' GROUP BY ' + this._groupBy.join(', ');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (this._having.length > 0) {
|
|
467
|
+
sql += ' HAVING ' + this._having.join(' AND ');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (this._orderBy.length > 0) {
|
|
471
|
+
sql += ' ORDER BY ' + this._orderBy.join(', ');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (this._limit !== null) {
|
|
475
|
+
sql += ` LIMIT ${this._limit}`;
|
|
476
|
+
if (this._offset !== null) {
|
|
477
|
+
sql += ` OFFSET ${this._offset}`;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return { sql, params: [...this._params] };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 构建 INSERT 查询
|
|
486
|
+
*/
|
|
487
|
+
toInsertSql(table: string, data: InsertData): SqlQuery {
|
|
488
|
+
if (!table || typeof table !== 'string') {
|
|
489
|
+
throw new Error(`INSERT 需要表名 (table: ${table})`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!data || typeof data !== 'object') {
|
|
493
|
+
throw new Error(`INSERT 需要数据 (table: ${table}, data: ${JSON.stringify(data)})`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const escapedTable = this._escapeTable(table);
|
|
497
|
+
|
|
498
|
+
if (Array.isArray(data)) {
|
|
499
|
+
if (data.length === 0) {
|
|
500
|
+
throw new Error(`插入数据不能为空 (table: ${table})`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const fields = Object.keys(data[0]);
|
|
504
|
+
if (fields.length === 0) {
|
|
505
|
+
throw new Error(`插入数据必须至少有一个字段 (table: ${table})`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const escapedFields = fields.map((field) => this._escapeField(field));
|
|
509
|
+
const placeholders = fields.map(() => '?').join(', ');
|
|
510
|
+
const values = data.map(() => `(${placeholders})`).join(', ');
|
|
511
|
+
|
|
512
|
+
const sql = `INSERT INTO ${escapedTable} (${escapedFields.join(', ')}) VALUES ${values}`;
|
|
513
|
+
const params = data.flatMap((row) => fields.map((field) => row[field]));
|
|
514
|
+
|
|
515
|
+
return { sql, params };
|
|
516
|
+
} else {
|
|
517
|
+
const fields = Object.keys(data);
|
|
518
|
+
if (fields.length === 0) {
|
|
519
|
+
throw new Error(`插入数据必须至少有一个字段 (table: ${table})`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const escapedFields = fields.map((field) => this._escapeField(field));
|
|
523
|
+
const placeholders = fields.map(() => '?').join(', ');
|
|
524
|
+
const sql = `INSERT INTO ${escapedTable} (${escapedFields.join(', ')}) VALUES (${placeholders})`;
|
|
525
|
+
const params = fields.map((field) => data[field]);
|
|
526
|
+
|
|
527
|
+
return { sql, params };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 构建 UPDATE 查询
|
|
533
|
+
*/
|
|
534
|
+
toUpdateSql(table: string, data: UpdateData): SqlQuery {
|
|
535
|
+
if (!table || typeof table !== 'string') {
|
|
536
|
+
throw new Error('UPDATE 需要表名');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
540
|
+
throw new Error('UPDATE 需要数据对象');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const fields = Object.keys(data);
|
|
544
|
+
if (fields.length === 0) {
|
|
545
|
+
throw new Error('更新数据必须至少有一个字段');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const escapedTable = this._escapeTable(table);
|
|
549
|
+
const setFields = fields.map((field) => `${this._escapeField(field)} = ?`);
|
|
550
|
+
const params: SqlValue[] = [...Object.values(data), ...this._params];
|
|
551
|
+
|
|
552
|
+
let sql = `UPDATE ${escapedTable} SET ${setFields.join(', ')}`;
|
|
553
|
+
|
|
554
|
+
if (this._where.length > 0) {
|
|
555
|
+
sql += ' WHERE ' + this._where.join(' AND ');
|
|
556
|
+
} else {
|
|
557
|
+
throw new Error('为安全起见,UPDATE 需要 WHERE 条件');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { sql, params };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 构建 DELETE 查询
|
|
565
|
+
*/
|
|
566
|
+
toDeleteSql(table: string): SqlQuery {
|
|
567
|
+
if (!table || typeof table !== 'string') {
|
|
568
|
+
throw new Error('DELETE 需要表名');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const escapedTable = this._escapeTable(table);
|
|
572
|
+
let sql = `DELETE FROM ${escapedTable}`;
|
|
573
|
+
|
|
574
|
+
if (this._where.length > 0) {
|
|
575
|
+
sql += ' WHERE ' + this._where.join(' AND ');
|
|
576
|
+
} else {
|
|
577
|
+
throw new Error('为安全起见,DELETE 需要 WHERE 条件');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return { sql, params: [...this._params] };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 构建 COUNT 查询
|
|
585
|
+
*/
|
|
586
|
+
toCountSql(): SqlQuery {
|
|
587
|
+
let sql = 'SELECT COUNT(*) as total';
|
|
588
|
+
|
|
589
|
+
if (!this._from) {
|
|
590
|
+
throw new Error('COUNT 需要 FROM 表名');
|
|
591
|
+
}
|
|
592
|
+
sql += ` FROM ${this._from}`;
|
|
593
|
+
|
|
594
|
+
if (this._joins.length > 0) {
|
|
595
|
+
sql += ' ' + this._joins.join(' ');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (this._where.length > 0) {
|
|
599
|
+
sql += ' WHERE ' + this._where.join(' AND ');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return { sql, params: [...this._params] };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* 创建新的 SQL 构建器实例
|
|
608
|
+
*/
|
|
609
|
+
export function createQueryBuilder(): SqlBuilder {
|
|
610
|
+
return new SqlBuilder();
|
|
611
|
+
}
|