@areumtecnologia/mysql-db-handler 1.0.8 → 1.0.10

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/README.md CHANGED
@@ -224,7 +224,9 @@ console.log('Inserted ID:', result.insertId);
224
224
 
225
225
  ### Updating Records (`update`)
226
226
 
227
- Performs selective updates by specifying both a `set` payload and a `where` filter.
227
+ Performs selective updates by specifying both a `set` payload and a `where` filter. The `where` filter supports both standard flat objects (where conditions are implicitly joined by `AND` using `=` or `REGEXP`) and complex array configurations (with operators, `AND`, and `OR`), sharing the same advanced querying capabilities as the `select` method.
228
+
229
+ #### 1. Simple Object Syntax (Implicit AND, = operator)
228
230
 
229
231
  ```javascript
230
232
  const result = await usersHandler.update({
@@ -239,6 +241,52 @@ const result = await usersHandler.update({
239
241
  });
240
242
 
241
243
  console.log('Affected rows:', result.affectedRows);
244
+ // Executes: UPDATE users SET `status` = ?, `notes` = ? WHERE `id` = ? AND `status` = ?
245
+ ```
246
+
247
+ #### 2. Advanced Array Syntax (with operators, AND/OR, and IN)
248
+
249
+ You can pass an array of condition objects interleaved with `AND` or `OR` string elements to construct complex queries:
250
+
251
+ ```javascript
252
+ // Example updating records based on numeric thresholds and logical operators
253
+ const result = await usersHandler.update({
254
+ set: {
255
+ status: 'inactive'
256
+ },
257
+ where: [
258
+ { age: 18, operator: '<' },
259
+ 'AND',
260
+ { status: 'active' }
261
+ ]
262
+ });
263
+ // Executes: UPDATE users SET `status` = ? WHERE `age` < ? AND `status` = ?
264
+
265
+ // Example updating records using automatic IN operator detection
266
+ const result = await usersHandler.update({
267
+ set: {
268
+ role: 'archived'
269
+ },
270
+ where: [
271
+ { id: [10, 20, 30] } // Automatically builds: WHERE `id` IN (?, ?, ?)
272
+ ]
273
+ });
274
+ // Executes: UPDATE users SET `role` = ? WHERE `id` IN (?, ?, ?)
275
+
276
+ // Example combining complex logic (AND/OR)
277
+ const result = await usersHandler.update({
278
+ set: {
279
+ category: 'Premium'
280
+ },
281
+ where: [
282
+ { total_purchases: 1000, operator: '>' },
283
+ 'AND',
284
+ { status: 'active' },
285
+ 'OR',
286
+ { is_vip: 1 }
287
+ ]
288
+ });
289
+ // Executes: UPDATE users SET `category` = ? WHERE `total_purchases` > ? AND `status` = ? OR `is_vip` = ?
242
290
  ```
243
291
 
244
292
  ### Deleting Records (`delete`)
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Áreum Tecnologia - Renan Moreira - renan.moreira@areum.com.br
2
+ const DataBaseCache = require('./lib/database-cache');
2
3
  const DataBase = require('./lib/database');
3
4
  const DataBaseHandler = require('./lib/databaseHandler');
4
5
  // We do not export the singleton by default in a library usually,
@@ -6,5 +7,6 @@ const DataBaseHandler = require('./lib/databaseHandler');
6
7
 
7
8
  module.exports = {
8
9
  DataBase,
9
- DataBaseHandler
10
+ DataBaseHandler,
11
+ DataBaseCache
10
12
  };
@@ -2,7 +2,7 @@
2
2
  const mysql = require('mysql2/promise');
3
3
  const NodeCache = require('node-cache');
4
4
 
5
- class DataBase {
5
+ class DataBaseCache {
6
6
  constructor(config = {}) {
7
7
  this.config = config;
8
8
  this.pool = mysql.createPool(this.config); // The pool is created once when the instance is created.
@@ -74,4 +74,4 @@ class DataBase {
74
74
  }
75
75
  }
76
76
 
77
- module.exports = DataBase;
77
+ module.exports = DataBaseCache;
package/lib/database.js CHANGED
@@ -31,8 +31,9 @@ class DataBase {
31
31
  * Executes a SQL query using a pooled connection.
32
32
  * @param {string} sql - The SQL query to execute.
33
33
  * @param {Array} [params] - The parameters for the SQL query.
34
- * @returns {Promise<Array>} The result set from the query.
35
- * @throws {Error} Throws an error if the query fails.
34
+ * @returns {Promise<Array|{error: Error}>} The result set from the query,
35
+ * or an object with an `error` property if the query fails.
36
+ * This prevents unhandled exceptions from crashing the application.
36
37
  */
37
38
  async query(sql, params) {
38
39
  let connection; // Mova a declaração da conexão para fora do bloco try
@@ -44,9 +45,12 @@ class DataBase {
44
45
  const [rows] = await connection.query(sql, params);
45
46
  return rows;
46
47
  } catch (error) {
47
- // Passo 3: Se a query falhar, logar e retornar o erro no formato desejado
48
+ // Passo 3: Logar o erro para diagnóstico
48
49
  console.error('Erro na execução da query:', error);
49
- return { error }; // Mantém o retorno do erro como um objeto
50
+
51
+ // Retorna o erro encapsulado em { error } para evitar crashes não tratados,
52
+ // mantendo o contrato documentado no README.
53
+ return { error };
50
54
  } finally {
51
55
  // Passo 4: Este bloco SEMPRE será executado, independentemente de sucesso ou falha no 'try'
52
56
  if (connection) {
@@ -49,19 +49,101 @@ class DatabaseHandler {
49
49
  const bindings = [];
50
50
  for (const item of strictCondition) {
51
51
  if (typeof item === 'string') {
52
+ // Operadores lógicos (AND / OR) são inseridos como-is, sem parênteses.
52
53
  clauses.push(item.toUpperCase());
53
54
  } else if (typeof item === 'object' && item !== null) {
54
55
  const key = Object.keys(item).find(k => k !== 'operator');
55
56
  if (!key) continue;
56
57
  const value = item[key];
57
- const operator = item.operator || '=';
58
- clauses.push(`\`${key}\` ${operator} ?`);
59
- bindings.push(value);
58
+ let operator = item.operator || '=';
59
+ let upperOp = String(operator).toUpperCase();
60
+
61
+ // Suporte a arrays => IN / NOT IN (consistente com _buildWhereClause)
62
+ if (Array.isArray(value) && upperOp === '=') {
63
+ operator = 'IN';
64
+ upperOp = 'IN';
65
+ }
66
+
67
+ if (Array.isArray(value) && (upperOp === 'IN' || upperOp === 'NOT IN')) {
68
+ if (value.length === 0) {
69
+ clauses.push(upperOp === 'IN' ? '1 = 0' : '1 = 1');
70
+ } else {
71
+ const placeholders = value.map(() => '?').join(', ');
72
+ clauses.push(`\`${key}\` ${operator} (${placeholders})`);
73
+ bindings.push(...value);
74
+ }
75
+ } else {
76
+ clauses.push(`\`${key}\` ${operator} ?`);
77
+ bindings.push(value);
78
+ }
60
79
  }
61
80
  }
81
+ // Junta com espaço para preservar operadores AND/OR intercalados.
82
+ // Ex: "(a = ? AND b = ?)" em vez de "(a = ? b = ?)" que era inválido.
62
83
  return { sql: `(${clauses.join(' ')})`, bindings };
63
84
  }
64
85
 
86
+ /**
87
+ * @private
88
+ * Constrói a cláusula WHERE (sem a palavra-chave WHERE) e seus bindings associados.
89
+ * @param {Object|Array} where - Condições do where.
90
+ * @param {boolean} useRegex - Se deve usar REGEXP por padrão (para compatibilidade com update).
91
+ * @returns {{ sql: string, bindings: Array }}
92
+ */
93
+ _buildWhereClause(where, useRegex = false) {
94
+ if (!where) {
95
+ return { sql: '', bindings: [] };
96
+ }
97
+
98
+ const conditions = [];
99
+ const bindings = [];
100
+
101
+ if (Array.isArray(where)) {
102
+ where.forEach((param) => {
103
+ if (typeof param === 'object' && param !== null && !Array.isArray(param)) {
104
+ const entries = Object.entries(param);
105
+ if (entries.length === 0) return;
106
+
107
+ const [key, value] = entries[0];
108
+ const [operatorKey, operatorValue] = entries[1] || [];
109
+
110
+ let op = operatorValue ?? (useRegex ? 'REGEXP' : '=');
111
+ let upperOp = String(op).toUpperCase();
112
+
113
+ if (Array.isArray(value) && upperOp === '=') {
114
+ op = 'IN';
115
+ upperOp = 'IN';
116
+ }
117
+
118
+ if (Array.isArray(value) && (upperOp === 'IN' || upperOp === 'NOT IN')) {
119
+ if (value.length === 0) {
120
+ conditions.push(upperOp === 'IN' ? '1 = 0' : '1 = 1');
121
+ } else {
122
+ const placeholders = value.map(() => '?').join(', ');
123
+ conditions.push(`\`${key}\` ${op} (${placeholders})`);
124
+ bindings.push(...value);
125
+ }
126
+ } else {
127
+ const condition = `\`${key}\` ${op} ?`;
128
+ conditions.push(condition);
129
+ bindings.push(value);
130
+ }
131
+ } else if (typeof param === 'string') {
132
+ conditions.push(param.toUpperCase());
133
+ }
134
+ });
135
+ return { sql: conditions.join(' '), bindings };
136
+ } else if (typeof where === 'object' && where !== null) {
137
+ const operator = useRegex ? 'REGEXP' : '=';
138
+ const keys = Object.keys(where);
139
+ const sql = keys.map(key => `\`${key}\` ${operator} ?`).join(' AND ');
140
+ const bindings = Object.values(where);
141
+ return { sql, bindings };
142
+ }
143
+
144
+ return { sql: '', bindings: [] };
145
+ }
146
+
65
147
  async selectToDatatable(rawDtQuery, strictCondition = []) {
66
148
  // NOVO: Hidrata o objeto da requisição para o formato aninhado correto.
67
149
  const dtQuery = this._hydrateObject(rawDtQuery);
@@ -121,12 +203,31 @@ class DatabaseHandler {
121
203
  // --- Queries ---
122
204
  const baseWhereSql = baseWhere.sql ? `WHERE ${baseWhere.sql}` : '';
123
205
  const totalCountSql = `SELECT COUNT(*) as total FROM ${this.table} ${baseWhereSql}`;
124
- const [totalResult] = await this.executeQuery(totalCountSql, baseWhere.bindings);
125
- const recordsTotal = totalResult.total;
206
+ const totalResult = await this.executeQuery(totalCountSql, baseWhere.bindings);
207
+ // Contrato: executeQuery propaga { error } em caso de falha (não lança).
208
+ if (totalResult && totalResult.error) {
209
+ return {
210
+ draw: parseInt(draw || 0),
211
+ recordsTotal: 0,
212
+ recordsFiltered: 0,
213
+ data: [],
214
+ error: totalResult.error.message
215
+ };
216
+ }
217
+ const recordsTotal = (totalResult && totalResult[0] && totalResult[0].total) || 0;
126
218
 
127
219
  const filteredCountSql = `SELECT COUNT(*) as total FROM ${this.table} ${whereSql}`;
128
- const [filteredResult] = await this.executeQuery(filteredCountSql, finalBindings);
129
- const recordsFiltered = filteredResult.total;
220
+ const filteredResult = await this.executeQuery(filteredCountSql, finalBindings);
221
+ if (filteredResult && filteredResult.error) {
222
+ return {
223
+ draw: parseInt(draw || 0),
224
+ recordsTotal: parseInt(recordsTotal),
225
+ recordsFiltered: 0,
226
+ data: [],
227
+ error: filteredResult.error.message
228
+ };
229
+ }
230
+ const recordsFiltered = (filteredResult && filteredResult[0] && filteredResult[0].total) || 0;
130
231
 
131
232
  // CORREÇÃO: Construção segura da cláusula SELECT para evitar vírgulas duplas.
132
233
  const selectFields = ['*'];
@@ -136,6 +237,15 @@ class DatabaseHandler {
136
237
  }
137
238
  const dataSql = `SELECT ${selectFields.join(', ')} FROM ${this.table} ${whereSql} ${orderSql} ${limitSql}`;
138
239
  const data = await this.executeQuery(dataSql, finalBindings);
240
+ if (data && data.error) {
241
+ return {
242
+ draw: parseInt(draw || 0),
243
+ recordsTotal: parseInt(recordsTotal),
244
+ recordsFiltered: parseInt(recordsFiltered),
245
+ data: [],
246
+ error: data.error.message
247
+ };
248
+ }
139
249
 
140
250
  return {
141
251
  draw: parseInt(draw || 0),
@@ -214,61 +324,8 @@ class DatabaseHandler {
214
324
  return new Error('O parâmetro "params" deve ser um array');
215
325
  }
216
326
 
217
- const conditions = [];
218
- const values = [];
219
-
220
- if (params && params.length) {
221
- // Dica: Troquei map por forEach. O map é para transformar arrays,
222
- // como você só está preenchendo variáveis externas, forEach é semanticamente correto.
223
- params.forEach((param) => {
224
- if (typeof param === 'object' && !Array.isArray(param)) {
225
- const entries = Object.entries(param);
226
- const [key, value] = entries[0];
227
- const [operatorKey, operatorValue] = entries[1] || [];
228
-
229
- // --- INÍCIO DAS MODIFICAÇÕES PARA SUPORTE AO IN ---
230
-
231
- // 1. Define o operador (padrão é '=')
232
- let op = operatorValue ?? '=';
233
- const upperOp = op.toUpperCase();
234
-
235
- // 2. "Magia": Se o valor for um array e não tiver operador (ou for '='),
236
- // converte automaticamente para 'IN' para facilitar sua vida.
237
- if (Array.isArray(value) && upperOp === '=') {
238
- op = 'IN';
239
- }
240
-
241
- // 3. Lógica específica para IN e NOT IN
242
- if (Array.isArray(value) && (upperOp === 'IN' || upperOp === 'NOT IN')) {
243
- if (value.length === 0) {
244
- // O SQL não permite "IN ()".
245
- // Um IN vazio nunca encontra nada (falso), um NOT IN vazio encontra tudo (verdadeiro).
246
- conditions.push(upperOp === 'IN' ? '1 = 0' : '1 = 1');
247
- } else {
248
- // Cria a string de placeholders: "?, ?, ?" baseada no tamanho do array
249
- const placeholders = value.map(() => '?').join(', ');
250
- conditions.push(`\`${key}\` ${op} (${placeholders})`);
251
-
252
- // Espalha os valores do array individualmente no array de values
253
- values.push(...value);
254
- }
255
- } else {
256
- // 4. Lógica original para valores únicos (string, number, boolean, etc)
257
- // ou outros operadores (LIKE, REGEXP, >, <, etc)
258
- const condition = `\`${key}\` ${op} ?`;
259
- conditions.push(condition);
260
- values.push(value);
261
- }
262
-
263
- // --- FIM DAS MODIFICAÇÕES ---
264
-
265
- } else if (typeof param === 'string') {
266
- conditions.push(param.toUpperCase());
267
- }
268
- });
269
- }
270
-
271
- const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' ')}` : '';
327
+ const { sql: whereSql, bindings: values } = this._buildWhereClause(params);
328
+ const whereClause = whereSql ? ` WHERE ${whereSql}` : '';
272
329
 
273
330
  // Cláusulas adicionais
274
331
  const groupByClause = clauses['GROUPBY'] ? ` GROUP BY ${clauses['GROUPBY']}` : '';
@@ -292,14 +349,16 @@ class DatabaseHandler {
292
349
 
293
350
  async update(params, options = {}) {
294
351
  // Inicializa a string SQL para a cláusula SET
295
- const operator = options.useRegex ? 'REGEXP' : '=';
296
352
  let setClause = Object.keys(params.set).map(key => `\`${key}\` = ?`).join(', ');
353
+ let setValues = Object.values(params.set);
354
+
297
355
  // Inicializa a string SQL para a cláusula WHERE
298
- let whereClause = Object.keys(params.where).map(key => `\`${key}\` ${operator} ?`).join(' AND ');
356
+ const { sql: whereClause, bindings: whereValues } = this._buildWhereClause(params.where, !!options.useRegex);
357
+
299
358
  // Constrói a consulta SQL completa
300
359
  const sql = `UPDATE ${this.table} SET ${setClause} WHERE ${whereClause}`;
301
360
  // Combina os valores SET e WHERE em um único array
302
- const values = [...Object.values(params.set), ...Object.values(params.where)];
361
+ const values = [...setValues, ...whereValues];
303
362
  // Executa a consulta
304
363
  const result = await this.executeQuery(sql, values);
305
364
 
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@areumtecnologia/mysql-db-handler",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "A powerful MySQL wrapper with connection pooling, singleton support, and advanced query handling for Node.js.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "node tests/update_operators.test.js && node tests/selectToDatatable.test.js"
8
8
  },
9
9
  "author": "Renan Moreira / Áreum Tecnologia",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "mysql2": "^3.22.5"
12
+ "mysql2": "^3.23.1"
13
13
  }
14
14
  }
@@ -1,19 +0,0 @@
1
- // Áreum Tecnologia - Renan Moreira - renan.moreira@areum.com.br
2
- // src/database/database-singleton.js
3
- // Exemplo Singleton para a conexão com o banco de dados
4
- const { DB_HOST, DB_USER, DB_PASS } = require('../constants');
5
- const DataBase = require("./database");
6
-
7
- const dbi = new DataBase({
8
- host: DB_HOST,
9
- user: DB_USER,
10
- password: DB_PASS,
11
- database: '',
12
- charset: "utf8mb4",
13
- timezone: "-03:00",
14
- connectionLimit: 1, // Ajustado para um limite de conexão mais comum
15
- waitForConnections: true,
16
- queueLimit: 0
17
- });
18
- // console.log("DATABASE SINGLETON CREATED", dbi.id);
19
- module.exports = dbi;