jerkjs 2.2.0 → 2.3.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.
@@ -0,0 +1,953 @@
1
+ /**
2
+ * QueryBuilder para MariaDB compatible con el framework JERK
3
+ * Implementación del componente query/queryBuilder.js
4
+ * Proporciona una interfaz fluida para construir consultas SQL complejas
5
+ */
6
+
7
+ class QueryBuilder {
8
+ /**
9
+ * Constructor del QueryBuilder
10
+ * @param {Object} adapter - Adaptador de base de datos (debería ser MariaDBAdapter)
11
+ * @param {string} tableName - Nombre de la tabla (opcional)
12
+ */
13
+ constructor(adapter = null, tableName = null) {
14
+ this.adapter = adapter;
15
+ this.tableName = tableName;
16
+
17
+ // Inicializar componentes de la consulta
18
+ this.selectFields = [];
19
+ this.whereConditions = [];
20
+ this.joins = [];
21
+ this.groupByFields = [];
22
+ this.havingConditions = [];
23
+ this.orderByFields = [];
24
+ this.limitValue = null;
25
+ this.offsetValue = null;
26
+ this.unionQueries = [];
27
+
28
+ // Parámetros para la consulta
29
+ this.parameters = [];
30
+ }
31
+
32
+ /**
33
+ * Establece el adaptador de base de datos para el QueryBuilder
34
+ * @param {Object} adapter - Adaptador de base de datos
35
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
36
+ */
37
+ setAdapter(adapter) {
38
+ this.adapter = adapter;
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Establece la tabla para la consulta
44
+ * @param {string} tableName - Nombre de la tabla
45
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
46
+ */
47
+ table(tableName) {
48
+ this.tableName = tableName;
49
+ return this;
50
+ }
51
+
52
+ /**
53
+ * Especifica los campos a seleccionar
54
+ * @param {...string} fields - Campos a seleccionar
55
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
56
+ */
57
+ select(...fields) {
58
+ if (fields.length === 1 && Array.isArray(fields[0])) {
59
+ this.selectFields = [...this.selectFields, ...fields[0]];
60
+ } else {
61
+ this.selectFields = [...this.selectFields, ...fields];
62
+ }
63
+ return this;
64
+ }
65
+
66
+ /**
67
+ * Agrega una condición WHERE
68
+ * @param {string|Object|Function} field - Campo, objeto de condiciones o función para condiciones complejas
69
+ * @param {string} [operator] - Operador (opcional)
70
+ * @param {*} [value] - Valor (opcional)
71
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
72
+ */
73
+ where(field, operator = null, value = null) {
74
+ // Si se pasa una función, manejarla como condición compleja
75
+ if (typeof field === 'function') {
76
+ // Ejecutar la función pasándole una instancia del QueryBuilder para construir condiciones anidadas
77
+ const subQueryBuilder = new QueryBuilder(this.adapter, this.tableName);
78
+ field(subQueryBuilder);
79
+
80
+ // Copiar las condiciones del subquery builder
81
+ this.whereConditions.push(...subQueryBuilder.whereConditions);
82
+ this.parameters.push(...subQueryBuilder.parameters);
83
+
84
+ return this;
85
+ }
86
+
87
+ if (typeof field === 'object') {
88
+ // Si se pasa un objeto, agregar múltiples condiciones AND
89
+ for (const [key, val] of Object.entries(field)) {
90
+ this.where(key, '=', val);
91
+ }
92
+ return this;
93
+ }
94
+
95
+ // Si solo se pasan dos argumentos, asumir que es campo = valor
96
+ if (arguments.length === 2) {
97
+ value = operator;
98
+ operator = '=';
99
+ }
100
+
101
+ // Escapar el campo para prevenir inyección SQL
102
+ const escapedField = this.escapeField(field);
103
+
104
+ this.whereConditions.push({
105
+ field: escapedField,
106
+ operator: operator,
107
+ value: '?',
108
+ condition: 'AND'
109
+ });
110
+
111
+ this.parameters.push(value);
112
+
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Agrega una condición WHERE OR
118
+ * @param {string|Object} field - Campo o objeto de condiciones
119
+ * @param {string} [operator] - Operador (opcional)
120
+ * @param {*} [value] - Valor (opcional)
121
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
122
+ */
123
+ orWhere(field, operator = null, value = null) {
124
+ if (typeof field === 'object') {
125
+ // Si se pasa un objeto, agregar múltiples condiciones OR
126
+ for (const [key, val] of Object.entries(field)) {
127
+ this.orWhere(key, '=', val);
128
+ }
129
+ return this;
130
+ }
131
+
132
+ // Si solo se pasan dos argumentos, asumir que es campo = valor
133
+ if (arguments.length === 2) {
134
+ value = operator;
135
+ operator = '=';
136
+ }
137
+
138
+ // Escapar el campo para prevenir inyección SQL
139
+ const escapedField = this.escapeField(field);
140
+
141
+ this.whereConditions.push({
142
+ field: escapedField,
143
+ operator: operator,
144
+ value: '?',
145
+ condition: 'OR'
146
+ });
147
+
148
+ this.parameters.push(value);
149
+
150
+ return this;
151
+ }
152
+
153
+ /**
154
+ * Agrega una condición WHERE IN
155
+ * @param {string} field - Campo
156
+ * @param {Array} values - Valores
157
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
158
+ */
159
+ whereIn(field, values) {
160
+ if (!Array.isArray(values)) {
161
+ throw new Error('Los valores para whereIn deben ser un array');
162
+ }
163
+
164
+ const escapedField = this.escapeField(field);
165
+ const placeholders = values.map(() => '?').join(', ');
166
+
167
+ this.whereConditions.push({
168
+ field: escapedField,
169
+ operator: 'IN',
170
+ value: `(${placeholders})`,
171
+ condition: 'AND'
172
+ });
173
+
174
+ this.parameters.push(...values);
175
+
176
+ return this;
177
+ }
178
+
179
+ /**
180
+ * Agrega una condición WHERE NOT IN
181
+ * @param {string} field - Campo
182
+ * @param {Array} values - Valores
183
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
184
+ */
185
+ whereNotIn(field, values) {
186
+ if (!Array.isArray(values)) {
187
+ throw new Error('Los valores para whereNotIn deben ser un array');
188
+ }
189
+
190
+ const escapedField = this.escapeField(field);
191
+ const placeholders = values.map(() => '?').join(', ');
192
+
193
+ this.whereConditions.push({
194
+ field: escapedField,
195
+ operator: 'NOT IN',
196
+ value: `(${placeholders})`,
197
+ condition: 'AND'
198
+ });
199
+
200
+ this.parameters.push(...values);
201
+
202
+ return this;
203
+ }
204
+
205
+ /**
206
+ * Agrega una condición WHERE BETWEEN
207
+ * @param {string} field - Campo
208
+ * @param {*} minValue - Valor mínimo
209
+ * @param {*} maxValue - Valor máximo
210
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
211
+ */
212
+ whereBetween(field, minValue, maxValue) {
213
+ const escapedField = this.escapeField(field);
214
+
215
+ this.whereConditions.push({
216
+ field: escapedField,
217
+ operator: 'BETWEEN',
218
+ value: '? AND ?',
219
+ condition: 'AND'
220
+ });
221
+
222
+ this.parameters.push(minValue, maxValue);
223
+
224
+ return this;
225
+ }
226
+
227
+ /**
228
+ * Agrega una condición WHERE NOT BETWEEN
229
+ * @param {string} field - Campo
230
+ * @param {*} minValue - Valor mínimo
231
+ * @param {*} maxValue - Valor máximo
232
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
233
+ */
234
+ whereNotBetween(field, minValue, maxValue) {
235
+ const escapedField = this.escapeField(field);
236
+
237
+ this.whereConditions.push({
238
+ field: escapedField,
239
+ operator: 'NOT BETWEEN',
240
+ value: '? AND ?',
241
+ condition: 'AND'
242
+ });
243
+
244
+ this.parameters.push(minValue, maxValue);
245
+
246
+ return this;
247
+ }
248
+
249
+ /**
250
+ * Agrega una condición WHERE LIKE
251
+ * @param {string} field - Campo
252
+ * @param {string} value - Valor para la comparación LIKE
253
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
254
+ */
255
+ whereLike(field, value) {
256
+ const escapedField = this.escapeField(field);
257
+
258
+ this.whereConditions.push({
259
+ field: escapedField,
260
+ operator: 'LIKE',
261
+ value: '?',
262
+ condition: 'AND'
263
+ });
264
+
265
+ this.parameters.push(value);
266
+
267
+ return this;
268
+ }
269
+
270
+ /**
271
+ * Agrega una condición WHERE NOT LIKE
272
+ * @param {string} field - Campo
273
+ * @param {string} value - Valor para la comparación NOT LIKE
274
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
275
+ */
276
+ whereNotLike(field, value) {
277
+ const escapedField = this.escapeField(field);
278
+
279
+ this.whereConditions.push({
280
+ field: escapedField,
281
+ operator: 'NOT LIKE',
282
+ value: '?',
283
+ condition: 'AND'
284
+ });
285
+
286
+ this.parameters.push(value);
287
+
288
+ return this;
289
+ }
290
+
291
+ /**
292
+ * Realiza un JOIN
293
+ * @param {string} table - Tabla a unir
294
+ * @param {string} first - Primer campo para la condición
295
+ * @param {string} operator - Operador de comparación
296
+ * @param {string} second - Segundo campo para la condición
297
+ * @param {string} type - Tipo de JOIN (INNER, LEFT, RIGHT, etc.)
298
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
299
+ */
300
+ join(table, first, operator, second, type = 'INNER') {
301
+ const escapedFirst = this.escapeField(first);
302
+ const escapedSecond = this.escapeField(second);
303
+
304
+ this.joins.push({
305
+ type: type,
306
+ table: table,
307
+ condition: `${escapedFirst} ${operator} ${escapedSecond}`
308
+ });
309
+
310
+ return this;
311
+ }
312
+
313
+ /**
314
+ * Realiza un LEFT JOIN
315
+ * @param {string} table - Tabla a unir
316
+ * @param {string} first - Primer campo para la condición
317
+ * @param {string} operator - Operador de comparación
318
+ * @param {string} second - Segundo campo para la condición
319
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
320
+ */
321
+ leftJoin(table, first, operator, second) {
322
+ return this.join(table, first, operator, second, 'LEFT');
323
+ }
324
+
325
+ /**
326
+ * Realiza un RIGHT JOIN
327
+ * @param {string} table - Tabla a unir
328
+ * @param {string} first - Primer campo para la condición
329
+ * @param {string} operator - Operador de comparación
330
+ * @param {string} second - Segundo campo para la condición
331
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
332
+ */
333
+ rightJoin(table, first, operator, second) {
334
+ return this.join(table, first, operator, second, 'RIGHT');
335
+ }
336
+
337
+ /**
338
+ * Realiza un INNER JOIN
339
+ * @param {string} table - Tabla a unir
340
+ * @param {string} first - Primer campo para la condición
341
+ * @param {string} operator - Operador de comparación
342
+ * @param {string} second - Segundo campo para la condición
343
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
344
+ */
345
+ innerJoin(table, first, operator, second) {
346
+ return this.join(table, first, operator, second, 'INNER');
347
+ }
348
+
349
+ /**
350
+ * Agrupa por campos
351
+ * @param {...string} fields - Campos para agrupar
352
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
353
+ */
354
+ groupBy(...fields) {
355
+ if (fields.length === 1 && Array.isArray(fields[0])) {
356
+ this.groupByFields = [...this.groupByFields, ...fields[0]];
357
+ } else {
358
+ this.groupByFields = [...this.groupByFields, ...fields];
359
+ }
360
+ return this;
361
+ }
362
+
363
+ /**
364
+ * Agrega una condición HAVING
365
+ * @param {string} field - Campo
366
+ * @param {string} operator - Operador
367
+ * @param {*} value - Valor
368
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
369
+ */
370
+ having(field, operator, value) {
371
+ const escapedField = this.escapeField(field);
372
+
373
+ this.havingConditions.push({
374
+ field: escapedField,
375
+ operator: operator,
376
+ value: '?'
377
+ });
378
+
379
+ this.parameters.push(value);
380
+
381
+ return this;
382
+ }
383
+
384
+ /**
385
+ * Ordena los resultados
386
+ * @param {string} field - Campo para ordenar
387
+ * @param {string} direction - Dirección (ASC o DESC)
388
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
389
+ */
390
+ orderBy(field, direction = 'ASC') {
391
+ const escapedField = this.escapeField(field);
392
+ this.orderByFields.push(`${escapedField} ${direction.toUpperCase()}`);
393
+ return this;
394
+ }
395
+
396
+ /**
397
+ * Limita el número de resultados
398
+ * @param {number} limit - Límite
399
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
400
+ */
401
+ limit(limit) {
402
+ this.limitValue = parseInt(limit);
403
+ return this;
404
+ }
405
+
406
+ /**
407
+ * Establece el desplazamiento para la paginación
408
+ * @param {number} offset - Desplazamiento
409
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
410
+ */
411
+ offset(offset) {
412
+ this.offsetValue = parseInt(offset);
413
+ return this;
414
+ }
415
+
416
+ /**
417
+ * Agrega una unión UNION con otra consulta
418
+ * @param {QueryBuilder} query - Otra instancia de QueryBuilder
419
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
420
+ */
421
+ union(query) {
422
+ if (!(query instanceof QueryBuilder)) {
423
+ throw new Error('La unión debe ser con otra instancia de QueryBuilder');
424
+ }
425
+
426
+ this.unionQueries.push(query);
427
+ return this;
428
+ }
429
+
430
+ /**
431
+ * Escapa un nombre de campo para prevenir inyección SQL
432
+ * @param {string|Function} field - Campo a escapar
433
+ * @returns {string} - Campo escapado
434
+ */
435
+ escapeField(field) {
436
+ // Si field es una función (caso especial para where(function(qb) {...})), no escapar
437
+ if (typeof field === 'function') {
438
+ return field;
439
+ }
440
+
441
+ // Si el campo es '*', no escapar (caso especial para SELECT *)
442
+ if (typeof field === 'string' && field === '*') {
443
+ return field;
444
+ }
445
+
446
+ // Verificar si el campo contiene una expresión o alias
447
+ if (typeof field === 'string' && (field.includes('(') || field.includes(' ') || field.includes('.'))) {
448
+ // Si es una expresión compleja o tiene alias, no escapar
449
+ return field;
450
+ }
451
+
452
+ // Si es un campo simple, escaparlo
453
+ return `\`${field}\``;
454
+ }
455
+
456
+ /**
457
+ * Genera la consulta SQL completa
458
+ * @returns {Object} - Objeto con la consulta SQL y los parámetros
459
+ */
460
+ toSQL() {
461
+ if (!this.tableName) {
462
+ throw new Error('Debe especificar una tabla usando el método table()');
463
+ }
464
+
465
+ let sql = 'SELECT ';
466
+
467
+ // Añadir campos SELECT
468
+ if (this.selectFields.length > 0) {
469
+ sql += this.selectFields.map(field => this.escapeField(field)).join(', ');
470
+ } else {
471
+ sql += '*';
472
+ }
473
+
474
+ // Añadir FROM
475
+ sql += ` FROM \`${this.tableName}\``;
476
+
477
+ // Añadir JOINs
478
+ for (const join of this.joins) {
479
+ sql += ` ${join.type} JOIN \`${join.table}\` ON ${join.condition}`;
480
+ }
481
+
482
+ // Añadir WHEREs
483
+ if (this.whereConditions.length > 0) {
484
+ sql += ' WHERE ';
485
+
486
+ const conditions = this.whereConditions.map((condition, index) => {
487
+ const prefix = index === 0 ? '' : ` ${condition.condition} `;
488
+ return `${prefix}${condition.field} ${condition.operator} ${condition.value}`;
489
+ });
490
+
491
+ sql += conditions.join('');
492
+ }
493
+
494
+ // Añadir GROUP BY
495
+ if (this.groupByFields.length > 0) {
496
+ sql += ' GROUP BY ' + this.groupByFields.map(field => this.escapeField(field)).join(', ');
497
+ }
498
+
499
+ // Añadir HAVING
500
+ if (this.havingConditions.length > 0) {
501
+ sql += ' HAVING ';
502
+
503
+ const havings = this.havingConditions.map((condition, index) => {
504
+ const prefix = index === 0 ? '' : ' AND ';
505
+ return `${prefix}${condition.field} ${condition.operator} ${condition.value}`;
506
+ });
507
+
508
+ sql += havings.join('');
509
+ }
510
+
511
+ // Añadir ORDER BY
512
+ if (this.orderByFields.length > 0) {
513
+ sql += ' ORDER BY ' + this.orderByFields.join(', ');
514
+ }
515
+
516
+ // Añadir LIMIT
517
+ if (this.limitValue !== null) {
518
+ sql += ` LIMIT ${this.limitValue}`;
519
+
520
+ if (this.offsetValue !== null) {
521
+ sql += ` OFFSET ${this.offsetValue}`;
522
+ }
523
+ }
524
+
525
+ // Añadir UNIONs
526
+ for (const unionQuery of this.unionQueries) {
527
+ const unionSQL = unionQuery.toSQL();
528
+ sql += ` UNION (${unionSQL.sql})`;
529
+ this.parameters.push(...unionSQL.params);
530
+ }
531
+
532
+ return {
533
+ sql: sql,
534
+ params: [...this.parameters]
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Ejecuta la consulta SELECT
540
+ * @returns {Promise<Array>} - Resultados de la consulta
541
+ */
542
+ async get() {
543
+ if (!this.adapter) {
544
+ throw new Error('No se ha configurado un adaptador para ejecutar la consulta. Use setAdapter() para configurar uno.');
545
+ }
546
+
547
+ const { sql, params } = this.toSQL();
548
+
549
+ // Disparar hook antes de ejecutar la consulta
550
+ const { hooks } = require('../index.js');
551
+ if (hooks) {
552
+ const hookResult = hooks.applyFilters('query_builder_before_execute', {
553
+ sql,
554
+ params,
555
+ queryBuilder: this
556
+ });
557
+
558
+ if (hookResult.cancelExecution) {
559
+ return hookResult.result || [];
560
+ }
561
+ }
562
+
563
+ try {
564
+ const results = await this.adapter.query(sql, params);
565
+
566
+ // Disparar hook después de ejecutar la consulta
567
+ if (hooks) {
568
+ hooks.doAction('query_builder_after_execute', {
569
+ sql,
570
+ params,
571
+ results,
572
+ queryBuilder: this
573
+ });
574
+ }
575
+
576
+ return results;
577
+ } catch (error) {
578
+ // Disparar hook en caso de error
579
+ if (hooks) {
580
+ hooks.doAction('query_builder_error', {
581
+ sql,
582
+ params,
583
+ error,
584
+ queryBuilder: this
585
+ });
586
+ }
587
+
588
+ throw error;
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Ejecuta la consulta SELECT y obtiene el primer resultado
594
+ * @returns {Promise<Object|null>} - Primer resultado o null
595
+ */
596
+ async first() {
597
+ this.limit(1);
598
+ const results = await this.get();
599
+ return results.length > 0 ? results[0] : null;
600
+ }
601
+
602
+ /**
603
+ * Cuenta el número de registros
604
+ * @returns {Promise<number>} - Número de registros
605
+ */
606
+ async count() {
607
+ // Guardar los campos originales
608
+ const originalSelectFields = [...this.selectFields];
609
+
610
+ // Establecer COUNT(*) como único campo
611
+ this.selectFields = ['COUNT(*) as count'];
612
+
613
+ // Ejecutar la consulta
614
+ const results = await this.get();
615
+
616
+ // Restaurar los campos originales
617
+ this.selectFields = originalSelectFields;
618
+
619
+ return results.length > 0 ? parseInt(results[0].count) : 0;
620
+ }
621
+
622
+ /**
623
+ * Obtiene el valor máximo de un campo
624
+ * @param {string} field - Campo
625
+ * @returns {Promise<*>} - Valor máximo
626
+ */
627
+ async max(field) {
628
+ // Guardar los campos originales
629
+ const originalSelectFields = [...this.selectFields];
630
+
631
+ // Establecer MAX(campo) como único campo
632
+ this.selectFields = [`MAX(${this.escapeField(field)}) as max_value`];
633
+
634
+ // Ejecutar la consulta
635
+ const results = await this.get();
636
+
637
+ // Restaurar los campos originales
638
+ this.selectFields = originalSelectFields;
639
+
640
+ return results.length > 0 ? results[0].max_value : null;
641
+ }
642
+
643
+ /**
644
+ * Obtiene el valor mínimo de un campo
645
+ * @param {string} field - Campo
646
+ * @returns {Promise<*>} - Valor mínimo
647
+ */
648
+ async min(field) {
649
+ // Guardar los campos originales
650
+ const originalSelectFields = [...this.selectFields];
651
+
652
+ // Establecer MIN(campo) como único campo
653
+ this.selectFields = [`MIN(${this.escapeField(field)}) as min_value`];
654
+
655
+ // Ejecutar la consulta
656
+ const results = await this.get();
657
+
658
+ // Restaurar los campos originales
659
+ this.selectFields = originalSelectFields;
660
+
661
+ return results.length > 0 ? results[0].min_value : null;
662
+ }
663
+
664
+ /**
665
+ * Calcula la suma de un campo
666
+ * @param {string} field - Campo
667
+ * @returns {Promise<number>} - Suma
668
+ */
669
+ async sum(field) {
670
+ // Guardar los campos originales
671
+ const originalSelectFields = [...this.selectFields];
672
+
673
+ // Establecer SUM(campo) como único campo
674
+ this.selectFields = [`SUM(${this.escapeField(field)}) as sum_value`];
675
+
676
+ // Ejecutar la consulta
677
+ const results = await this.get();
678
+
679
+ // Restaurar los campos originales
680
+ this.selectFields = originalSelectFields;
681
+
682
+ return results.length > 0 ? parseFloat(results[0].sum_value) : 0;
683
+ }
684
+
685
+ /**
686
+ * Calcula el promedio de un campo
687
+ * @param {string} field - Campo
688
+ * @returns {Promise<number>} - Promedio
689
+ */
690
+ async avg(field) {
691
+ // Guardar los campos originales
692
+ const originalSelectFields = [...this.selectFields];
693
+
694
+ // Establecer AVG(campo) como único campo
695
+ this.selectFields = [`AVG(${this.escapeField(field)}) as avg_value`];
696
+
697
+ // Ejecutar la consulta
698
+ const results = await this.get();
699
+
700
+ // Restaurar los campos originales
701
+ this.selectFields = originalSelectFields;
702
+
703
+ return results.length > 0 ? parseFloat(results[0].avg_value) : 0;
704
+ }
705
+
706
+ /**
707
+ * Inserta un registro
708
+ * @param {Object} data - Datos a insertar
709
+ * @returns {Promise<Object>} - Resultado de la inserción
710
+ */
711
+ async insert(data) {
712
+ if (!this.adapter) {
713
+ throw new Error('No se ha configurado un adaptador para ejecutar la inserción. Use setAdapter() para configurar uno.');
714
+ }
715
+
716
+ if (!this.tableName) {
717
+ throw new Error('Debe especificar una tabla usando el método table()');
718
+ }
719
+
720
+ const columns = Object.keys(data);
721
+ const values = Object.values(data);
722
+ const placeholders = columns.map(() => '?').join(', ');
723
+
724
+ const sql = `INSERT INTO \`${this.tableName}\` (\`${columns.join('`, `')}\`) VALUES (${placeholders})`;
725
+
726
+ // Disparar hook antes de ejecutar la inserción
727
+ const { hooks } = require('../index.js');
728
+ if (hooks) {
729
+ const hookResult = hooks.applyFilters('query_builder_before_insert', {
730
+ sql,
731
+ data,
732
+ queryBuilder: this
733
+ });
734
+
735
+ if (hookResult.cancelExecution) {
736
+ return hookResult.result || {};
737
+ }
738
+ }
739
+
740
+ try {
741
+ const result = await this.adapter.query(sql, values);
742
+
743
+ // Disparar hook después de ejecutar la inserción
744
+ if (hooks) {
745
+ hooks.doAction('query_builder_after_insert', {
746
+ sql,
747
+ data,
748
+ result,
749
+ queryBuilder: this
750
+ });
751
+ }
752
+
753
+ return result;
754
+ } catch (error) {
755
+ // Disparar hook en caso de error
756
+ if (hooks) {
757
+ hooks.doAction('query_builder_insert_error', {
758
+ sql,
759
+ data,
760
+ error,
761
+ queryBuilder: this
762
+ });
763
+ }
764
+
765
+ throw error;
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Actualiza registros
771
+ * @param {Object} data - Datos a actualizar
772
+ * @returns {Promise<number>} - Número de filas afectadas
773
+ */
774
+ async update(data) {
775
+ if (!this.adapter) {
776
+ throw new Error('No se ha configurado un adaptador para ejecutar la actualización. Use setAdapter() para configurar uno.');
777
+ }
778
+
779
+ if (!this.tableName) {
780
+ throw new Error('Debe especificar una tabla usando el método table()');
781
+ }
782
+
783
+ const updates = Object.keys(data).map(key => `\`${key}\` = ?`).join(', ');
784
+ const values = Object.values(data);
785
+
786
+ let sql = `UPDATE \`${this.tableName}\` SET ${updates}`;
787
+
788
+ // Añadir WHEREs
789
+ if (this.whereConditions.length > 0) {
790
+ sql += ' WHERE ';
791
+
792
+ const conditions = this.whereConditions.map((condition, index) => {
793
+ const prefix = index === 0 ? '' : ` ${condition.condition} `;
794
+ return `${prefix}${condition.field} ${condition.operator} ${condition.value}`;
795
+ });
796
+
797
+ sql += conditions.join('');
798
+ }
799
+
800
+ // Añadir ORDER BY
801
+ if (this.orderByFields.length > 0) {
802
+ sql += ' ORDER BY ' + this.orderByFields.join(', ');
803
+ }
804
+
805
+ // Añadir LIMIT
806
+ if (this.limitValue !== null) {
807
+ sql += ` LIMIT ${this.limitValue}`;
808
+ }
809
+
810
+ // Combinar los valores de actualización con los parámetros WHERE
811
+ const allParams = [...values, ...this.parameters];
812
+
813
+ // Disparar hook antes de ejecutar la actualización
814
+ const hooks = require('jerkjs').hooks;
815
+ if (hooks) {
816
+ const hookResult = hooks.applyFilters('query_builder_before_update', {
817
+ sql,
818
+ data,
819
+ queryBuilder: this
820
+ });
821
+
822
+ if (hookResult.cancelExecution) {
823
+ return hookResult.result || 0;
824
+ }
825
+ }
826
+
827
+ try {
828
+ const result = await this.adapter.query(sql, allParams);
829
+
830
+ // Disparar hook después de ejecutar la actualización
831
+ if (hooks) {
832
+ hooks.doAction('query_builder_after_update', {
833
+ sql,
834
+ data,
835
+ result,
836
+ queryBuilder: this
837
+ });
838
+ }
839
+
840
+ return result.affectedRows || 0;
841
+ } catch (error) {
842
+ // Disparar hook en caso de error
843
+ if (hooks) {
844
+ hooks.doAction('query_builder_update_error', {
845
+ sql,
846
+ data,
847
+ error,
848
+ queryBuilder: this
849
+ });
850
+ }
851
+
852
+ throw error;
853
+ }
854
+ }
855
+
856
+ /**
857
+ * Elimina registros
858
+ * @returns {Promise<number>} - Número de filas afectadas
859
+ */
860
+ async delete() {
861
+ if (!this.adapter) {
862
+ throw new Error('No se ha configurado un adaptador para ejecutar la eliminación. Use setAdapter() para configurar uno.');
863
+ }
864
+
865
+ if (!this.tableName) {
866
+ throw new Error('Debe especificar una tabla usando el método table()');
867
+ }
868
+
869
+ let sql = `DELETE FROM \`${this.tableName}\``;
870
+
871
+ // Añadir WHEREs
872
+ if (this.whereConditions.length > 0) {
873
+ sql += ' WHERE ';
874
+
875
+ const conditions = this.whereConditions.map((condition, index) => {
876
+ const prefix = index === 0 ? '' : ` ${condition.condition} `;
877
+ return `${prefix}${condition.field} ${condition.operator} ${condition.value}`;
878
+ });
879
+
880
+ sql += conditions.join('');
881
+ }
882
+
883
+ // Añadir ORDER BY
884
+ if (this.orderByFields.length > 0) {
885
+ sql += ' ORDER BY ' + this.orderByFields.join(', ');
886
+ }
887
+
888
+ // Añadir LIMIT
889
+ if (this.limitValue !== null) {
890
+ sql += ` LIMIT ${this.limitValue}`;
891
+ }
892
+
893
+ // Disparar hook antes de ejecutar la eliminación
894
+ const hooks = require('jerkjs').hooks;
895
+ if (hooks) {
896
+ const hookResult = hooks.applyFilters('query_builder_before_delete', {
897
+ sql,
898
+ queryBuilder: this
899
+ });
900
+
901
+ if (hookResult.cancelExecution) {
902
+ return hookResult.result || 0;
903
+ }
904
+ }
905
+
906
+ try {
907
+ const result = await this.adapter.query(sql, this.parameters);
908
+
909
+ // Disparar hook después de ejecutar la eliminación
910
+ if (hooks) {
911
+ hooks.doAction('query_builder_after_delete', {
912
+ sql,
913
+ result,
914
+ queryBuilder: this
915
+ });
916
+ }
917
+
918
+ return result.affectedRows || 0;
919
+ } catch (error) {
920
+ // Disparar hook en caso de error
921
+ if (hooks) {
922
+ hooks.doAction('query_builder_delete_error', {
923
+ sql,
924
+ error,
925
+ queryBuilder: this
926
+ });
927
+ }
928
+
929
+ throw error;
930
+ }
931
+ }
932
+
933
+ /**
934
+ * Reinicia todos los componentes de la consulta
935
+ * @returns {QueryBuilder} - Instancia del QueryBuilder para encadenamiento
936
+ */
937
+ reset() {
938
+ this.selectFields = [];
939
+ this.whereConditions = [];
940
+ this.joins = [];
941
+ this.groupByFields = [];
942
+ this.havingConditions = [];
943
+ this.orderByFields = [];
944
+ this.limitValue = null;
945
+ this.offsetValue = null;
946
+ this.unionQueries = [];
947
+ this.parameters = [];
948
+
949
+ return this;
950
+ }
951
+ }
952
+
953
+ module.exports = QueryBuilder;