@bkmj/node-red-contrib-odbcmj 2.4.0 → 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 +235 -106
  2. package/package.json +1 -1
package/odbc.js CHANGED
@@ -15,7 +15,7 @@ module.exports = function (RED) {
15
15
  if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
16
16
  this.config.queryTimeoutSeconds = 0;
17
17
  }
18
- this.closeOperationTimeout = 10000;
18
+ this.closeOperationTimeout = 10000; // 10 secondes
19
19
 
20
20
  this._buildConnectionString = function() {
21
21
  if (this.config.connectionMode === 'structured') {
@@ -131,12 +131,33 @@ module.exports = function (RED) {
131
131
  RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
132
132
 
133
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)
135
134
  const tempConfig = req.body;
136
- const buildTestConnectionString = () => { /* ... */ }; // Définition interne
135
+ const buildTestConnectionString = () => {
136
+ if (tempConfig.connectionMode === 'structured') {
137
+ if (!tempConfig.dbType || !tempConfig.server) { throw new Error("En mode structuré, le type de base de données et le serveur sont requis."); }
138
+ let driver;
139
+ let parts = [];
140
+ switch (tempConfig.dbType) {
141
+ case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
142
+ case 'postgresql': driver = 'PostgreSQL Unicode'; break;
143
+ case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
144
+ default: driver = tempConfig.driver || ''; break;
145
+ }
146
+ if(driver) parts.unshift(`DRIVER={${driver}}`);
147
+ parts.push(`SERVER=${tempConfig.server}`);
148
+ if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
149
+ if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
150
+ if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
151
+ return parts.join(';');
152
+ } else {
153
+ let connStr = tempConfig.connectionString || "";
154
+ if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
155
+ return connStr;
156
+ }
157
+ };
137
158
  let connection;
138
159
  try {
139
- const testConnectionString = buildTestConnectionString(); // Utilise la définition interne
160
+ const testConnectionString = buildTestConnectionString();
140
161
  const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
141
162
  connection = await odbcModule.connect(connectionOptions);
142
163
  res.sendStatus(200);
@@ -156,12 +177,10 @@ module.exports = function (RED) {
156
177
  this.isAwaitingRetry = false;
157
178
  this.retryTimer = null;
158
179
  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
161
-
180
+ this.currentQueryForErrorContext = null;
181
+ this.currentParamsForErrorContext = null;
162
182
 
163
183
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
164
- // Utilise this.currentQueryForErrorContext et this.currentParamsForErrorContext s'ils sont définis
165
184
  const q = query || this.currentQueryForErrorContext;
166
185
  const p = params || this.currentParamsForErrorContext;
167
186
  const queryContext = (() => {
@@ -185,27 +204,156 @@ module.exports = function (RED) {
185
204
  return finalError;
186
205
  };
187
206
 
188
- this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => { /* ... (inchangé) ... */ };
189
- this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => { /* ... (inchangé) ... */ };
207
+ this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
208
+ const result = await dbConnection.query(queryString, queryParams);
209
+ if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
210
+ const newMsg = RED.util.cloneMessage(msg);
211
+ const outputProperty = this.config.outputObj || "payload";
212
+ const otherParams = {};
213
+ let actualDataRows = [];
214
+ if (result !== null && typeof result === "object") {
215
+ if (Array.isArray(result)) {
216
+ actualDataRows = result.map(row => (typeof row === 'object' && row !== null) ? { ...row } : row);
217
+ for (const [key, value] of Object.entries(result)) {
218
+ if (isNaN(parseInt(key))) { otherParams[key] = value; }
219
+ }
220
+ } else {
221
+ for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
222
+ }
223
+ }
224
+ const columnMetadata = otherParams.columns;
225
+ if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
226
+ const sqlBitColumnNames = new Set();
227
+ columnMetadata.forEach(col => { if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") sqlBitColumnNames.add(col.name); });
228
+ if (sqlBitColumnNames.size > 0) {
229
+ actualDataRows.forEach(row => {
230
+ if (typeof row === "object" && row !== null) {
231
+ for (const columnName of sqlBitColumnNames) {
232
+ if (row.hasOwnProperty(columnName)) {
233
+ const value = row[columnName];
234
+ if (value === "1" || value === 1) row[columnName] = true;
235
+ else if (value === "0" || value === 0) row[columnName] = false;
236
+ }
237
+ }
238
+ }
239
+ });
240
+ }
241
+ }
242
+ objPath.set(newMsg, outputProperty, actualDataRows);
243
+ if (Object.keys(otherParams).length) newMsg.odbc = otherParams;
244
+ return newMsg;
245
+ };
246
+
247
+ this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
248
+ const chunkSize = parseInt(this.config.streamChunkSize) || 1;
249
+ const fetchSize = chunkSize > 100 ? 100 : chunkSize;
250
+ let cursor;
251
+ try {
252
+ cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
253
+ this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
254
+ let rowCount = 0;
255
+ let chunk = [];
256
+ while (true) {
257
+ const rows = await cursor.fetch();
258
+ if (!rows || rows.length === 0) break;
259
+ for (const row of rows) {
260
+ rowCount++;
261
+ const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
262
+ chunk.push(cleanRow);
263
+ if (chunk.length >= chunkSize) {
264
+ const newMsg = RED.util.cloneMessage(msg);
265
+ objPath.set(newMsg, this.config.outputObj || "payload", chunk);
266
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
267
+ send(newMsg);
268
+ chunk = [];
269
+ }
270
+ }
271
+ }
272
+ if (chunk.length > 0) {
273
+ const newMsg = RED.util.cloneMessage(msg);
274
+ objPath.set(newMsg, this.config.outputObj || "payload", chunk);
275
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
276
+ send(newMsg);
277
+ }
278
+ const finalMsg = RED.util.cloneMessage(msg);
279
+ objPath.set(finalMsg, this.config.outputObj || "payload", []);
280
+ finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
281
+ send(finalMsg);
282
+ this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
283
+ } finally {
284
+ if (cursor) {
285
+ try {
286
+ await Promise.race([
287
+ cursor.close(),
288
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout))
289
+ ]);
290
+ } catch (cursorCloseError) { this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`); }
291
+ }
292
+ }
293
+ };
190
294
 
191
- // NOUVELLE fonction utilitaire
192
- async function testBasicConnectivity(connection, nodeInstance) {
295
+ this.testBasicConnectivity = async function(connection) {
193
296
  if (!connection || typeof connection.query !== 'function') {
194
- nodeInstance.warn("Test de connectivité basique : connexion invalide fournie.");
297
+ this.warn("Test de connectivité basique : connexion invalide fournie.");
195
298
  return false;
196
299
  }
300
+ let originalTimeout;
197
301
  try {
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.");
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.");
203
306
  return true;
204
307
  } catch (testError) {
205
- nodeInstance.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
308
+ this.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
206
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é.")}
314
+ }
207
315
  }
208
- }
316
+ };
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
+ };
209
357
 
210
358
  this.on("input", async (msg, send, done) => {
211
359
  this.currentQueryForErrorContext = null;
@@ -225,75 +373,53 @@ module.exports = function (RED) {
225
373
 
226
374
  if (!this.poolNode) {
227
375
  this.status({ fill: "red", shape: "ring", text: "No config node" });
228
- return done(new Error("ODBC Config node not properly configured."));
376
+ return done(this.enhanceError(new Error("ODBC Config node not properly configured.")));
229
377
  }
230
-
231
- const getRenderedQueryAndParams = async () => {
232
- const querySourceType = this.config.querySourceType || 'msg';
233
- const querySource = this.config.querySource || 'query';
234
- const paramsSourceType = this.config.paramsSourceType || 'msg';
235
- const paramsSource = this.config.paramsSource || 'parameters';
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);
246
- }
247
- return { query: finalQuery, params: this.currentParamsForErrorContext };
248
- };
249
378
 
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}`); }
256
- } else {
257
- connection.queryTimeout = 0; // Infini ou défaut driver
258
- }
259
-
260
- this.status({ fill: "blue", shape: "dot", text: "executing..." });
261
- if (this.config.streaming) {
262
- await this.executeStreamQuery(connection, query, params, msg, send);
263
- } else {
264
- const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
265
- this.status({ fill: "green", shape: "dot", text: "success" });
266
- send(newMsg);
267
- }
268
- };
269
-
270
- let activeConnection = null; // Pour gérer la connexion active (pool ou fraîche)
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;
271
392
  let shouldProceedToTimedRetry = false;
272
- let errorForTimedRetry = null;
273
-
274
- try { // Tentative Principale (avec connexion du pool)
275
- const { query, params } = await getRenderedQueryAndParams();
276
-
393
+
394
+ try {
277
395
  this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
278
396
  activeConnection = await this.poolNode.connect();
279
- await executeUserQuery(activeConnection, query, params);
397
+ await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
280
398
 
281
- if (activeConnection) { await activeConnection.close(); activeConnection = null; }
282
- return done();
399
+ done();
400
+
401
+ if (activeConnection) {
402
+ try { await activeConnection.close(); } catch(e) { this.warn("Error closing pool connection after success: " + e.message); }
403
+ activeConnection = null;
404
+ }
405
+ return;
406
+
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
283
411
 
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);}
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; }
289
416
 
290
- if (connStillGood) { // La connexion est bonne, l'erreur vient de la requête utilisateur
417
+ if (connStillGood) {
291
418
  this.status({ fill: "red", shape: "ring", text: "SQL error" });
292
- return done(this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error"));
419
+ return done(this.enhanceError(initialDbError));
293
420
  }
294
421
  }
295
- // Si on arrive ici, la connexion poolée a eu un problème (soit pour se connecter, soit SELECT 1 a échoué)
296
-
422
+
297
423
  if (this.poolNode.config.retryFreshConnection) {
298
424
  this.warn("Attempting retry with a fresh connection.");
299
425
  this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
@@ -302,47 +428,50 @@ module.exports = function (RED) {
302
428
  activeConnection = await odbcModule.connect(freshConnectConfig);
303
429
  this.log("Fresh connection established.");
304
430
 
305
- const freshConnGood = await testBasicConnectivity(activeConnection, this);
431
+ const freshConnGood = await this.testBasicConnectivity(activeConnection);
306
432
  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");
433
+ errorForUser = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), null, null, "Fresh Connection Test Failed");
309
434
  shouldProceedToTimedRetry = true;
310
- throw errorForTimedRetry; // Va au catch externe de ce bloc try-fresh
435
+ throw errorForUser;
311
436
  }
312
437
 
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);
438
+ await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
316
439
 
317
440
  this.log("Query successful with fresh connection. Resetting pool.");
441
+ done();
442
+
318
443
  await this.poolNode.resetPool();
319
- if (activeConnection) { await activeConnection.close(); activeConnection = null; }
320
- return done(); // Succès !
444
+ if (activeConnection) {
445
+ try { await activeConnection.close(); } catch(e) { this.warn("Error closing fresh connection after success: " + e.message); }
446
+ activeConnection = null;
447
+ }
448
+ return;
321
449
 
322
450
  } 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
451
  if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
326
452
 
327
- if (shouldProceedToTimedRetry) { // Signifie que SELECT 1 sur la connexion fraîche a échoué
328
- // errorForTimedRetry est déjà setté
453
+ if (shouldProceedToTimedRetry) {
454
+ // errorForUser a été setté par l'échec du SELECT 1 sur la connexion fraîche
329
455
  } else {
330
- // SELECT 1 sur connexion fraîche a réussi, mais la requête utilisateur a échoué. C'est une erreur SQL.
331
456
  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)"));
457
+ return done(this.enhanceError(freshErrorOrConnectivityFail));
333
458
  }
334
459
  }
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");
460
+ } else {
461
+ errorForUser = this.enhanceError(initialDbError, null, null, "Connection Error (no fresh retry)");
337
462
  shouldProceedToTimedRetry = true;
338
463
  }
339
464
  }
340
465
 
341
- // Logique de Retry Temporisé
342
- if (shouldProceedToTimedRetry && errorForTimedRetry) {
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) {
343
472
  const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
344
473
  if (retryDelaySeconds > 0) {
345
- this.warn(`Connection issue suspected. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorForTimedRetry.message}`);
474
+ this.warn(`Connection issue. Scheduling retry in ${retryDelaySeconds}s. Error: ${errorForUser.message}`);
346
475
  this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
347
476
  this.isAwaitingRetry = true;
348
477
  this.retryTimer = setTimeout(() => {
@@ -350,17 +479,17 @@ module.exports = function (RED) {
350
479
  this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
351
480
  this.receive(msg);
352
481
  }, retryDelaySeconds * 1000);
353
- return done(); // Termine l'invocation actuelle du message
354
- } else { // Pas de délai de retry, ou délai à 0
482
+ return done();
483
+ } else {
355
484
  this.status({ fill: "red", shape: "ring", text: "Connection Error" });
356
- return done(errorForTimedRetry);
485
+ return done(errorForUser);
357
486
  }
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
487
+ } else if (errorForUser) {
488
+ this.status({ fill: "red", shape: "ring", text: "Error (No Timed Retry)" });
489
+ return done(errorForUser); // Cas c'est une erreur SQL identifiée, pas de retry temporisé.
361
490
  } else {
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().");
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().");
364
493
  return done();
365
494
  }
366
495
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.4.0",
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",