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