@bkmj/node-red-contrib-odbcmj 2.3.0 → 2.3.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.
Files changed (2) hide show
  1. package/odbc.js +140 -53
  2. package/package.json +1 -1
package/odbc.js CHANGED
@@ -12,10 +12,11 @@ module.exports = function (RED) {
12
12
 
13
13
  this.credentials = RED.nodes.getCredentials(this.id);
14
14
 
15
- // NOUVEAU: Timeout par défaut pour les requêtes (0 = infini/défaut du driver)
16
- // Sera configurable dans le .html plus tard
17
- this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10) || 0;
18
- // NOUVEAU: Timeout fixe pour les opérations de fermeture (en ms)
15
+ this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10);
16
+ if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
17
+ this.config.queryTimeoutSeconds = 0;
18
+ }
19
+
19
20
  this.closeOperationTimeout = 10000; // 10 secondes
20
21
 
21
22
  this._buildConnectionString = function() {
@@ -36,8 +37,6 @@ module.exports = function (RED) {
36
37
  if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
37
38
  if (this.config.user) parts.push(`UID=${this.config.user}`);
38
39
  if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
39
- // NOUVEAU: Potentiellement ajouter des options de timeout ici si le driver les supporte dans la CS
40
- // Exemple (non standard, dépend du driver): if (this.config.loginTimeout > 0) parts.push(`LoginTimeout=${this.config.loginTimeout}`);
41
40
  return parts.join(';');
42
41
  } else {
43
42
  let connStr = this.config.connectionString || "";
@@ -53,19 +52,18 @@ module.exports = function (RED) {
53
52
  const finalConnectionString = this._buildConnectionString();
54
53
  if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
55
54
 
56
- const poolParams = { ...this.config }; // Contient initialSize, maxSize, loginTimeout, connectionTimeout (idle) etc.
57
- poolParams.connectionString = finalConnectionString;
58
-
59
- // Supprimer les clés non reconnues par odbc.pool ou spécifiques à notre nœud
60
- ['retryFreshConnection', 'retryDelay', 'retryOnMsg', 'syntax', 'connectionMode',
61
- 'dbType', 'server', 'database', 'user', 'driver', 'queryTimeoutSeconds', 'name', 'id', 'type', '_users', 'z', 'x', 'y', 'wires']
62
- .forEach(k => delete poolParams[k]);
55
+ const poolParams = {
56
+ connectionString: finalConnectionString,
57
+ initialSize: parseInt(this.config.initialSize, 10) || undefined,
58
+ incrementSize: parseInt(this.config.incrementSize, 10) || undefined,
59
+ maxSize: parseInt(this.config.maxSize, 10) || undefined,
60
+ shrink: typeof this.config.shrink === 'boolean' ? this.config.shrink : true,
61
+ connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) || undefined,
62
+ loginTimeout: parseInt(this.config.loginTimeout, 10) || undefined
63
+ };
64
+
65
+ Object.keys(poolParams).forEach(key => poolParams[key] === undefined && delete poolParams[key]);
63
66
 
64
- // NOUVEAU: Debug des paramètres du pool
65
- // this.log(`Initializing pool with params: ${JSON.stringify(poolParams)}`);
66
-
67
- // Potentiel point de blocage si odbcModule.pool() ne gère pas bien les erreurs de driver/connexion
68
- // Il n'y a pas de timeout direct pour odbcModule.pool() lui-même
69
67
  this.pool = await odbcModule.pool(poolParams);
70
68
  this.connecting = false;
71
69
  this.status({ fill: "green", shape: "dot", text: "Pool ready" });
@@ -78,8 +76,6 @@ module.exports = function (RED) {
78
76
  }
79
77
  }
80
78
  try {
81
- // odbc.pool.connect() peut aussi théoriquement bloquer, mais devrait utiliser
82
- // les timeouts des connexions individuelles ou le connectionTimeout du pool (pour l'attente d'une connexion dispo)
83
79
  return await this.pool.connect();
84
80
  } catch (poolConnectError) {
85
81
  this.error(`Error connecting to pool: ${poolConnectError.message}`, poolConnectError);
@@ -89,16 +85,13 @@ module.exports = function (RED) {
89
85
  };
90
86
 
91
87
  this.getFreshConnectionConfig = function() {
92
- // Ces timeouts sont pour odbcModule.connect (connexion unique)
93
88
  return {
94
89
  connectionString: this._buildConnectionString(),
95
- connectionTimeout: 0, // Pour une connexion unique, on ne veut pas qu'elle se ferme automatiquement après un idle time.
96
- // Le `connectionTimeout` de node-odbc connect est "Number of seconds for the connection to be open before it is automatically closed."
97
- loginTimeout: parseInt(this.config.loginTimeout, 10) || 5, // Timeout pour l'établissement de la connexion. 5s par défaut.
90
+ connectionTimeout: 0,
91
+ loginTimeout: parseInt(this.config.loginTimeout, 10) || 5,
98
92
  };
99
93
  };
100
94
 
101
- // MODIFIÉ: Ajout de timeout pour pool.close()
102
95
  this.resetPool = async () => {
103
96
  if (this.pool) {
104
97
  this.log("Resetting connection pool.");
@@ -129,7 +122,6 @@ module.exports = function (RED) {
129
122
  }
130
123
  };
131
124
 
132
- // MODIFIÉ: Ajout de timeout pour pool.close()
133
125
  this.on("close", async (removed, done) => {
134
126
  this.log("Closing ODBC config node. Attempting to close pool.");
135
127
  if (this.pool) {
@@ -144,7 +136,7 @@ module.exports = function (RED) {
144
136
  } catch (error) {
145
137
  this.error(`Error or timeout closing connection pool on node close: ${error.message}`, error);
146
138
  } finally {
147
- this.pool = null; // S'assurer que le pool est marqué comme null
139
+ this.pool = null;
148
140
  }
149
141
  }
150
142
  done();
@@ -189,20 +181,20 @@ module.exports = function (RED) {
189
181
  let connection;
190
182
  try {
191
183
  const testConnectionString = buildTestConnectionString();
192
- console.log("[ODBC Test] Attempting to connect with string:", testConnectionString);
184
+ // console.log("[ODBC Test] Attempting to connect with string:", testConnectionString); // Conservé pour debug si besoin
193
185
 
194
186
  const connectionOptions = {
195
187
  connectionString: testConnectionString,
196
- loginTimeout: 10 // Déjà présent et correct
188
+ loginTimeout: 10
197
189
  };
198
190
  connection = await odbcModule.connect(connectionOptions);
199
191
  res.sendStatus(200);
200
192
  } catch (err) {
201
- console.error("[ODBC Test] Connection failed:", err);
193
+ // console.error("[ODBC Test] Connection failed:", err); // Conservé pour debug si besoin
202
194
  res.status(500).send(err.message || "Erreur inconnue durant le test.");
203
195
  } finally {
204
196
  if (connection) {
205
- await connection.close(); // Fermeture simple, pas besoin de timeout ici car c'est une op rapide.
197
+ await connection.close();
206
198
  }
207
199
  }
208
200
  });
@@ -215,21 +207,95 @@ module.exports = function (RED) {
215
207
  this.name = this.config.name;
216
208
  this.isAwaitingRetry = false;
217
209
  this.retryTimer = null;
210
+ this.cursorCloseOperationTimeout = 5000;
218
211
 
219
- // NOUVEAU: Timeout fixe pour les opérations de fermeture de curseur (en ms)
220
- this.cursorCloseOperationTimeout = 5000; // 5 secondes
212
+ this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
213
+ const queryContext = (() => {
214
+ let s = "";
215
+ if (query || params) {
216
+ s += " {";
217
+ if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
218
+ if (params) s += `, "params": '${JSON.stringify(params)}'`;
219
+ s += "}";
220
+ return s;
221
+ }
222
+ return "";
223
+ })();
224
+ let finalError;
225
+ if (typeof error === "object" && error !== null && error.message) { finalError = error; }
226
+ else if (typeof error === "string") { finalError = new Error(error); }
227
+ else { finalError = new Error(defaultMessage); }
228
+ finalError.message = `${finalError.message}${queryContext}`;
229
+ if (query) finalError.query = query;
230
+ if (params) finalError.params = params;
231
+ return finalError;
232
+ };
233
+
234
+ // MODIFIÉ : Nettoyage des objets de résultat
235
+ this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
236
+ const result = await dbConnection.query(queryString, queryParams);
237
+ if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
238
+
239
+ const newMsg = RED.util.cloneMessage(msg);
240
+ const outputProperty = this.config.outputObj || "payload"; // Utiliser la propriété configurée ou 'payload' par défaut
241
+ const otherParams = {};
242
+ let actualDataRows = [];
221
243
 
222
- this.enhanceError = (error, query, params, defaultMessage = "Query error") => { /* ... (inchangé) ... */ };
223
- this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => { /* ... (inchangé) ... */ };
244
+ if (result !== null && typeof result === "object") {
245
+ if (Array.isArray(result)) {
246
+ actualDataRows = result.map(row => {
247
+ if (typeof row === 'object' && row !== null) {
248
+ return { ...row }; // Copie superficielle pour "nettoyer" l'objet
249
+ }
250
+ return row;
251
+ });
252
+
253
+ for (const [key, value] of Object.entries(result)) {
254
+ if (isNaN(parseInt(key))) {
255
+ otherParams[key] = value;
256
+ }
257
+ }
258
+ } else {
259
+ for (const [key, value] of Object.entries(result)) {
260
+ otherParams[key] = value;
261
+ }
262
+ }
263
+ }
264
+
265
+ const columnMetadata = otherParams.columns;
266
+ if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
267
+ const sqlBitColumnNames = new Set();
268
+ columnMetadata.forEach((col) => {
269
+ if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
270
+ sqlBitColumnNames.add(col.name);
271
+ }
272
+ });
273
+ if (sqlBitColumnNames.size > 0) {
274
+ actualDataRows.forEach((row) => {
275
+ if (typeof row === "object" && row !== null) {
276
+ for (const columnName of sqlBitColumnNames) {
277
+ if (row.hasOwnProperty(columnName)) {
278
+ const value = row[columnName];
279
+ if (value === "1" || value === 1) { row[columnName] = true; }
280
+ else if (value === "0" || value === 0) { row[columnName] = false; }
281
+ }
282
+ }
283
+ }
284
+ });
285
+ }
286
+ }
287
+
288
+ objPath.set(newMsg, outputProperty, actualDataRows);
289
+ if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
290
+ return newMsg;
291
+ };
224
292
 
225
- // MODIFIÉ: Ajout de timeout pour cursor.close()
226
293
  this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
227
294
  const chunkSize = parseInt(this.config.streamChunkSize) || 1;
228
295
  const fetchSize = chunkSize > 100 ? 100 : chunkSize;
229
296
  let cursor;
230
297
 
231
298
  try {
232
- // dbConnection.query() utilisera le dbConnection.queryTimeout défini plus bas
233
299
  cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
234
300
  this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
235
301
 
@@ -237,16 +303,17 @@ module.exports = function (RED) {
237
303
  let chunk = [];
238
304
 
239
305
  while (true) {
240
- // cursor.fetch() pourrait aussi théoriquement bloquer, mais c'est plus rare si la requête initiale a fonctionné
241
306
  const rows = await cursor.fetch();
242
307
  if (!rows || rows.length === 0) { break; }
243
308
 
244
309
  for (const row of rows) {
245
310
  rowCount++;
246
- chunk.push(row);
311
+ // Nettoyer chaque ligne aussi pour le streaming
312
+ const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
313
+ chunk.push(cleanRow);
247
314
  if (chunk.length >= chunkSize) {
248
315
  const newMsg = RED.util.cloneMessage(msg);
249
- objPath.set(newMsg, this.config.outputObj, chunk);
316
+ objPath.set(newMsg, this.config.outputObj || "payload", chunk);
250
317
  newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
251
318
  send(newMsg);
252
319
  chunk = [];
@@ -256,13 +323,13 @@ module.exports = function (RED) {
256
323
 
257
324
  if (chunk.length > 0) {
258
325
  const newMsg = RED.util.cloneMessage(msg);
259
- objPath.set(newMsg, this.config.outputObj, chunk);
326
+ objPath.set(newMsg, this.config.outputObj || "payload", chunk);
260
327
  newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
261
328
  send(newMsg);
262
329
  }
263
330
 
264
331
  const finalMsg = RED.util.cloneMessage(msg);
265
- objPath.set(finalMsg, this.config.outputObj, []);
332
+ objPath.set(finalMsg, this.config.outputObj || "payload", []);
266
333
  finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
267
334
  send(finalMsg);
268
335
 
@@ -271,7 +338,6 @@ module.exports = function (RED) {
271
338
  } finally {
272
339
  if (cursor) {
273
340
  try {
274
- // NOUVEAU: Timeout pour la fermeture du curseur
275
341
  await Promise.race([
276
342
  cursor.close(),
277
343
  new Promise((_, reject) =>
@@ -285,7 +351,6 @@ module.exports = function (RED) {
285
351
  }
286
352
  };
287
353
 
288
- // MODIFIÉ: Ajout de la définition du queryTimeout sur la connexion
289
354
  this.on("input", async (msg, send, done) => {
290
355
  if (this.isAwaitingRetry) {
291
356
  if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
@@ -308,7 +373,8 @@ module.exports = function (RED) {
308
373
  }
309
374
 
310
375
  const executeWithConnection = async (connection) => {
311
- this.config.outputObj = this.config.outputObj || "payload";
376
+ const outputProperty = this.config.outputObj || "payload"; // S'assurer que outputProperty est défini
377
+
312
378
  const querySourceType = this.config.querySourceType || 'msg';
313
379
  const querySource = this.config.querySource || 'query';
314
380
  const paramsSourceType = this.config.paramsSourceType || 'msg';
@@ -321,16 +387,14 @@ module.exports = function (RED) {
321
387
  query = mustache.render(query, msg);
322
388
  }
323
389
 
324
- // NOUVEAU: Appliquer le queryTimeout à la connexion avant exécution
325
390
  if (this.poolNode.config.queryTimeoutSeconds > 0) {
326
391
  try {
327
392
  connection.queryTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
328
- // this.log(`Query timeout set to ${connection.queryTimeout}s for this execution.`);
329
393
  } catch (e) {
330
394
  this.warn(`Could not set queryTimeout on connection: ${e.message}`);
331
395
  }
332
396
  } else {
333
- connection.queryTimeout = 0; // Assurer le reset au défaut du driver (infini)
397
+ connection.queryTimeout = 0;
334
398
  }
335
399
 
336
400
  this.status({ fill: "blue", shape: "dot", text: "executing..." });
@@ -349,10 +413,23 @@ module.exports = function (RED) {
349
413
  try {
350
414
  this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
351
415
  connectionFromPool = await this.poolNode.connect();
416
+ //this.log("[ODBC Node] DEBUG: Avant executeWithConnection (pooled)"); // Logs de debug
352
417
  await executeWithConnection(connectionFromPool);
418
+ //this.log("[ODBC Node] DEBUG: Après executeWithConnection (pooled), avant fermeture et done()");
419
+
420
+ // Fermer la connexion avant d'appeler done() dans le chemin de succès principal
421
+ if (connectionFromPool) {
422
+ await connectionFromPool.close();
423
+ connectionFromPool = null;
424
+ }
353
425
  return done();
354
426
  } catch (poolError) {
427
+ if (connectionFromPool) {
428
+ try { await connectionFromPool.close(); } catch(e) { this.warn("Error closing pool connection in poolError catch: " + e.message); }
429
+ connectionFromPool = null;
430
+ }
355
431
  this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
432
+
356
433
  if (this.poolNode.config.retryFreshConnection) {
357
434
  this.warn("Attempting retry with a fresh connection.");
358
435
  this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
@@ -361,21 +438,28 @@ module.exports = function (RED) {
361
438
  const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
362
439
  freshConnection = await odbcModule.connect(freshConnectConfig);
363
440
  this.log("Fresh connection established for retry.");
441
+ //this.log("[ODBC Node] DEBUG: Avant executeWithConnection (fresh)");
364
442
  await executeWithConnection(freshConnection);
365
- this.log("Query successful with fresh connection. Resetting pool.");
443
+ //this.log("[ODBC Node] DEBUG: Après executeWithConnection (fresh), avant resetPool et done()");
366
444
  await this.poolNode.resetPool();
445
+
446
+ if (freshConnection) {
447
+ await freshConnection.close();
448
+ freshConnection = null;
449
+ }
367
450
  return done();
368
451
  } catch (freshError) {
369
452
  errorAfterInitialAttempts = this.enhanceError(freshError, null, null, "Retry with fresh connection also failed");
370
453
  } finally {
371
- if (freshConnection) await freshConnection.close();
454
+ if (freshConnection) {
455
+ try { await freshConnection.close(); } catch(e) { this.warn("Error closing fresh connection in finally: " + e.message); }
456
+ }
372
457
  }
373
458
  } else {
374
459
  errorAfterInitialAttempts = this.enhanceError(poolError);
375
460
  }
376
- } finally {
377
- if (connectionFromPool) await connectionFromPool.close();
378
- }
461
+ }
462
+ // Le 'finally' qui fermait connectionFromPool est retiré ici car géré dans chaque chemin
379
463
 
380
464
  if (errorAfterInitialAttempts) {
381
465
  const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
@@ -395,6 +479,9 @@ module.exports = function (RED) {
395
479
  if (done) return done(errorAfterInitialAttempts);
396
480
  }
397
481
  } else {
482
+ // Si on arrive ici SANS errorAfterInitialAttempts, c'est que done() aurait dû être appelé.
483
+ // C'est une situation anormale, mais assurons-nous que done() soit appelé.
484
+ // this.log("[ODBC Node] DEBUG: Atteint la fin de on('input') sans erreur signalée et done() non appelé plus tôt. Appel de done().");
398
485
  if (done) return done();
399
486
  }
400
487
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "A powerful Node-RED node to connect to any ODBC data source, with connection pooling, advanced retry logic, and result streaming.",
5
5
  "keywords": [
6
6
  "node-red",