befly 1.2.9 → 2.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/utils/curd.js ADDED
@@ -0,0 +1,419 @@
1
+ /**
2
+ * SQL 构造器 - 生产级稳定版本
3
+ */
4
+ export class SqlBuilder {
5
+ constructor() {
6
+ this.reset();
7
+ }
8
+
9
+ reset() {
10
+ this._select = [];
11
+ this._from = '';
12
+ this._where = [];
13
+ this._joins = [];
14
+ this._orderBy = [];
15
+ this._groupBy = [];
16
+ this._having = [];
17
+ this._limit = null;
18
+ this._offset = null;
19
+ this._params = [];
20
+ return this;
21
+ }
22
+
23
+ select(fields = '*') {
24
+ if (Array.isArray(fields)) {
25
+ this._select = [...this._select, ...fields];
26
+ } else if (typeof fields === 'string') {
27
+ this._select.push(fields);
28
+ } else {
29
+ throw new Error('SELECT fields must be string or array');
30
+ }
31
+ return this;
32
+ }
33
+
34
+ from(table) {
35
+ if (typeof table !== 'string' || !table.trim()) {
36
+ throw new Error('FROM table must be a non-empty string');
37
+ }
38
+ this._from = table.trim();
39
+ return this;
40
+ }
41
+
42
+ // 安全的参数验证
43
+ _validateParam(value) {
44
+ if (value === undefined) {
45
+ throw new Error('Parameter value cannot be undefined');
46
+ }
47
+ return value;
48
+ }
49
+
50
+ // 处理复杂的 where 条件对象
51
+ _processWhereConditions(whereObj) {
52
+ if (!whereObj || typeof whereObj !== 'object') {
53
+ return;
54
+ }
55
+
56
+ Object.entries(whereObj).forEach(([key, value]) => {
57
+ if (key === '$and') {
58
+ if (Array.isArray(value)) {
59
+ value.forEach((condition) => this._processWhereConditions(condition));
60
+ }
61
+ } else if (key === '$or') {
62
+ if (Array.isArray(value)) {
63
+ const orConditions = [];
64
+ const tempParams = [];
65
+
66
+ value.forEach((condition) => {
67
+ const tempBuilder = new SqlBuilder();
68
+ tempBuilder._processWhereConditions(condition);
69
+ if (tempBuilder._where.length > 0) {
70
+ orConditions.push(`(${tempBuilder._where.join(' AND ')})`);
71
+ tempParams.push(...tempBuilder._params);
72
+ }
73
+ });
74
+
75
+ if (orConditions.length > 0) {
76
+ this._where.push(`(${orConditions.join(' OR ')})`);
77
+ this._params.push(...tempParams);
78
+ }
79
+ }
80
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
81
+ // 字段级操作符
82
+ Object.entries(value).forEach(([operator, operatorValue]) => {
83
+ this._validateParam(operatorValue);
84
+
85
+ switch (operator) {
86
+ case '$ne':
87
+ case '$not':
88
+ this._where.push(`${key} != ?`);
89
+ this._params.push(operatorValue);
90
+ break;
91
+ case '$in':
92
+ if (Array.isArray(operatorValue) && operatorValue.length > 0) {
93
+ const placeholders = operatorValue.map(() => '?').join(',');
94
+ this._where.push(`${key} IN (${placeholders})`);
95
+ this._params.push(...operatorValue);
96
+ }
97
+ break;
98
+ case '$nin':
99
+ case '$notIn':
100
+ if (Array.isArray(operatorValue) && operatorValue.length > 0) {
101
+ const placeholders = operatorValue.map(() => '?').join(',');
102
+ this._where.push(`${key} NOT IN (${placeholders})`);
103
+ this._params.push(...operatorValue);
104
+ }
105
+ break;
106
+ case '$like':
107
+ this._where.push(`${key} LIKE ?`);
108
+ this._params.push(operatorValue);
109
+ break;
110
+ case '$notLike':
111
+ this._where.push(`${key} NOT LIKE ?`);
112
+ this._params.push(operatorValue);
113
+ break;
114
+ case '$gt':
115
+ this._where.push(`${key} > ?`);
116
+ this._params.push(operatorValue);
117
+ break;
118
+ case '$gte':
119
+ this._where.push(`${key} >= ?`);
120
+ this._params.push(operatorValue);
121
+ break;
122
+ case '$lt':
123
+ this._where.push(`${key} < ?`);
124
+ this._params.push(operatorValue);
125
+ break;
126
+ case '$lte':
127
+ this._where.push(`${key} <= ?`);
128
+ this._params.push(operatorValue);
129
+ break;
130
+ case '$between':
131
+ if (Array.isArray(operatorValue) && operatorValue.length === 2) {
132
+ this._where.push(`${key} BETWEEN ? AND ?`);
133
+ this._params.push(operatorValue[0], operatorValue[1]);
134
+ }
135
+ break;
136
+ case '$notBetween':
137
+ if (Array.isArray(operatorValue) && operatorValue.length === 2) {
138
+ this._where.push(`${key} NOT BETWEEN ? AND ?`);
139
+ this._params.push(operatorValue[0], operatorValue[1]);
140
+ }
141
+ break;
142
+ case '$null':
143
+ if (operatorValue === true) {
144
+ this._where.push(`${key} IS NULL`);
145
+ }
146
+ break;
147
+ case '$notNull':
148
+ if (operatorValue === true) {
149
+ this._where.push(`${key} IS NOT NULL`);
150
+ }
151
+ break;
152
+ default:
153
+ this._where.push(`${key} = ?`);
154
+ this._params.push(operatorValue);
155
+ }
156
+ });
157
+ } else {
158
+ // 简单的等于条件
159
+ this._validateParam(value);
160
+ this._where.push(`${key} = ?`);
161
+ this._params.push(value);
162
+ }
163
+ });
164
+ }
165
+
166
+ where(condition, value = null) {
167
+ if (typeof condition === 'object' && condition !== null) {
168
+ this._processWhereConditions(condition);
169
+ } else if (value !== null) {
170
+ this._validateParam(value);
171
+ this._where.push(`${condition} = ?`);
172
+ this._params.push(value);
173
+ } else if (typeof condition === 'string') {
174
+ this._where.push(condition);
175
+ }
176
+ return this;
177
+ }
178
+
179
+ leftJoin(table, on) {
180
+ if (typeof table !== 'string' || typeof on !== 'string') {
181
+ throw new Error('JOIN table and condition must be strings');
182
+ }
183
+ this._joins.push(`LEFT JOIN ${table} ON ${on}`);
184
+ return this;
185
+ }
186
+
187
+ orderBy(field, direction = 'ASC') {
188
+ if (Array.isArray(field)) {
189
+ field.forEach((item) => {
190
+ if (typeof item === 'string' && item.includes('#')) {
191
+ const [fieldName, dir] = item.split('#');
192
+ const cleanDir = (dir || 'ASC').trim().toUpperCase();
193
+ if (!['ASC', 'DESC'].includes(cleanDir)) {
194
+ throw new Error('ORDER BY direction must be ASC or DESC');
195
+ }
196
+ this._orderBy.push(`${fieldName.trim()} ${cleanDir}`);
197
+ } else if (Array.isArray(item) && item.length >= 1) {
198
+ const [fieldName, dir] = item;
199
+ const cleanDir = (dir || 'ASC').toUpperCase();
200
+ if (!['ASC', 'DESC'].includes(cleanDir)) {
201
+ throw new Error('ORDER BY direction must be ASC or DESC');
202
+ }
203
+ this._orderBy.push(`${fieldName} ${cleanDir}`);
204
+ } else if (typeof item === 'string') {
205
+ this._orderBy.push(`${item} ASC`);
206
+ }
207
+ });
208
+ } else if (typeof field === 'string') {
209
+ if (field.includes('#')) {
210
+ const [fieldName, dir] = field.split('#');
211
+ const cleanDir = (dir || 'ASC').trim().toUpperCase();
212
+ if (!['ASC', 'DESC'].includes(cleanDir)) {
213
+ throw new Error('ORDER BY direction must be ASC or DESC');
214
+ }
215
+ this._orderBy.push(`${fieldName.trim()} ${cleanDir}`);
216
+ } else {
217
+ const cleanDir = direction.toUpperCase();
218
+ if (!['ASC', 'DESC'].includes(cleanDir)) {
219
+ throw new Error('ORDER BY direction must be ASC or DESC');
220
+ }
221
+ this._orderBy.push(`${field} ${cleanDir}`);
222
+ }
223
+ }
224
+ return this;
225
+ }
226
+
227
+ groupBy(field) {
228
+ if (Array.isArray(field)) {
229
+ this._groupBy = [...this._groupBy, ...field.filter((f) => typeof f === 'string')];
230
+ } else if (typeof field === 'string') {
231
+ this._groupBy.push(field);
232
+ }
233
+ return this;
234
+ }
235
+
236
+ having(condition) {
237
+ if (typeof condition === 'string') {
238
+ this._having.push(condition);
239
+ }
240
+ return this;
241
+ }
242
+
243
+ limit(count, offset = null) {
244
+ if (typeof count !== 'number' || count < 0) {
245
+ throw new Error('LIMIT count must be a non-negative number');
246
+ }
247
+ this._limit = Math.floor(count);
248
+ if (offset !== null) {
249
+ if (typeof offset !== 'number' || offset < 0) {
250
+ throw new Error('OFFSET must be a non-negative number');
251
+ }
252
+ this._offset = Math.floor(offset);
253
+ }
254
+ return this;
255
+ }
256
+
257
+ offset(count) {
258
+ if (typeof count !== 'number' || count < 0) {
259
+ throw new Error('OFFSET must be a non-negative number');
260
+ }
261
+ this._offset = Math.floor(count);
262
+ return this;
263
+ }
264
+
265
+ // 构建 SELECT 查询
266
+ toSelectSql() {
267
+ let sql = 'SELECT ';
268
+
269
+ sql += this._select.length > 0 ? this._select.join(', ') : '*';
270
+
271
+ if (!this._from) {
272
+ throw new Error('FROM table is required');
273
+ }
274
+ sql += ` FROM ${this._from}`;
275
+
276
+ if (this._joins.length > 0) {
277
+ sql += ' ' + this._joins.join(' ');
278
+ }
279
+
280
+ if (this._where.length > 0) {
281
+ sql += ' WHERE ' + this._where.join(' AND ');
282
+ }
283
+
284
+ if (this._groupBy.length > 0) {
285
+ sql += ' GROUP BY ' + this._groupBy.join(', ');
286
+ }
287
+
288
+ if (this._having.length > 0) {
289
+ sql += ' HAVING ' + this._having.join(' AND ');
290
+ }
291
+
292
+ if (this._orderBy.length > 0) {
293
+ sql += ' ORDER BY ' + this._orderBy.join(', ');
294
+ }
295
+
296
+ if (this._limit !== null) {
297
+ sql += ` LIMIT ${this._limit}`;
298
+ if (this._offset !== null) {
299
+ sql += ` OFFSET ${this._offset}`;
300
+ }
301
+ }
302
+
303
+ return { sql, params: [...this._params] };
304
+ }
305
+
306
+ // 构建 INSERT 查询
307
+ toInsertSql(table, data) {
308
+ if (!table || typeof table !== 'string') {
309
+ throw new Error('Table name is required for INSERT');
310
+ }
311
+
312
+ if (!data || typeof data !== 'object') {
313
+ throw new Error('Data is required for INSERT');
314
+ }
315
+
316
+ if (Array.isArray(data)) {
317
+ if (data.length === 0) {
318
+ throw new Error('Insert data cannot be empty');
319
+ }
320
+
321
+ const fields = Object.keys(data[0]);
322
+ if (fields.length === 0) {
323
+ throw new Error('Insert data must have at least one field');
324
+ }
325
+
326
+ const placeholders = fields.map(() => '?').join(', ');
327
+ const values = data.map(() => `(${placeholders})`).join(', ');
328
+
329
+ const sql = `INSERT INTO ${table} (${fields.join(', ')}) VALUES ${values}`;
330
+ const params = data.flatMap((row) => fields.map((field) => row[field]));
331
+
332
+ return { sql, params };
333
+ } else {
334
+ const fields = Object.keys(data);
335
+ if (fields.length === 0) {
336
+ throw new Error('Insert data must have at least one field');
337
+ }
338
+
339
+ const placeholders = fields.map(() => '?').join(', ');
340
+ const sql = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${placeholders})`;
341
+ const params = fields.map((field) => data[field]);
342
+
343
+ return { sql, params };
344
+ }
345
+ }
346
+
347
+ // 构建 UPDATE 查询
348
+ toUpdateSql(table, data) {
349
+ if (!table || typeof table !== 'string') {
350
+ throw new Error('Table name is required for UPDATE');
351
+ }
352
+
353
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
354
+ throw new Error('Data object is required for UPDATE');
355
+ }
356
+
357
+ const fields = Object.keys(data);
358
+ if (fields.length === 0) {
359
+ throw new Error('Update data must have at least one field');
360
+ }
361
+
362
+ const setFields = fields.map((field) => `${field} = ?`);
363
+ const params = [...Object.values(data), ...this._params];
364
+
365
+ let sql = `UPDATE ${table} SET ${setFields.join(', ')}`;
366
+
367
+ if (this._where.length > 0) {
368
+ sql += ' WHERE ' + this._where.join(' AND ');
369
+ } else {
370
+ throw new Error('UPDATE requires WHERE condition for safety');
371
+ }
372
+
373
+ return { sql, params };
374
+ }
375
+
376
+ // 构建 DELETE 查询
377
+ toDeleteSql(table) {
378
+ if (!table || typeof table !== 'string') {
379
+ throw new Error('Table name is required for DELETE');
380
+ }
381
+
382
+ let sql = `DELETE FROM ${table}`;
383
+
384
+ if (this._where.length > 0) {
385
+ sql += ' WHERE ' + this._where.join(' AND ');
386
+ } else {
387
+ throw new Error('DELETE requires WHERE condition for safety');
388
+ }
389
+
390
+ return { sql, params: [...this._params] };
391
+ }
392
+
393
+ // 构建 COUNT 查询
394
+ toCountSql() {
395
+ let sql = 'SELECT COUNT(*) as total';
396
+
397
+ if (!this._from) {
398
+ throw new Error('FROM table is required for COUNT');
399
+ }
400
+ sql += ` FROM ${this._from}`;
401
+
402
+ if (this._joins.length > 0) {
403
+ sql += ' ' + this._joins.join(' ');
404
+ }
405
+
406
+ if (this._where.length > 0) {
407
+ sql += ' WHERE ' + this._where.join(' AND ');
408
+ }
409
+
410
+ return { sql, params: [...this._params] };
411
+ }
412
+ }
413
+
414
+ /**
415
+ * 创建新的 SQL 构造器实例
416
+ */
417
+ export function createQueryBuilder() {
418
+ return new SqlBuilder();
419
+ }