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.
Files changed (87) hide show
  1. package/checks/conflict.ts +329 -0
  2. package/checks/table.ts +252 -0
  3. package/config/env.ts +218 -0
  4. package/config/fields.ts +55 -0
  5. package/config/regexAliases.ts +51 -0
  6. package/config/reserved.ts +96 -0
  7. package/main.ts +47 -0
  8. package/package.json +26 -11
  9. package/plugins/db.ts +60 -0
  10. package/plugins/logger.ts +28 -0
  11. package/plugins/redis.ts +47 -0
  12. package/scripts/syncDb/apply.ts +171 -0
  13. package/scripts/syncDb/constants.ts +71 -0
  14. package/scripts/syncDb/ddl.ts +189 -0
  15. package/scripts/syncDb/helpers.ts +173 -0
  16. package/scripts/syncDb/index.ts +203 -0
  17. package/scripts/syncDb/schema.ts +199 -0
  18. package/scripts/syncDb/sqlite.ts +50 -0
  19. package/scripts/syncDb/state.ts +106 -0
  20. package/scripts/syncDb/table.ts +214 -0
  21. package/scripts/syncDb/tableCreate.ts +148 -0
  22. package/scripts/syncDb/tests/constants.test.ts +105 -0
  23. package/scripts/syncDb/tests/ddl.test.ts +134 -0
  24. package/scripts/syncDb/tests/helpers.test.ts +70 -0
  25. package/scripts/syncDb/types.ts +92 -0
  26. package/scripts/syncDb/version.ts +73 -0
  27. package/scripts/syncDb.ts +10 -0
  28. package/tsconfig.json +58 -0
  29. package/types/addon.d.ts +53 -0
  30. package/types/api.d.ts +249 -0
  31. package/types/befly.d.ts +230 -0
  32. package/types/common.d.ts +215 -0
  33. package/types/context.d.ts +7 -0
  34. package/types/crypto.d.ts +23 -0
  35. package/types/database.d.ts +273 -0
  36. package/types/index.d.ts +450 -0
  37. package/types/index.ts +438 -0
  38. package/types/jwt.d.ts +99 -0
  39. package/types/logger.d.ts +43 -0
  40. package/types/plugin.d.ts +109 -0
  41. package/types/redis.d.ts +46 -0
  42. package/types/tool.d.ts +67 -0
  43. package/types/validator.d.ts +43 -0
  44. package/types/validator.ts +43 -0
  45. package/utils/colors.ts +221 -0
  46. package/utils/crypto.ts +308 -0
  47. package/utils/database.ts +348 -0
  48. package/utils/dbHelper.ts +713 -0
  49. package/utils/helper.ts +812 -0
  50. package/utils/index.ts +33 -0
  51. package/utils/jwt.ts +493 -0
  52. package/utils/logger.ts +191 -0
  53. package/utils/redisHelper.ts +321 -0
  54. package/utils/requestContext.ts +167 -0
  55. package/utils/sqlBuilder.ts +611 -0
  56. package/utils/validate.ts +493 -0
  57. package/utils/{xml.js → xml.ts} +100 -74
  58. package/.npmrc +0 -3
  59. package/.prettierignore +0 -2
  60. package/.prettierrc +0 -11
  61. package/apis/health/info.js +0 -49
  62. package/apis/tool/tokenCheck.js +0 -29
  63. package/bin/befly.js +0 -109
  64. package/bunfig.toml +0 -3
  65. package/checks/table.js +0 -206
  66. package/config/env.js +0 -64
  67. package/main.js +0 -579
  68. package/plugins/db.js +0 -46
  69. package/plugins/logger.js +0 -14
  70. package/plugins/redis.js +0 -32
  71. package/plugins/tool.js +0 -8
  72. package/scripts/syncDb.js +0 -752
  73. package/scripts/syncDev.js +0 -96
  74. package/system.js +0 -118
  75. package/tables/common.json +0 -16
  76. package/tables/tool.json +0 -6
  77. package/utils/api.js +0 -27
  78. package/utils/colors.js +0 -83
  79. package/utils/crypto.js +0 -260
  80. package/utils/index.js +0 -334
  81. package/utils/jwt.js +0 -387
  82. package/utils/logger.js +0 -143
  83. package/utils/redisHelper.js +0 -74
  84. package/utils/sqlBuilder.js +0 -498
  85. package/utils/sqlManager.js +0 -471
  86. package/utils/tool.js +0 -31
  87. 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
+ }