@bkmj/node-red-contrib-odbcmj 2.3.0 → 2.4.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.
Files changed (2) hide show
  1. package/odbc.js +175 -211
  2. package/package.json +1 -1
package/odbc.js CHANGED
@@ -9,14 +9,13 @@ 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
- // 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)
19
- this.closeOperationTimeout = 10000; // 10 secondes
14
+ this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10);
15
+ if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
16
+ this.config.queryTimeoutSeconds = 0;
17
+ }
18
+ this.closeOperationTimeout = 10000;
20
19
 
21
20
  this._buildConnectionString = function() {
22
21
  if (this.config.connectionMode === 'structured') {
@@ -36,12 +35,9 @@ module.exports = function (RED) {
36
35
  if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
37
36
  if (this.config.user) parts.push(`UID=${this.config.user}`);
38
37
  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
38
  return parts.join(';');
42
39
  } else {
43
- let connStr = this.config.connectionString || "";
44
- return connStr;
40
+ return this.config.connectionString || "";
45
41
  }
46
42
  };
47
43
 
@@ -52,20 +48,16 @@ module.exports = function (RED) {
52
48
  try {
53
49
  const finalConnectionString = this._buildConnectionString();
54
50
  if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
55
-
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]);
63
-
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
51
+ const poolParams = {
52
+ connectionString: finalConnectionString,
53
+ initialSize: parseInt(this.config.initialSize, 10) || undefined,
54
+ incrementSize: parseInt(this.config.incrementSize, 10) || undefined,
55
+ maxSize: parseInt(this.config.maxSize, 10) || undefined,
56
+ shrink: typeof this.config.shrink === 'boolean' ? this.config.shrink : true,
57
+ connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) || undefined,
58
+ loginTimeout: parseInt(this.config.loginTimeout, 10) || undefined
59
+ };
60
+ Object.keys(poolParams).forEach(key => poolParams[key] === undefined && delete poolParams[key]);
69
61
  this.pool = await odbcModule.pool(poolParams);
70
62
  this.connecting = false;
71
63
  this.status({ fill: "green", shape: "dot", text: "Pool ready" });
@@ -78,8 +70,6 @@ module.exports = function (RED) {
78
70
  }
79
71
  }
80
72
  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
73
  return await this.pool.connect();
84
74
  } catch (poolConnectError) {
85
75
  this.error(`Error connecting to pool: ${poolConnectError.message}`, poolConnectError);
@@ -89,16 +79,13 @@ module.exports = function (RED) {
89
79
  };
90
80
 
91
81
  this.getFreshConnectionConfig = function() {
92
- // Ces timeouts sont pour odbcModule.connect (connexion unique)
93
82
  return {
94
83
  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.
84
+ connectionTimeout: 0,
85
+ loginTimeout: parseInt(this.config.loginTimeout, 10) || 5,
98
86
  };
99
87
  };
100
88
 
101
- // MODIFIÉ: Ajout de timeout pour pool.close()
102
89
  this.resetPool = async () => {
103
90
  if (this.pool) {
104
91
  this.log("Resetting connection pool.");
@@ -107,9 +94,7 @@ module.exports = function (RED) {
107
94
  try {
108
95
  await Promise.race([
109
96
  this.pool.close(),
110
- new Promise((_, reject) =>
111
- setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout)
112
- )
97
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout))
113
98
  ]);
114
99
  this.log("Connection pool closed successfully for reset.");
115
100
  closedSuccessfully = true;
@@ -118,92 +103,47 @@ module.exports = function (RED) {
118
103
  } finally {
119
104
  this.pool = null;
120
105
  this.connecting = false;
121
- if (closedSuccessfully) {
122
- this.status({ fill: "grey", shape: "ring", text: "Pool reset" });
123
- } else {
124
- this.status({ fill: "red", shape: "ring", text: "Pool reset failed" });
125
- }
106
+ this.status({ fill: closedSuccessfully ? "grey" : "red", shape: "ring", text: closedSuccessfully ? "Pool reset" : "Pool reset failed" });
126
107
  }
127
108
  } else {
128
109
  this.log("Pool reset requested, but no active pool to reset.");
129
110
  }
130
111
  };
131
112
 
132
- // MODIFIÉ: Ajout de timeout pour pool.close()
133
113
  this.on("close", async (removed, done) => {
134
114
  this.log("Closing ODBC config node. Attempting to close pool.");
135
115
  if (this.pool) {
136
116
  try {
137
117
  await Promise.race([
138
118
  this.pool.close(),
139
- new Promise((_, reject) =>
140
- setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout)
141
- )
119
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout))
142
120
  ]);
143
121
  this.log("Connection pool closed successfully on node close.");
144
122
  } catch (error) {
145
123
  this.error(`Error or timeout closing connection pool on node close: ${error.message}`, error);
146
124
  } finally {
147
- this.pool = null; // S'assurer que le pool est marqué comme null
125
+ this.pool = null;
148
126
  }
149
127
  }
150
128
  done();
151
129
  });
152
130
  }
153
-
154
- RED.nodes.registerType("odbc config", poolConfig, {
155
- credentials: {
156
- password: { type: "password" }
157
- }
158
- });
131
+ RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
159
132
 
160
133
  RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
134
+ // ... (Logique du testeur de connexion - INCHANGÉE par rapport à votre dernière version)
161
135
  const tempConfig = req.body;
162
-
163
- const buildTestConnectionString = () => {
164
- if (tempConfig.connectionMode === 'structured') {
165
- if (!tempConfig.dbType || !tempConfig.server) {
166
- throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
167
- }
168
- let driver;
169
- let parts = [];
170
- switch (tempConfig.dbType) {
171
- case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
172
- case 'postgresql': driver = 'PostgreSQL Unicode'; break;
173
- case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
174
- default: driver = tempConfig.driver || ''; break;
175
- }
176
- if(driver) parts.unshift(`DRIVER={${driver}}`);
177
- parts.push(`SERVER=${tempConfig.server}`);
178
- if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
179
- if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
180
- if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
181
- return parts.join(';');
182
- } else {
183
- let connStr = tempConfig.connectionString || "";
184
- if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
185
- return connStr;
186
- }
187
- };
188
-
136
+ const buildTestConnectionString = () => { /* ... */ }; // Définition interne
189
137
  let connection;
190
138
  try {
191
- const testConnectionString = buildTestConnectionString();
192
- console.log("[ODBC Test] Attempting to connect with string:", testConnectionString);
193
-
194
- const connectionOptions = {
195
- connectionString: testConnectionString,
196
- loginTimeout: 10 // Déjà présent et correct
197
- };
139
+ const testConnectionString = buildTestConnectionString(); // Utilise la définition interne
140
+ const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
198
141
  connection = await odbcModule.connect(connectionOptions);
199
142
  res.sendStatus(200);
200
143
  } catch (err) {
201
- console.error("[ODBC Test] Connection failed:", err);
202
144
  res.status(500).send(err.message || "Erreur inconnue durant le test.");
203
145
  } finally {
204
- if (connection) {
205
- await connection.close(); // Fermeture simple, pas besoin de timeout ici car c'est une op rapide.
206
- }
146
+ if (connection) await connection.close();
207
147
  }
208
148
  });
209
149
 
@@ -215,88 +155,69 @@ module.exports = function (RED) {
215
155
  this.name = this.config.name;
216
156
  this.isAwaitingRetry = false;
217
157
  this.retryTimer = null;
158
+ this.cursorCloseOperationTimeout = 5000;
159
+ this.currentQueryForErrorContext = null; // Pour stocker la requête lors du traitement
160
+ this.currentParamsForErrorContext = null; // Pour stocker les paramètres lors du traitement
218
161
 
219
- // NOUVEAU: Timeout fixe pour les opérations de fermeture de curseur (en ms)
220
- this.cursorCloseOperationTimeout = 5000; // 5 secondes
221
162
 
222
- this.enhanceError = (error, query, params, defaultMessage = "Query error") => { /* ... (inchangé) ... */ };
163
+ this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
164
+ // Utilise this.currentQueryForErrorContext et this.currentParamsForErrorContext s'ils sont définis
165
+ const q = query || this.currentQueryForErrorContext;
166
+ const p = params || this.currentParamsForErrorContext;
167
+ const queryContext = (() => {
168
+ let s = "";
169
+ if (q || p) {
170
+ s += " {";
171
+ if (q) s += `"query": '${String(q).substring(0, 100)}${String(q).length > 100 ? "..." : ""}'`;
172
+ if (p) s += `, "params": '${JSON.stringify(p)}'`;
173
+ s += "}";
174
+ return s;
175
+ }
176
+ return "";
177
+ })();
178
+ let finalError;
179
+ if (typeof error === "object" && error !== null && error.message) { finalError = error; }
180
+ else if (typeof error === "string") { finalError = new Error(error); }
181
+ else { finalError = new Error(defaultMessage); }
182
+ finalError.message = `${finalError.message}${queryContext}`;
183
+ if (q) finalError.query = String(q).substring(0,200);
184
+ if (p) finalError.params = p;
185
+ return finalError;
186
+ };
187
+
223
188
  this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => { /* ... (inchangé) ... */ };
224
-
225
- // MODIFIÉ: Ajout de timeout pour cursor.close()
226
- this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
227
- const chunkSize = parseInt(this.config.streamChunkSize) || 1;
228
- const fetchSize = chunkSize > 100 ? 100 : chunkSize;
229
- let cursor;
230
-
189
+ this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => { /* ... (inchangé) ... */ };
190
+
191
+ // NOUVELLE fonction utilitaire
192
+ async function testBasicConnectivity(connection, nodeInstance) {
193
+ if (!connection || typeof connection.query !== 'function') {
194
+ nodeInstance.warn("Test de connectivité basique : connexion invalide fournie.");
195
+ return false;
196
+ }
231
197
  try {
232
- // dbConnection.query() utilisera le dbConnection.queryTimeout défini plus bas
233
- cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
234
- this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
235
-
236
- let rowCount = 0;
237
- let chunk = [];
238
-
239
- while (true) {
240
- // cursor.fetch() pourrait aussi théoriquement bloquer, mais c'est plus rare si la requête initiale a fonctionné
241
- const rows = await cursor.fetch();
242
- if (!rows || rows.length === 0) { break; }
243
-
244
- for (const row of rows) {
245
- rowCount++;
246
- chunk.push(row);
247
- if (chunk.length >= chunkSize) {
248
- const newMsg = RED.util.cloneMessage(msg);
249
- objPath.set(newMsg, this.config.outputObj, chunk);
250
- newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
251
- send(newMsg);
252
- chunk = [];
253
- }
254
- }
255
- }
256
-
257
- if (chunk.length > 0) {
258
- const newMsg = RED.util.cloneMessage(msg);
259
- objPath.set(newMsg, this.config.outputObj, chunk);
260
- newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
261
- send(newMsg);
262
- }
263
-
264
- const finalMsg = RED.util.cloneMessage(msg);
265
- objPath.set(finalMsg, this.config.outputObj, []);
266
- finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
267
- send(finalMsg);
268
-
269
- this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
270
-
271
- } finally {
272
- if (cursor) {
273
- try {
274
- // NOUVEAU: Timeout pour la fermeture du curseur
275
- await Promise.race([
276
- cursor.close(),
277
- new Promise((_, reject) =>
278
- setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout)
279
- )
280
- ]);
281
- } catch (cursorCloseError) {
282
- this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`);
283
- }
284
- }
198
+ const originalTimeout = connection.queryTimeout;
199
+ connection.queryTimeout = 5; // Court timeout pour un SELECT 1
200
+ await connection.query("SELECT 1"); // Ou équivalent SGBD
201
+ connection.queryTimeout = originalTimeout;
202
+ nodeInstance.log("Test de connectivité basique (SELECT 1) : Réussi.");
203
+ return true;
204
+ } catch (testError) {
205
+ nodeInstance.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
206
+ return false;
285
207
  }
286
- };
208
+ }
287
209
 
288
- // MODIFIÉ: Ajout de la définition du queryTimeout sur la connexion
289
210
  this.on("input", async (msg, send, done) => {
211
+ this.currentQueryForErrorContext = null;
212
+ this.currentParamsForErrorContext = null;
213
+
290
214
  if (this.isAwaitingRetry) {
291
215
  if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
292
216
  this.log("New message received, overriding retry timer and attempting query now.");
293
- clearTimeout(this.retryTimer);
294
- this.retryTimer = null;
295
- this.isAwaitingRetry = false;
217
+ clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
296
218
  } else {
297
- this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
298
- if (done) done();
299
- return;
219
+ this.warn("Node is in a retry-wait state. New message ignored.");
220
+ if (done) done(); return;
300
221
  }
301
222
  }
302
223
  this.isAwaitingRetry = false;
@@ -307,30 +228,33 @@ module.exports = function (RED) {
307
228
  return done(new Error("ODBC Config node not properly configured."));
308
229
  }
309
230
 
310
- const executeWithConnection = async (connection) => {
311
- this.config.outputObj = this.config.outputObj || "payload";
231
+ const getRenderedQueryAndParams = async () => {
312
232
  const querySourceType = this.config.querySourceType || 'msg';
313
233
  const querySource = this.config.querySource || 'query';
314
234
  const paramsSourceType = this.config.paramsSourceType || 'msg';
315
235
  const paramsSource = this.config.paramsSource || 'parameters';
316
- const params = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
317
- let query = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
318
- if (!query) throw new Error("No query to execute");
319
- const isPreparedStatement = params || (query && query.includes("?"));
320
- if (!isPreparedStatement && query) {
321
- query = mustache.render(query, msg);
236
+
237
+ this.currentParamsForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
238
+ this.currentQueryForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
239
+
240
+ if (!this.currentQueryForErrorContext) throw new Error("No query to execute");
241
+
242
+ let finalQuery = this.currentQueryForErrorContext;
243
+ const isPreparedStatement = this.currentParamsForErrorContext || (finalQuery && finalQuery.includes("?"));
244
+ if (!isPreparedStatement && finalQuery) {
245
+ finalQuery = mustache.render(finalQuery, msg);
322
246
  }
247
+ return { query: finalQuery, params: this.currentParamsForErrorContext };
248
+ };
323
249
 
324
- // NOUVEAU: Appliquer le queryTimeout à la connexion avant exécution
325
- if (this.poolNode.config.queryTimeoutSeconds > 0) {
326
- try {
327
- connection.queryTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
328
- // this.log(`Query timeout set to ${connection.queryTimeout}s for this execution.`);
329
- } catch (e) {
330
- this.warn(`Could not set queryTimeout on connection: ${e.message}`);
331
- }
250
+ const executeUserQuery = async (connection, query, params) => {
251
+ // Appliquer le queryTimeout configuré
252
+ const configuredTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
253
+ if (configuredTimeout > 0) {
254
+ try { connection.queryTimeout = configuredTimeout; }
255
+ catch (e) { this.warn(`Could not set queryTimeout on connection: ${e.message}`); }
332
256
  } else {
333
- connection.queryTimeout = 0; // Assurer le reset au défaut du driver (infini)
257
+ connection.queryTimeout = 0; // Infini ou défaut driver
334
258
  }
335
259
 
336
260
  this.status({ fill: "blue", shape: "dot", text: "executing..." });
@@ -343,67 +267,107 @@ module.exports = function (RED) {
343
267
  }
344
268
  };
345
269
 
346
- let connectionFromPool;
347
- let errorAfterInitialAttempts = null;
270
+ let activeConnection = null; // Pour gérer la connexion active (pool ou fraîche)
271
+ let shouldProceedToTimedRetry = false;
272
+ let errorForTimedRetry = null;
348
273
 
349
- try {
350
- this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
351
- connectionFromPool = await this.poolNode.connect();
352
- await executeWithConnection(connectionFromPool);
274
+ try { // Tentative Principale (avec connexion du pool)
275
+ const { query, params } = await getRenderedQueryAndParams();
276
+
277
+ this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
278
+ activeConnection = await this.poolNode.connect();
279
+ await executeUserQuery(activeConnection, query, params);
280
+
281
+ if (activeConnection) { await activeConnection.close(); activeConnection = null; }
353
282
  return done();
354
- } catch (poolError) {
355
- this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
283
+
284
+ } catch (initialError) {
285
+ this.warn(`Initial attempt failed: ${initialError.message}`);
286
+ if (activeConnection) { // Si la connexion a été obtenue mais que executeUserQuery a échoué
287
+ const connStillGood = await testBasicConnectivity(activeConnection, this);
288
+ try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing pool conn after initial error: "+e.message);}
289
+
290
+ if (connStillGood) { // La connexion est bonne, l'erreur vient de la requête utilisateur
291
+ this.status({ fill: "red", shape: "ring", text: "SQL error" });
292
+ return done(this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error"));
293
+ }
294
+ }
295
+ // Si on arrive ici, la connexion poolée a eu un problème (soit pour se connecter, soit SELECT 1 a échoué)
296
+
356
297
  if (this.poolNode.config.retryFreshConnection) {
357
298
  this.warn("Attempting retry with a fresh connection.");
358
299
  this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
359
- let freshConnection;
360
300
  try {
361
301
  const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
362
- freshConnection = await odbcModule.connect(freshConnectConfig);
363
- this.log("Fresh connection established for retry.");
364
- await executeWithConnection(freshConnection);
302
+ activeConnection = await odbcModule.connect(freshConnectConfig);
303
+ this.log("Fresh connection established.");
304
+
305
+ const freshConnGood = await testBasicConnectivity(activeConnection, this);
306
+ if (!freshConnGood) {
307
+ // Erreur de connectivité même sur une connexion fraîche
308
+ errorForTimedRetry = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), this.currentQueryForErrorContext, this.currentParamsForErrorContext, "Fresh Connection Test Failed");
309
+ shouldProceedToTimedRetry = true;
310
+ throw errorForTimedRetry; // Va au catch externe de ce bloc try-fresh
311
+ }
312
+
313
+ // La connexion fraîche est bonne, on retente la requête utilisateur originale
314
+ const { query, params } = await getRenderedQueryAndParams(); // Re-préparer au cas où
315
+ await executeUserQuery(activeConnection, query, params);
316
+
365
317
  this.log("Query successful with fresh connection. Resetting pool.");
366
318
  await this.poolNode.resetPool();
367
- return done();
368
- } catch (freshError) {
369
- errorAfterInitialAttempts = this.enhanceError(freshError, null, null, "Retry with fresh connection also failed");
370
- } finally {
371
- if (freshConnection) await freshConnection.close();
319
+ if (activeConnection) { await activeConnection.close(); activeConnection = null; }
320
+ return done(); // Succès !
321
+
322
+ } catch (freshErrorOrConnectivityFail) {
323
+ // Soit odbcModule.connect a échoué, soit SELECT 1 a échoué (et errorForTimedRetry est déjà setté),
324
+ // soit executeUserQuery sur la connexion fraîche a échoué.
325
+ if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
326
+
327
+ if (shouldProceedToTimedRetry) { // Signifie que SELECT 1 sur la connexion fraîche a échoué
328
+ // errorForTimedRetry est déjà setté
329
+ } else {
330
+ // SELECT 1 sur connexion fraîche a réussi, mais la requête utilisateur a échoué. C'est une erreur SQL.
331
+ this.status({ fill: "red", shape: "ring", text: "SQL error (on retry)" });
332
+ return done(this.enhanceError(freshErrorOrConnectivityFail, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error (on fresh connection)"));
333
+ }
372
334
  }
373
- } else {
374
- errorAfterInitialAttempts = this.enhanceError(poolError);
335
+ } else { // Pas de retryFreshConnection configuré, l'erreur initiale était donc un problème de connexion.
336
+ errorForTimedRetry = this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "Connection Error");
337
+ shouldProceedToTimedRetry = true;
375
338
  }
376
- } finally {
377
- if (connectionFromPool) await connectionFromPool.close();
378
339
  }
379
-
380
- if (errorAfterInitialAttempts) {
340
+
341
+ // Logique de Retry Temporisé
342
+ if (shouldProceedToTimedRetry && errorForTimedRetry) {
381
343
  const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
382
344
  if (retryDelaySeconds > 0) {
383
- this.warn(`Query failed. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorAfterInitialAttempts.message}`);
345
+ this.warn(`Connection issue suspected. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorForTimedRetry.message}`);
384
346
  this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
385
347
  this.isAwaitingRetry = true;
386
348
  this.retryTimer = setTimeout(() => {
387
- this.isAwaitingRetry = false;
388
- this.retryTimer = null;
389
- this.log(`Retry timer expired for message. Re-emitting for node ${this.id || this.name}.`);
349
+ this.isAwaitingRetry = false; this.retryTimer = null;
350
+ this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
390
351
  this.receive(msg);
391
352
  }, retryDelaySeconds * 1000);
392
- if (done) return done();
393
- } else {
394
- this.status({ fill: "red", shape: "ring", text: "query error" });
395
- if (done) return done(errorAfterInitialAttempts);
353
+ return done(); // Termine l'invocation actuelle du message
354
+ } else { // Pas de délai de retry, ou délai à 0
355
+ this.status({ fill: "red", shape: "ring", text: "Connection Error" });
356
+ return done(errorForTimedRetry);
396
357
  }
358
+ } else if (errorForTimedRetry) { // Une erreur SQL a été identifiée et ne doit pas déclencher de retry de connexion
359
+ this.status({ fill: "red", shape: "ring", text: "Error (No Retry)" });
360
+ return done(errorForTimedRetry); // Devrait déjà avoir été fait
397
361
  } else {
398
- if (done) return done();
362
+ // Normalement, on ne devrait pas arriver ici si done() a été appelé après un succès.
363
+ this.log("[ODBC Node] DEBUG: Reached end of on('input') without error or prior done(). Calling done().");
364
+ return done();
399
365
  }
400
366
  });
401
367
 
402
368
  this.on("close", (done) => {
403
369
  if (this.retryTimer) {
404
- clearTimeout(this.retryTimer);
405
- this.retryTimer = null;
406
- this.isAwaitingRetry = false;
370
+ clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
407
371
  this.log("Cleared pending retry timer on node close/redeploy.");
408
372
  }
409
373
  this.status({});
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.4.0",
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",