db-crud-wrapper 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/mysql.js ADDED
@@ -0,0 +1,532 @@
1
+ 'use strict';
2
+
3
+ import sql from 'mysql2/promise';
4
+ import log from './log.js'
5
+
6
+ // Regex
7
+ const _match_LIMIT_n = /\bLIMIT\b +\d+/ig
8
+ const _match_TOP_n = /\bTOP\b +\d+/ig
9
+ const _match_TOP = /\bTOP\b/ig
10
+
11
+ const pools = {};
12
+
13
+ // Common
14
+ function isExpression(str) {
15
+ if (typeof str !== 'string') return false;
16
+ const trimmed = str.trim();
17
+ return (trimmed.startsWith('[') && trimmed.endsWith(']')) ||
18
+ (trimmed.startsWith('`') && trimmed.endsWith('`'));
19
+ }
20
+
21
+ function removeExpressionDelimiters(str) {
22
+ if (typeof str !== 'string') return str;
23
+ const trimmed = str.trim();
24
+ if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
25
+ (trimmed.startsWith('`') && trimmed.endsWith('`'))) {
26
+ return trimmed.slice(1, -1);
27
+ }
28
+ return str;
29
+ }
30
+
31
+ function stringifyValue(fieldName, value, tSchema) {
32
+
33
+ // null or undefined
34
+ if (value == undefined)
35
+ return 'null';
36
+
37
+ // if values is an expression than remove delimiters
38
+ if (isExpression(value)) {
39
+ return removeExpressionDelimiters(value);
40
+ }
41
+
42
+ // detect field type
43
+ let _fieldType = undefined;
44
+ if (tSchema.table.fields && tSchema.table.fields[fieldName] && tSchema.table.fields[fieldName].type) {
45
+ _fieldType = tSchema.table.fields[fieldName].type;
46
+ }
47
+
48
+ // if datetime
49
+ if (_fieldType == 'datetime') {
50
+ // my-sql not accepts 'Z' at end of ISO string
51
+ if (value instanceof Date)
52
+ return `\'${value.toISOString().slice(0, -1)}\'`;
53
+ if (typeof value == 'string') {
54
+ const valueDate = new Date(Date.parse(value));
55
+ return `\'${valueDate.toISOString().slice(0, -1)}\'`;
56
+ }
57
+ return value;
58
+ }
59
+
60
+ // if boolean
61
+ if (_fieldType == 'boolean' && typeof value == 'boolean')
62
+ return `\'${value}\'`;
63
+
64
+ // if string or uuid
65
+ if (_fieldType == 'string' || _fieldType == 'uuid') {
66
+ return sql.escape(`${value}`);
67
+ }
68
+
69
+ // field not in schema
70
+ if (_fieldType == undefined) {
71
+ if (value instanceof Date)
72
+ return `\'${value.toISOString().slice(0, -1)}\'`;
73
+ if (typeof value == 'boolean')
74
+ return `\'${value}\'`;
75
+ if (typeof value == 'string')
76
+ return sql.escape(`${value}`);
77
+ }
78
+
79
+ return value;
80
+ }
81
+
82
+
83
+
84
+ // Create config object for pool
85
+ export function prepareConnection(tSchema) {
86
+ return {
87
+ id: `${tSchema.server.realName}-${tSchema.database.realName}-${tSchema.server.user}`,
88
+ host: ((tSchema.server.instance && tSchema.server.instance != 'DEFAULT') ? (tSchema.server.realName + '\\' + tSchema.server.instance) : tSchema.server.realName) ?? 'localhost',
89
+ port: tSchema.server.port,
90
+ user: tSchema.server.user,
91
+ password: tSchema.server.password,
92
+ database: tSchema.database.realName,
93
+ ssl: {
94
+ rejectUnauthorized: false
95
+ },
96
+ multipleStatements: true,
97
+ connectionLimit: tSchema.server.connectionLimit,
98
+ enableKeepAlive: true,
99
+ keepAliveInitialDelay: 30000,
100
+ timezone: 'Z' // Forza l'uso dell'UTC (Zulu time)
101
+ };
102
+ };
103
+
104
+ // Close connection
105
+ export async function closeConnection(connection) {
106
+ let pool = pools[connection.id];
107
+ if (pool) {
108
+ pool.releaseConnection();
109
+ await connection.end();
110
+ pools[connection.id] = undefined; // remove pool from pools
111
+ }
112
+ }
113
+
114
+ // Close all connections
115
+ export async function closeAllConnections() {
116
+ for (let poolId in pools) {
117
+ if (!pools.hasOwnProperty(poolId)) continue;
118
+ let pool = pools[poolId];
119
+ if (pool) {
120
+ await pool.end();
121
+ pool = undefined; // remove pool from pools
122
+ }
123
+ }
124
+ }
125
+
126
+ // Test connection
127
+ export async function testConnection(connection) {
128
+ let _pool;
129
+ try {
130
+ // const _connection = await sql.createConnection(connection);
131
+ // await _connection.end();
132
+ _pool = new sql.createPool(connection); // Create new pool
133
+ await _pool.query('SELECT 1');
134
+ return true;
135
+ }
136
+ catch (error) {
137
+ throw (error);
138
+ }
139
+ finally {
140
+ if (_pool) await _pool.end();
141
+ }
142
+ }
143
+
144
+ // Query
145
+ export async function query(connection, dbOpes, rawResult = false) {
146
+
147
+ // Prepare pool
148
+ let pool = pools[connection.id]; // Try to get an existing pool
149
+ if (!pool) {
150
+ pool = new sql.createPool(connection); // Create new pool
151
+ pools[connection.id] = pool; // add new pool to pools
152
+ }
153
+
154
+ // Prepare sql statement
155
+ let sqlString = '';
156
+ const paramsObject = {};
157
+ let appLog = true;
158
+
159
+ if (Array.isArray(dbOpes)) {
160
+ dbOpes.forEach((dbOpe, index) => {
161
+ sqlString += ToSql(dbOpe, paramsObject); // if exists, input params will be added to paramsObject
162
+ if (dbOpe?.hasOwnProperty('appLog') && dbOpe.appLog === false) appLog = false;
163
+ });
164
+ }
165
+ else {
166
+ sqlString += ToSql(dbOpes, paramsObject); // if exists, input params will be added to paramsObject
167
+ if (dbOpes?.hasOwnProperty('appLog') && dbOpes.appLog === false) appLog = false;
168
+ }
169
+
170
+ sqlString = normalizeSpecialName(sqlString);
171
+
172
+ // convert @param to ? and build params array
173
+ const _translated = translateParamsToMysql(sqlString, paramsObject);
174
+ sqlString = _translated.sqlString;
175
+ const sqlParams = _translated.paramsArray;
176
+
177
+ // Log
178
+ if (appLog) {
179
+ log(JSON.stringify(dbOpes), 60);
180
+ log(sqlString, 50);
181
+ }
182
+
183
+ // Run query
184
+ let sqlresult = undefined;
185
+ //let sqlconn = undefined;
186
+ try {
187
+ // sqlconn = await pool.getConnection();
188
+ // sqlresult = await sqlconn.query(sqlString);
189
+ sqlresult = await pool.query(sqlString, sqlParams);
190
+ }
191
+ catch (err) { throw (err); } // using original error
192
+ // finally { if (sqlconn) sqlconn.release(); }
193
+
194
+ // normalize return object
195
+ const _return = normalizeQueryResults(sqlresult);
196
+
197
+ // Log
198
+ if (appLog) {
199
+ if (Array.isArray(dbOpes)) // multiple query
200
+ log(`Batch return ${Number.isInteger(_return) ? _return : Array.isArray(_return) ? _return.length : _return ? 1 : 0} result(s).`, 50)
201
+ else // single query
202
+ log(`Query result: ${Number.isInteger(_return) ? _return : Array.isArray(_return) ? _return.length : _return ? 1 : 0} row(s).`, 50)
203
+ }
204
+
205
+ // return
206
+ return rawResult ? sqlresult : _return;
207
+ }
208
+
209
+ // Normalize special name to replace square brackets with backticks, considering quoted strings
210
+ function normalizeSpecialName(sql) {
211
+ let result = "";
212
+ let inSingleQuote = false;
213
+ let inDoubleQuote = false;
214
+
215
+ for (let i = 0; i < sql.length; i++) {
216
+ const char = sql[i];
217
+
218
+ // Gestione delle virgolette singole '
219
+ if (char === "'" && !inDoubleQuote) {
220
+ inSingleQuote = !inSingleQuote;
221
+ }
222
+ // Gestione delle virgolette doppie "
223
+ else if (char === '"' && !inSingleQuote) {
224
+ inDoubleQuote = !inDoubleQuote;
225
+ }
226
+
227
+ // Se non siamo all'interno di una stringa, sostituiamo le quadre
228
+ if (!inSingleQuote && !inDoubleQuote) {
229
+ if (char === '[' || char === ']') {
230
+ result += '`';
231
+ continue;
232
+ }
233
+ }
234
+
235
+ result += char;
236
+ }
237
+
238
+ return result;
239
+ }
240
+
241
+ // Compose fully qualified table name
242
+ function fullyQualifiedTableName(tSchema) {
243
+ return (tSchema.database.realName + '.' + tSchema.table.realName);
244
+ }
245
+
246
+ function containLIMIT(s) {
247
+ const _return = s?.match(_match_LIMIT_n);
248
+ if (_return) return _return[0];
249
+ else return undefined;
250
+ };
251
+
252
+ function containTOP(s) {
253
+ const _return = s?.match(_match_TOP_n);
254
+ if (_return) return _return[0];
255
+ else return undefined;
256
+ };
257
+
258
+ function replaceTOPToLIMIT(s) {
259
+ const _replace = containTOP(s);
260
+ if (_replace) { return s.replace(_match_TOP_n, _replace.replace(_match_TOP, 'LIMIT')); }
261
+ else return s;
262
+ }
263
+
264
+ /**
265
+ * Normalizza l'output di mysql2 ignorando i campi (fields).
266
+ * @param {Array} rawResponse - L'intero array restituito dalla query()
267
+ * @return {Array|number|Array<number>} - Restituisce i risultati normalizzati:
268
+ * - Per query SELECT singole: un array di record.
269
+ * - Per query di modifica singole (INSERT, UPDATE, DELETE): il numero di righe interessate.
270
+ * - Per batch di query (MULTIPLE STATEMENTS): un array dove ogni elemento è
271
+ * o un array di record (per SELECT) o un intero (per modifiche).
272
+ */
273
+ function normalizeQueryResults(rawResponse) {
274
+ // 1. Prendiamo solo il primo elemento (i dati/risultati)
275
+ // mysql2 restituisce sempre [risultati, campi].
276
+ const results = rawResponse[0];
277
+
278
+ // Helper per processare ogni singola unità di risultato
279
+ const normalizeItem = (item) => {
280
+ // Se è un array, è una SELECT -> restituisco l'array di record
281
+ if (Array.isArray(item)) return item;
282
+ // Se è un oggetto (ResultSetHeader), restituisco l'intero affectedRows
283
+ if (item && typeof item === 'object') return item.affectedRows ?? 0;
284
+ return 0;
285
+ };
286
+
287
+ // 2. Controllo se siamo in modalità MULTIPLE STATEMENTS (Batch or stored procedure)
288
+ // Se è un batch, results è un array dove il primo elemento è a sua volta
289
+ // un array (SELECT) o un oggetto (ResultSetHeader).
290
+ const isBatch = Array.isArray(results) &&
291
+ results.length > 0 &&
292
+ (Array.isArray(results[0]) || results[0]?.constructor?.name === 'ResultSetHeader');
293
+
294
+ if (isBatch) {
295
+ // Mappiamo ogni query del batch trasformandola in [Array o Intero]
296
+ return results.map(normalizeItem);
297
+ }
298
+
299
+ // 3. Caso Query Singola
300
+ return normalizeItem(results);
301
+ }
302
+
303
+ // Parse oprations object to sql string
304
+ function ToSql(dbOpe, paramsObject) {
305
+ if (dbOpe.get) return getToSql(dbOpe, paramsObject);
306
+ if (dbOpe.post) return postToSql(dbOpe, paramsObject);
307
+ if (dbOpe.patch) return patchToSql(dbOpe, paramsObject);
308
+ if (dbOpe.put) return putToSql(dbOpe, paramsObject);
309
+ if (dbOpe.delete) return deleteToSql(dbOpe, paramsObject);
310
+ if (dbOpe.command) return commandToSql(dbOpe, paramsObject);
311
+ if (dbOpe.begin) return beginToSql(dbOpe, paramsObject);
312
+ if (dbOpe.commit) return commitToSql(dbOpe, paramsObject);
313
+ if (dbOpe.passthrough) return passthroughToSql(dbOpe, paramsObject);
314
+ throw new Error('Sintax error, missing property get/post/patch/put/delete/...');
315
+ }
316
+
317
+ // Parse operation object to sql string without any trasformation.
318
+ function passthroughToSql(dbOpe, paramsObject) {
319
+ let result = "";
320
+
321
+ if (dbOpe.passthrough.command) {
322
+ result += dbOpe.passthrough.command;
323
+ }
324
+ else { throw new Error('command is missing.'); }
325
+
326
+ if (dbOpe.passthrough.params) {
327
+ Object.assign(paramsObject, dbOpe.passthrough.params);
328
+ }
329
+
330
+ return result;
331
+ }
332
+
333
+ // Parse get operation object to sql string
334
+ function getToSql(dbOpe, paramsObject) {
335
+ let _first = true;
336
+
337
+ let result = 'select'
338
+
339
+ // omit 'limit' or 'top'
340
+ if (dbOpe.get.options && dbOpe.get.options.length > 0) {
341
+ if (Array.isArray(dbOpe.get.options)) {
342
+ for (const s of dbOpe.get.options) { if (!containLIMIT(s) && !containTOP(s)) result += ' ' + s; }
343
+ }
344
+ else if (!containLIMIT(dbOpe.get.options) && !containTOP(dbOpe.get.options)) result += ' ' + dbOpe.get.options;
345
+ }
346
+
347
+ if (dbOpe.get.fields && dbOpe.get.fields.length > 0) {
348
+ if (Array.isArray(dbOpe.get.fields)) result += ' ' + dbOpe.get.fields.join(',')
349
+ else result += ' ' + dbOpe.get.fields;
350
+ }
351
+ else { throw new Error('fields is missing.'); }
352
+
353
+ result += ' from ' + fullyQualifiedTableName(dbOpe.get.schema);
354
+
355
+ if (dbOpe.get.filters && dbOpe.get.filters.length > 0) {
356
+ if (Array.isArray(dbOpe.get.filters)) result += ' where ' + dbOpe.get.filters.join(' ');
357
+ else result += ' where ' + dbOpe.get.filters;
358
+ }
359
+
360
+ if (dbOpe.get.groupBy && dbOpe.get.groupBy.length > 0) {
361
+ if (Array.isArray(dbOpe.get.groupBy)) result += ' group by ' + dbOpe.get.groupBy.join(', ');
362
+ else result += ' group by ' + dbOpe.get.groupBy;
363
+ }
364
+
365
+ if (dbOpe.get.groupFilters && dbOpe.get.groupFilters.length > 0) {
366
+ if (Array.isArray(dbOpe.get.groupFilters)) result += ' having ' + dbOpe.get.groupFilters.join(' ');
367
+ else result += ' having ' + dbOpe.get.groupFilters;
368
+ }
369
+
370
+ if (dbOpe.get.orderBy && dbOpe.get.orderBy.length > 0) {
371
+ if (Array.isArray(dbOpe.get.orderBy)) result += ' order by ' + dbOpe.get.orderBy.join(', ');
372
+ else result += ' order by ' + dbOpe.get.orderBy;
373
+ }
374
+
375
+ // search if 'limit' or 'top'
376
+ if (dbOpe.get.options && dbOpe.get.options.length > 0) {
377
+ if (Array.isArray(dbOpe.get.options)) {
378
+ for (const s of dbOpe.get.options) { if (containLIMIT(s) || containTOP(s)) result += ' ' + replaceTOPToLIMIT(s); }
379
+ }
380
+ else if (containLIMIT(dbOpe.get.options) || containTOP(dbOpe.get.options)) result += ' ' + replaceTOPToLIMIT(dbOpe.get.options);
381
+ }
382
+
383
+ if (dbOpe.get.params) {
384
+ Object.assign(paramsObject, dbOpe.get.params);
385
+ }
386
+
387
+ return result += ';' ;
388
+ }
389
+
390
+ // Parse post operation object to sql string
391
+ function postToSql(dbOpe, paramsObject) {
392
+ let result = 'insert into ' + fullyQualifiedTableName(dbOpe.post.schema);
393
+
394
+ if ((dbOpe.post.values) && (Object.keys(dbOpe.post.values).length > 0)) {
395
+ let result_f = ' (';
396
+ let result_v = ' values (';
397
+ let _first = true;
398
+ for (const [key, value] of Object.entries(dbOpe.post.values)) {
399
+ if (!_first) {
400
+ result_f += ', ';
401
+ result_v += ', ';
402
+ } else {
403
+ _first = false
404
+ }
405
+ if (key || value) {
406
+ result_f += key;
407
+ result_v += stringifyValue(key, value, dbOpe.post.schema);
408
+ }
409
+ }
410
+ result_f += ')'; result_v += ')';
411
+ result += result_f + result_v;
412
+ }
413
+ else { throw new Error('values is missing.'); }
414
+
415
+ if (dbOpe.post.params) {
416
+ Object.assign(paramsObject, dbOpe.post.params);
417
+ }
418
+
419
+ return result += ';' ;
420
+ }
421
+
422
+ // Parse patch operation object to sql string
423
+ function patchToSql(dbOpe, paramsObject) {
424
+ let result = 'update ' + fullyQualifiedTableName(dbOpe.patch.schema);
425
+
426
+ if (dbOpe.patch.values) { result += ' set ' + Object.entries(dbOpe.patch.values).map(e => { return e[0] + '=' + stringifyValue(e[0], e[1], dbOpe.patch.schema) }).join(', '); }
427
+ else { throw new Error('values is missing.'); }
428
+
429
+ if (dbOpe.patch.filters && dbOpe.patch.filters.length > 0) {
430
+ if (Array.isArray(dbOpe.patch.filters)) result += ' where ' + dbOpe.patch.filters.join(' ');
431
+ else result += ' where ' + dbOpe.patch.filters;
432
+ }
433
+
434
+ if (dbOpe.patch.params) {
435
+ Object.assign(paramsObject, dbOpe.patch.params);
436
+ }
437
+
438
+ return result += ';' ;
439
+ }
440
+
441
+ // Parse put operation object to sql string
442
+ function putToSql(dbOpe, paramsObject) {
443
+ let result = 'update ' + fullyQualifiedTableName(dbOpe.put.schema);
444
+
445
+ if (dbOpe.put.values) { result += ' set ' + Object.entries(dbOpe.put.values).map(e => { return e[0] + '=' + stringifyValue(e[0], e[1], dbOpe.put.schema) }).join(', '); }
446
+ else { throw new Error('values is missing.'); }
447
+
448
+ if (dbOpe.put.filters && dbOpe.put.filters.length > 0) {
449
+ if (Array.isArray(dbOpe.put.filters)) result += ' where ' + dbOpe.put.filters.join(' ');
450
+ else result += ' where ' + dbOpe.put.filters;
451
+ }
452
+
453
+ if (dbOpe.put.params) {
454
+ Object.assign(paramsObject, dbOpe.put.params);
455
+ }
456
+
457
+ return result += ';' ;
458
+ }
459
+
460
+ // Parse delete operation object to sql string and consolidate params to paramsObject
461
+ function deleteToSql(dbOpe, paramsObject) {
462
+ let result = 'delete from ' + fullyQualifiedTableName(dbOpe.delete.schema);
463
+
464
+ if (dbOpe.delete.filters && dbOpe.delete.filters.length > 0) {
465
+ if (Array.isArray(dbOpe.delete.filters)) result += ' where ' + dbOpe.delete.filters.join(' ');
466
+ else result += ' where ' + dbOpe.delete.filters;
467
+ }
468
+
469
+ if (dbOpe.delete.params) {
470
+ Object.assign(paramsObject, dbOpe.delete.params);
471
+ }
472
+
473
+ return result += ';' ;
474
+ }
475
+
476
+ // Parse command operation object to sql string
477
+ function commandToSql(dbOpe, paramsObject) {
478
+ let result = "CALL ";
479
+
480
+ if (dbOpe.command.schema.procedure) {
481
+ result += dbOpe.command.schema.procedure.command;
482
+ if (dbOpe.command.arguments) {
483
+ // if argument is string but not param, will be wrap into ''
484
+ result += ('(' + [].concat(dbOpe.command.arguments).map(item => {
485
+ return (typeof item === 'string' && !item.trimStart().startsWith("@")) ? `'${item}'` : item
486
+ }).join(', ') + ')');
487
+ }
488
+ }
489
+ else { throw new Error('missing procedure/function name.'); }
490
+
491
+ if (dbOpe.command.params) {
492
+ Object.assign(paramsObject, dbOpe.command.params);
493
+ }
494
+
495
+ return result += ';' ;
496
+ }
497
+
498
+ // Parse begin operation object to sql string
499
+ function beginToSql(dbOpe, paramsObject) {
500
+ return "START TRANSACTION; ";
501
+ }
502
+
503
+ // Parse commit operation object to sql string
504
+ function commitToSql(dbOpe, paramsObject) {
505
+ return " COMMIT;";
506
+ }
507
+
508
+ /**
509
+ * Converte una query da formato MSSQL (@param) a MySQL (?)
510
+ * @param {string} sqlString - La stringa SQL con segnaposti in formato MSSQL
511
+ * @param {Object} params - Oggetto con i valori { nome: 'valore' }
512
+ * @returns {Object} { sqlString: string, params: Array }
513
+ */
514
+ function translateParamsToMysql(sqlString, params) {
515
+ const _params = [];
516
+
517
+ // Regex per trovare i segnaposti che iniziano con @ seguiti da caratteri alfanumerici
518
+ // Usa un lookahead negativo per evitare di catturare @ nelle email se necessario
519
+ const regex = /@([a-zA-Z0-9_]+)/g;
520
+
521
+ const _sqlString = sqlString.replace(regex, (match, name) => {
522
+ if (Object.prototype.hasOwnProperty.call(params, name)) {
523
+ _params.push(params[name]);
524
+ return '?';
525
+ }
526
+ // Se non è nell'oggetto params, è probabilmente una variabile MySQL
527
+ // o parte di una stringa, quindi la lasciamo intatta
528
+ return match;
529
+ });
530
+
531
+ return { sqlString: _sqlString, paramsArray: _params };
532
+ }