@bkmj/node-red-contrib-odbcmj 2.3.1 → 2.4.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 +191 -185
  2. package/package.json +1 -1
package/odbc.js CHANGED
@@ -9,14 +9,12 @@ module.exports = function (RED) {
9
9
  this.config = config;
10
10
  this.pool = null;
11
11
  this.connecting = false;
12
-
13
12
  this.credentials = RED.nodes.getCredentials(this.id);
14
13
 
15
14
  this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10);
16
15
  if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
17
16
  this.config.queryTimeoutSeconds = 0;
18
17
  }
19
-
20
18
  this.closeOperationTimeout = 10000; // 10 secondes
21
19
 
22
20
  this._buildConnectionString = function() {
@@ -39,8 +37,7 @@ module.exports = function (RED) {
39
37
  if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
40
38
  return parts.join(';');
41
39
  } else {
42
- let connStr = this.config.connectionString || "";
43
- return connStr;
40
+ return this.config.connectionString || "";
44
41
  }
45
42
  };
46
43
 
@@ -51,7 +48,6 @@ module.exports = function (RED) {
51
48
  try {
52
49
  const finalConnectionString = this._buildConnectionString();
53
50
  if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
54
-
55
51
  const poolParams = {
56
52
  connectionString: finalConnectionString,
57
53
  initialSize: parseInt(this.config.initialSize, 10) || undefined,
@@ -61,9 +57,7 @@ module.exports = function (RED) {
61
57
  connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) || undefined,
62
58
  loginTimeout: parseInt(this.config.loginTimeout, 10) || undefined
63
59
  };
64
-
65
60
  Object.keys(poolParams).forEach(key => poolParams[key] === undefined && delete poolParams[key]);
66
-
67
61
  this.pool = await odbcModule.pool(poolParams);
68
62
  this.connecting = false;
69
63
  this.status({ fill: "green", shape: "dot", text: "Pool ready" });
@@ -100,9 +94,7 @@ module.exports = function (RED) {
100
94
  try {
101
95
  await Promise.race([
102
96
  this.pool.close(),
103
- new Promise((_, reject) =>
104
- setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout)
105
- )
97
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout))
106
98
  ]);
107
99
  this.log("Connection pool closed successfully for reset.");
108
100
  closedSuccessfully = true;
@@ -111,11 +103,7 @@ module.exports = function (RED) {
111
103
  } finally {
112
104
  this.pool = null;
113
105
  this.connecting = false;
114
- if (closedSuccessfully) {
115
- this.status({ fill: "grey", shape: "ring", text: "Pool reset" });
116
- } else {
117
- this.status({ fill: "red", shape: "ring", text: "Pool reset failed" });
118
- }
106
+ this.status({ fill: closedSuccessfully ? "grey" : "red", shape: "ring", text: closedSuccessfully ? "Pool reset" : "Pool reset failed" });
119
107
  }
120
108
  } else {
121
109
  this.log("Pool reset requested, but no active pool to reset.");
@@ -128,9 +116,7 @@ module.exports = function (RED) {
128
116
  try {
129
117
  await Promise.race([
130
118
  this.pool.close(),
131
- new Promise((_, reject) =>
132
- setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout)
133
- )
119
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout))
134
120
  ]);
135
121
  this.log("Connection pool closed successfully on node close.");
136
122
  } catch (error) {
@@ -142,21 +128,13 @@ module.exports = function (RED) {
142
128
  done();
143
129
  });
144
130
  }
145
-
146
- RED.nodes.registerType("odbc config", poolConfig, {
147
- credentials: {
148
- password: { type: "password" }
149
- }
150
- });
131
+ RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
151
132
 
152
133
  RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
153
134
  const tempConfig = req.body;
154
-
155
135
  const buildTestConnectionString = () => {
156
136
  if (tempConfig.connectionMode === 'structured') {
157
- if (!tempConfig.dbType || !tempConfig.server) {
158
- throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
159
- }
137
+ if (!tempConfig.dbType || !tempConfig.server) { throw new Error("En mode structuré, le type de base de données et le serveur sont requis."); }
160
138
  let driver;
161
139
  let parts = [];
162
140
  switch (tempConfig.dbType) {
@@ -177,25 +155,16 @@ module.exports = function (RED) {
177
155
  return connStr;
178
156
  }
179
157
  };
180
-
181
158
  let connection;
182
159
  try {
183
160
  const testConnectionString = buildTestConnectionString();
184
- // console.log("[ODBC Test] Attempting to connect with string:", testConnectionString); // Conservé pour debug si besoin
185
-
186
- const connectionOptions = {
187
- connectionString: testConnectionString,
188
- loginTimeout: 10
189
- };
161
+ const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
190
162
  connection = await odbcModule.connect(connectionOptions);
191
163
  res.sendStatus(200);
192
164
  } catch (err) {
193
- // console.error("[ODBC Test] Connection failed:", err); // Conservé pour debug si besoin
194
165
  res.status(500).send(err.message || "Erreur inconnue durant le test.");
195
166
  } finally {
196
- if (connection) {
197
- await connection.close();
198
- }
167
+ if (connection) await connection.close();
199
168
  }
200
169
  });
201
170
 
@@ -208,14 +177,18 @@ module.exports = function (RED) {
208
177
  this.isAwaitingRetry = false;
209
178
  this.retryTimer = null;
210
179
  this.cursorCloseOperationTimeout = 5000;
180
+ this.currentQueryForErrorContext = null;
181
+ this.currentParamsForErrorContext = null;
211
182
 
212
183
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
184
+ const q = query || this.currentQueryForErrorContext;
185
+ const p = params || this.currentParamsForErrorContext;
213
186
  const queryContext = (() => {
214
187
  let s = "";
215
- if (query || params) {
188
+ if (q || p) {
216
189
  s += " {";
217
- if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
218
- if (params) s += `, "params": '${JSON.stringify(params)}'`;
190
+ if (q) s += `"query": '${String(q).substring(0, 100)}${String(q).length > 100 ? "..." : ""}'`;
191
+ if (p) s += `, "params": '${JSON.stringify(p)}'`;
219
192
  s += "}";
220
193
  return s;
221
194
  }
@@ -226,67 +199,48 @@ module.exports = function (RED) {
226
199
  else if (typeof error === "string") { finalError = new Error(error); }
227
200
  else { finalError = new Error(defaultMessage); }
228
201
  finalError.message = `${finalError.message}${queryContext}`;
229
- if (query) finalError.query = query;
230
- if (params) finalError.params = params;
202
+ if (q) finalError.query = String(q).substring(0,200);
203
+ if (p) finalError.params = p;
231
204
  return finalError;
232
205
  };
233
206
 
234
- // MODIFIÉ : Nettoyage des objets de résultat
235
207
  this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
236
208
  const result = await dbConnection.query(queryString, queryParams);
237
209
  if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
238
-
239
210
  const newMsg = RED.util.cloneMessage(msg);
240
- const outputProperty = this.config.outputObj || "payload"; // Utiliser la propriété configurée ou 'payload' par défaut
211
+ const outputProperty = this.config.outputObj || "payload";
241
212
  const otherParams = {};
242
213
  let actualDataRows = [];
243
-
244
214
  if (result !== null && typeof result === "object") {
245
215
  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
-
216
+ actualDataRows = result.map(row => (typeof row === 'object' && row !== null) ? { ...row } : row);
253
217
  for (const [key, value] of Object.entries(result)) {
254
- if (isNaN(parseInt(key))) {
255
- otherParams[key] = value;
256
- }
218
+ if (isNaN(parseInt(key))) { otherParams[key] = value; }
257
219
  }
258
220
  } else {
259
- for (const [key, value] of Object.entries(result)) {
260
- otherParams[key] = value;
261
- }
221
+ for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
262
222
  }
263
223
  }
264
-
265
224
  const columnMetadata = otherParams.columns;
266
225
  if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
267
226
  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
- });
227
+ columnMetadata.forEach(col => { if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") sqlBitColumnNames.add(col.name); });
273
228
  if (sqlBitColumnNames.size > 0) {
274
- actualDataRows.forEach((row) => {
229
+ actualDataRows.forEach(row => {
275
230
  if (typeof row === "object" && row !== null) {
276
231
  for (const columnName of sqlBitColumnNames) {
277
232
  if (row.hasOwnProperty(columnName)) {
278
233
  const value = row[columnName];
279
- if (value === "1" || value === 1) { row[columnName] = true; }
280
- else if (value === "0" || value === 0) { row[columnName] = false; }
234
+ if (value === "1" || value === 1) row[columnName] = true;
235
+ else if (value === "0" || value === 0) row[columnName] = false;
281
236
  }
282
237
  }
283
238
  }
284
239
  });
285
240
  }
286
241
  }
287
-
288
242
  objPath.set(newMsg, outputProperty, actualDataRows);
289
- if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
243
+ if (Object.keys(otherParams).length) newMsg.odbc = otherParams;
290
244
  return newMsg;
291
245
  };
292
246
 
@@ -294,21 +248,16 @@ module.exports = function (RED) {
294
248
  const chunkSize = parseInt(this.config.streamChunkSize) || 1;
295
249
  const fetchSize = chunkSize > 100 ? 100 : chunkSize;
296
250
  let cursor;
297
-
298
251
  try {
299
252
  cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
300
253
  this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
301
-
302
254
  let rowCount = 0;
303
255
  let chunk = [];
304
-
305
256
  while (true) {
306
257
  const rows = await cursor.fetch();
307
- if (!rows || rows.length === 0) { break; }
308
-
258
+ if (!rows || rows.length === 0) break;
309
259
  for (const row of rows) {
310
260
  rowCount++;
311
- // Nettoyer chaque ligne aussi pour le streaming
312
261
  const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
313
262
  chunk.push(cleanRow);
314
263
  if (chunk.length >= chunkSize) {
@@ -320,48 +269,103 @@ module.exports = function (RED) {
320
269
  }
321
270
  }
322
271
  }
323
-
324
272
  if (chunk.length > 0) {
325
273
  const newMsg = RED.util.cloneMessage(msg);
326
274
  objPath.set(newMsg, this.config.outputObj || "payload", chunk);
327
275
  newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
328
276
  send(newMsg);
329
277
  }
330
-
331
278
  const finalMsg = RED.util.cloneMessage(msg);
332
279
  objPath.set(finalMsg, this.config.outputObj || "payload", []);
333
280
  finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
334
281
  send(finalMsg);
335
-
336
282
  this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
337
-
338
283
  } finally {
339
284
  if (cursor) {
340
285
  try {
341
286
  await Promise.race([
342
287
  cursor.close(),
343
- new Promise((_, reject) =>
344
- setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout)
345
- )
288
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout))
346
289
  ]);
347
- } catch (cursorCloseError) {
348
- this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`);
349
- }
290
+ } catch (cursorCloseError) { this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`); }
291
+ }
292
+ }
293
+ };
294
+
295
+ this.testBasicConnectivity = async function(connection) {
296
+ if (!connection || typeof connection.query !== 'function') {
297
+ this.warn("Test de connectivité basique : connexion invalide fournie.");
298
+ return false;
299
+ }
300
+ let originalTimeout;
301
+ try {
302
+ originalTimeout = connection.queryTimeout;
303
+ connection.queryTimeout = 5;
304
+ await connection.query("SELECT 1");
305
+ this.log("Test de connectivité basique (SELECT 1) : Réussi.");
306
+ return true;
307
+ } catch (testError) {
308
+ this.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
309
+ return false;
310
+ } finally {
311
+ if (typeof originalTimeout !== 'undefined' && connection && typeof connection.query === 'function') {
312
+ try { connection.queryTimeout = originalTimeout; }
313
+ catch(e) { this.warn("Impossible de restaurer le queryTimeout original après le test de connectivité.")}
350
314
  }
351
315
  }
352
316
  };
353
317
 
318
+ this.getRenderedQueryAndParams = async function(msg) {
319
+ const querySourceType = this.config.querySourceType || 'msg';
320
+ const querySource = this.config.querySource || 'query';
321
+ const paramsSourceType = this.config.paramsSourceType || 'msg';
322
+ const paramsSource = this.config.paramsSource || 'parameters';
323
+
324
+ this.currentParamsForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
325
+ this.currentQueryForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
326
+
327
+ if (!this.currentQueryForErrorContext) {
328
+ throw new Error("No query to execute. Please provide a query in the node's configuration or via msg." + (querySourceType === 'msg' ? querySource : 'querySource (non-msg)'));
329
+ }
330
+
331
+ let finalQuery = this.currentQueryForErrorContext;
332
+ const isPreparedStatement = this.currentParamsForErrorContext || (finalQuery && finalQuery.includes("?"));
333
+ if (!isPreparedStatement && finalQuery) {
334
+ finalQuery = mustache.render(finalQuery, msg);
335
+ }
336
+ return { query: finalQuery, params: this.currentParamsForErrorContext };
337
+ };
338
+
339
+ this.executeUserQuery = async function(connection, query, params, msg, send) {
340
+ const configuredTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
341
+ if (configuredTimeout > 0) {
342
+ try { connection.queryTimeout = configuredTimeout; }
343
+ catch (e) { this.warn(`Could not set queryTimeout on connection: ${e.message}`); }
344
+ } else {
345
+ connection.queryTimeout = 0;
346
+ }
347
+
348
+ this.status({ fill: "blue", shape: "dot", text: "executing..." });
349
+ if (this.config.streaming) {
350
+ await this.executeStreamQuery(connection, query, params, msg, send);
351
+ } else {
352
+ const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
353
+ this.status({ fill: "green", shape: "dot", text: "success" });
354
+ send(newMsg);
355
+ }
356
+ };
357
+
354
358
  this.on("input", async (msg, send, done) => {
359
+ this.currentQueryForErrorContext = null;
360
+ this.currentParamsForErrorContext = null;
361
+
355
362
  if (this.isAwaitingRetry) {
356
363
  if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
357
364
  this.log("New message received, overriding retry timer and attempting query now.");
358
- clearTimeout(this.retryTimer);
359
- this.retryTimer = null;
360
- this.isAwaitingRetry = false;
365
+ clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
361
366
  } else {
362
- this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
363
- if (done) done();
364
- return;
367
+ this.warn("Node is in a retry-wait state. New message ignored.");
368
+ if (done) done(); return;
365
369
  }
366
370
  }
367
371
  this.isAwaitingRetry = false;
@@ -369,128 +373,130 @@ module.exports = function (RED) {
369
373
 
370
374
  if (!this.poolNode) {
371
375
  this.status({ fill: "red", shape: "ring", text: "No config node" });
372
- return done(new Error("ODBC Config node not properly configured."));
376
+ return done(this.enhanceError(new Error("ODBC Config node not properly configured.")));
373
377
  }
374
-
375
- const executeWithConnection = async (connection) => {
376
- const outputProperty = this.config.outputObj || "payload"; // S'assurer que outputProperty est défini
378
+
379
+ let queryToExecute;
380
+ let paramsToExecute;
381
+ try {
382
+ const queryData = await this.getRenderedQueryAndParams(msg);
383
+ queryToExecute = queryData.query;
384
+ paramsToExecute = queryData.params;
385
+ } catch (inputValidationError) {
386
+ this.status({ fill: "red", shape: "ring", text: "Input Error" });
387
+ return done(this.enhanceError(inputValidationError));
388
+ }
389
+
390
+ let activeConnection = null;
391
+ let errorForUser = null;
392
+ let shouldProceedToTimedRetry = false;
393
+
394
+ try {
395
+ this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
396
+ activeConnection = await this.poolNode.connect();
397
+ await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
398
+
399
+ done();
377
400
 
378
- const querySourceType = this.config.querySourceType || 'msg';
379
- const querySource = this.config.querySource || 'query';
380
- const paramsSourceType = this.config.paramsSourceType || 'msg';
381
- const paramsSource = this.config.paramsSource || 'parameters';
382
- const params = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
383
- let query = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
384
- if (!query) throw new Error("No query to execute");
385
- const isPreparedStatement = params || (query && query.includes("?"));
386
- if (!isPreparedStatement && query) {
387
- query = mustache.render(query, msg);
401
+ if (activeConnection) {
402
+ try { await activeConnection.close(); } catch(e) { this.warn("Error closing pool connection after success: " + e.message); }
403
+ activeConnection = null;
388
404
  }
405
+ return;
389
406
 
390
- if (this.poolNode.config.queryTimeoutSeconds > 0) {
391
- try {
392
- connection.queryTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
393
- } catch (e) {
394
- this.warn(`Could not set queryTimeout on connection: ${e.message}`);
407
+ } catch (initialDbError) {
408
+ this.warn(`Initial DB attempt failed: ${initialDbError.message}`);
409
+ // Garder la requête originale pour le contexte d'erreur, même si une erreur de connexion se produit
410
+ // this.currentQueryForErrorContext et this.currentParamsForErrorContext sont déjà settés par getRenderedQueryAndParams
411
+
412
+ if (activeConnection) {
413
+ const connStillGood = await this.testBasicConnectivity(activeConnection);
414
+ try { await activeConnection.close(); activeConnection = null; }
415
+ catch(e){ this.warn("Error closing pool conn after initial error: "+e.message); activeConnection = null; }
416
+
417
+ if (connStillGood) {
418
+ this.status({ fill: "red", shape: "ring", text: "SQL error" });
419
+ return done(this.enhanceError(initialDbError));
395
420
  }
396
- } else {
397
- connection.queryTimeout = 0;
398
- }
399
-
400
- this.status({ fill: "blue", shape: "dot", text: "executing..." });
401
- if (this.config.streaming) {
402
- await this.executeStreamQuery(connection, query, params, msg, send);
403
- } else {
404
- const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
405
- this.status({ fill: "green", shape: "dot", text: "success" });
406
- send(newMsg);
407
421
  }
408
- };
409
-
410
- let connectionFromPool;
411
- let errorAfterInitialAttempts = null;
412
-
413
- try {
414
- this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
415
- connectionFromPool = await this.poolNode.connect();
416
- //this.log("[ODBC Node] DEBUG: Avant executeWithConnection (pooled)"); // Logs de debug
417
- await executeWithConnection(connectionFromPool);
418
- //this.log("[ODBC Node] DEBUG: Après executeWithConnection (pooled), avant fermeture et done()");
419
422
 
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
- }
425
- return done();
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
- }
431
- this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
432
-
433
423
  if (this.poolNode.config.retryFreshConnection) {
434
424
  this.warn("Attempting retry with a fresh connection.");
435
425
  this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
436
- let freshConnection;
437
426
  try {
438
427
  const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
439
- freshConnection = await odbcModule.connect(freshConnectConfig);
440
- this.log("Fresh connection established for retry.");
441
- //this.log("[ODBC Node] DEBUG: Avant executeWithConnection (fresh)");
442
- await executeWithConnection(freshConnection);
443
- //this.log("[ODBC Node] DEBUG: Après executeWithConnection (fresh), avant resetPool et done()");
444
- await this.poolNode.resetPool();
428
+ activeConnection = await odbcModule.connect(freshConnectConfig);
429
+ this.log("Fresh connection established.");
430
+
431
+ const freshConnGood = await this.testBasicConnectivity(activeConnection);
432
+ if (!freshConnGood) {
433
+ errorForUser = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), null, null, "Fresh Connection Test Failed");
434
+ shouldProceedToTimedRetry = true;
435
+ throw errorForUser;
436
+ }
445
437
 
446
- if (freshConnection) {
447
- await freshConnection.close();
448
- freshConnection = null;
438
+ await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
439
+
440
+ this.log("Query successful with fresh connection. Resetting pool.");
441
+ done();
442
+
443
+ await this.poolNode.resetPool();
444
+ if (activeConnection) {
445
+ try { await activeConnection.close(); } catch(e) { this.warn("Error closing fresh connection after success: " + e.message); }
446
+ activeConnection = null;
449
447
  }
450
- return done();
451
- } catch (freshError) {
452
- errorAfterInitialAttempts = this.enhanceError(freshError, null, null, "Retry with fresh connection also failed");
453
- } finally {
454
- if (freshConnection) {
455
- try { await freshConnection.close(); } catch(e) { this.warn("Error closing fresh connection in finally: " + e.message); }
448
+ return;
449
+
450
+ } catch (freshErrorOrConnectivityFail) {
451
+ if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
452
+
453
+ if (shouldProceedToTimedRetry) {
454
+ // errorForUser a été setté par l'échec du SELECT 1 sur la connexion fraîche
455
+ } else {
456
+ this.status({ fill: "red", shape: "ring", text: "SQL error (on retry)" });
457
+ return done(this.enhanceError(freshErrorOrConnectivityFail));
456
458
  }
457
459
  }
458
- } else {
459
- errorAfterInitialAttempts = this.enhanceError(poolError);
460
+ } else {
461
+ errorForUser = this.enhanceError(initialDbError, null, null, "Connection Error (no fresh retry)");
462
+ shouldProceedToTimedRetry = true;
460
463
  }
461
- }
462
- // Le 'finally' qui fermait connectionFromPool est retiré ici car géré dans chaque chemin
463
-
464
- if (errorAfterInitialAttempts) {
464
+ }
465
+
466
+ if (activeConnection) { // Sécurité supplémentaire pour fermer une connexion si elle est restée active
467
+ try { await activeConnection.close(); } catch(e) { this.warn("Final cleanup: Error closing activeConnection: " + e.message); }
468
+ activeConnection = null;
469
+ }
470
+
471
+ if (shouldProceedToTimedRetry && errorForUser) {
465
472
  const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
466
473
  if (retryDelaySeconds > 0) {
467
- this.warn(`Query failed. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorAfterInitialAttempts.message}`);
474
+ this.warn(`Connection issue. Scheduling retry in ${retryDelaySeconds}s. Error: ${errorForUser.message}`);
468
475
  this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
469
476
  this.isAwaitingRetry = true;
470
477
  this.retryTimer = setTimeout(() => {
471
- this.isAwaitingRetry = false;
472
- this.retryTimer = null;
473
- this.log(`Retry timer expired for message. Re-emitting for node ${this.id || this.name}.`);
478
+ this.isAwaitingRetry = false; this.retryTimer = null;
479
+ this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
474
480
  this.receive(msg);
475
481
  }, retryDelaySeconds * 1000);
476
- if (done) return done();
477
- } else {
478
- this.status({ fill: "red", shape: "ring", text: "query error" });
479
- if (done) return done(errorAfterInitialAttempts);
482
+ return done();
483
+ } else {
484
+ this.status({ fill: "red", shape: "ring", text: "Connection Error" });
485
+ return done(errorForUser);
480
486
  }
487
+ } else if (errorForUser) {
488
+ this.status({ fill: "red", shape: "ring", text: "Error (No Timed Retry)" });
489
+ return done(errorForUser); // Cas où c'est une erreur SQL identifiée, pas de retry temporisé.
481
490
  } else {
482
- // Si on arrive ici SANS errorAfterInitialAttempts, c'est que done() aurait ê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().");
485
- if (done) return done();
491
+ // Ce chemin ne devrait pas être atteint si done() a été appelé dans un chemin de succès.
492
+ this.log("[ODBC Node] DEBUG: Reached unexpected end of on('input') path. Calling done().");
493
+ return done();
486
494
  }
487
495
  });
488
496
 
489
497
  this.on("close", (done) => {
490
498
  if (this.retryTimer) {
491
- clearTimeout(this.retryTimer);
492
- this.retryTimer = null;
493
- this.isAwaitingRetry = false;
499
+ clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
494
500
  this.log("Cleared pending retry timer on node close/redeploy.");
495
501
  }
496
502
  this.status({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.3.1",
3
+ "version": "2.4.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",