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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +201 -0
- package/README.md +4 -1
- package/README_EN.md +230 -0
- package/README_PT.md +230 -0
- package/docs/ARQUITECTURA_ROUTES.md +186 -0
- package/docs/EXTENSION_MANUAL.md +955 -0
- package/docs/FIREWALL_MANUAL.md +416 -0
- package/docs/HOOK-2.0.md +512 -0
- package/docs/HOOKS_REFERENCE_IMPROVED.md +596 -0
- package/docs/JERK_FRAMEWORK_DIAGRAM.txt +492 -0
- package/docs/JERK_FRAMEWORK_DIAGRAM_MERMAID.mmd +124 -0
- package/docs/JERK_FRAMEWORK_DOCUMENTATION.md +553 -0
- package/docs/JERK_MODELOS_HOWTO.md +566 -0
- package/docs/MANUAL_API_SDK.md +536 -0
- package/docs/MARIADB_TOKENS_IMPLEMENTATION.md +110 -0
- package/docs/MIDDLEWARE_MANUAL.md +518 -0
- package/docs/OAUTH2_GOOGLE_MANUAL.md +405 -0
- package/docs/ROUTING_WITHOUT_JSON_GUIDE.md +454 -0
- package/docs/frontend-and-sessions.md +353 -0
- package/docs/guia_inicio_rapido_jerkjs.md +113 -0
- package/examples/examples.arj +0 -0
- package/index.js +12 -1
- package/jerk-qbuilder/CHANGELOG.md +71 -0
- package/jerk-qbuilder/HOWTO.md +325 -0
- package/jerk-qbuilder/README.md +52 -0
- package/lib/mvc/controllerBase.js +31 -14
- package/lib/query/MariaDBAdapter.js +78 -0
- package/lib/query/consoleAdapter.js +184 -0
- package/lib/query/queryBuilder.js +953 -0
- package/lib/query/queryBuilderHooks.js +455 -0
- package/lib/query/queryBuilderMiddleware.js +332 -0
- package/package.json +2 -2
- package/utils/find_file_path.sh +36 -0
|
@@ -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;
|