@bkmj/node-red-contrib-odbcmj 2.2.2 → 2.3.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 (4) hide show
  1. package/README.md +26 -16
  2. package/odbc.html +74 -147
  3. package/odbc.js +116 -137
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,7 @@ This node is a fork with significant enhancements to provide stability and advan
14
14
  - **Advanced Retry Logic**: Automatically handles connection errors with configurable delays and retries to ensure flow resilience.
15
15
  - **Result Streaming**: Process queries with millions of rows without exhausting memory by streaming results as chunks.
16
16
  - **Syntax Checker**: Optionally parse the SQL query to validate its structure.
17
+ - **Query Timeout**: Configure a timeout for query execution to prevent indefinite hangs.
17
18
 
18
19
  ---
19
20
 
@@ -46,21 +47,23 @@ This mode gives you full control for complex or non-standard connection strings.
46
47
  #### Test Connection
47
48
 
48
49
  A **Test Connection** button in the configuration panel allows you to instantly verify your settings without deploying the flow.
50
+ > **Note:** For security reasons, passwords are not reloaded into the editor. If your connection requires a password, you must **re-enter it** in the password field before clicking the test button (in Structured Mode). For Connection String mode, ensure the full string (including password if needed) is present in the connection string field itself.
49
51
 
50
- #### Pool Options
52
+ #### Pool & Connection Options
51
53
 
52
- - **`initialSize`** `<number>` (optional): The number of connections to create when the pool is initialized. Default: 5.
53
- - **`incrementSize`** `<number>` (optional): The number of connections to create when the pool is exhausted. Default: 5.
54
- - **`maxSize`** `<number>` (optional): The maximum number of connections allowed in the pool. Default: 15.
55
- - **`shrinkPool`** `<boolean>` (optional): Whether to reduce the number of connections to `initialSize` when they are returned to the pool. Default: true.
56
- - **`connectionTimeout`** `<number>` (optional): The number of seconds for a connection to remain idle before closing. Default: 3.
57
- - **`loginTimeout`** `<number>` (optional): The number of seconds for an attempt to create a connection to succeed. Default: 3.
54
+ - **`Initial Pool Size`** `<number>` (optional): The number of connections to create when the pool is initialized. Default: 5.
55
+ - **`Increment Pool Size`** `<number>` (optional): The number of connections to create when the pool is exhausted. Default: 5.
56
+ - **`Max Pool Size`** `<number>` (optional): The maximum number of connections allowed in the pool. Default: 15.
57
+ - **`Shrink Pool`** `<boolean>` (optional): If checked, reduces the number of connections to `Initial Pool Size` when they are returned to the pool if the pool has grown. Default: true.
58
+ - **`Idle Timeout`** `<number>` (optional): The number of seconds for a connection in the pool to remain idle before closing. Default: 3 seconds. (Refers to the `connectionTimeout` property of the `odbc` library's pool options).
59
+ - **`Login Timeout`** `<number>` (optional): The number of seconds for an attempt to establish a new connection to succeed. Default: 5 seconds.
60
+ - **`Query Timeout`** `<number>` (optional): The number of seconds for a query to execute before timing out. A value of **0** means infinite or uses the driver/database default. Default: 0 seconds.
58
61
 
59
62
  #### Error Handling & Retry
60
63
 
61
- - **`retryFreshConnection`** `<boolean>` (optional): If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections. Default: false.
62
- - **`retryDelay`** `<number>` (optional): If both the pooled and the fresh connection attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries. Default: 5.
63
- - **`retryOnMsg`** `<boolean>` (optional): If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry. Default: true.
64
+ - **`Retry with fresh connection`** `<boolean>` (optional): If a query fails using a connection from the pool, the node will try once more with a brand new, direct connection. If this succeeds, the entire connection pool is reset to clear any potentially stale connections. Default: false.
65
+ - **`Retry Delay`** `<number>` (optional): If all immediate attempts (pooled and, if enabled, fresh connection) fail, this sets a delay in seconds before another retry is attempted for the incoming message. A value of **0** disables this timed retry mechanism. Default: 5.
66
+ - **`Retry on new message`** `<boolean>` (optional): If the node is waiting for a timed retry (due to `Retry Delay`), a new incoming message can, if this is checked, override the timer and trigger an immediate retry of the *original* message that failed. Default: true.
64
67
 
65
68
  #### Advanced
66
69
 
@@ -100,9 +103,16 @@ For queries that return a large number of rows, streaming prevents high memory u
100
103
 
101
104
  ##### Streaming Output Format
102
105
 
103
- When streaming is active, each output message will contain:
104
- - A payload (or the configured output property) containing an array of rows for the current chunk.
105
- - A `msg.odbc_stream` object with metadata for tracking progress:
106
- - `index`: The starting index of the current chunk (e.g., 0, 100, 200...).
107
- - `count`: The number of rows in the current chunk.
108
- - `complete`: A boolean that is `true` only on the very last message, and `false` otherwise. The last payload will always be an empty array. This is useful for triggering a downstream action once all rows have been processed.
106
+ When streaming is active, the node sends messages in sequence:
107
+
108
+ 1. **Data Messages**: One or more messages where the payload (or the configured output property) contains an array of rows for the current chunk. For these messages, `msg.odbc_stream.complete` will be **`false`**.
109
+ 2. **Completion Message**: A single, final message indicating the end of the stream. For this message:
110
+ - The payload (or configured output property) will be an **empty array `[]`**.
111
+ - `msg.odbc_stream.complete` will be **`true`**.
112
+
113
+ The `msg.odbc_stream` object contains metadata for tracking:
114
+ - `index`: The starting index of the current chunk (0-based). For the completion message, this will be the total number of data rows processed.
115
+ - `count`: The number of rows in the current chunk. This will be `0` for the completion message.
116
+ - `complete`: The boolean flag (`true`/`false`).
117
+
118
+ This pattern ensures you can reliably trigger a final action (like closing a file or calculating an aggregate) only when the message with `complete: true` is received.
package/odbc.html CHANGED
@@ -13,14 +13,15 @@
13
13
  database: { value: "" },
14
14
  user: { value: "" },
15
15
  connectionString: { value: "" },
16
- initialSize: { value: 5 },
17
- incrementSize: { value: 5 },
18
- maxSize: { value: 15 },
16
+ initialSize: { value: 5, validate: RED.validators.number(true) },
17
+ incrementSize: { value: 5, validate: RED.validators.number(true) },
18
+ maxSize: { value: 15, validate: RED.validators.number(true) },
19
19
  shrink: { value: true },
20
- connectionTimeout: { value: 3 },
21
- loginTimeout: { value: 3 },
20
+ connectionTimeout: { value: 3, validate: RED.validators.number(true) }, // Idle timeout for connections in pool (odbc lib specific)
21
+ loginTimeout: { value: 5, validate: RED.validators.number(true) }, // Login timeout for new connections
22
+ queryTimeoutSeconds: { value: 0, validate: RED.validators.number(true) }, // NOUVEAU: Timeout pour les requêtes
22
23
  retryFreshConnection: { value: false },
23
- retryDelay: { value: 5, validate: RED.validators.number() },
24
+ retryDelay: { value: 5, validate: RED.validators.number(true) },
24
25
  retryOnMsg: { value: true },
25
26
  syntaxtick: { value: false },
26
27
  syntax: { value: "mysql" },
@@ -79,19 +80,22 @@
79
80
  RED.notify("Le champ 'Server' est requis pour le test.", {type: "warning", timeout: 3000});
80
81
  return;
81
82
  }
82
- } else {
83
+ if ($("#node-config-input-password").val() === "" && $("#node-config-input-user").val().trim() !== "") {
84
+ RED.notify("Note: Pour tester une connexion structurée avec mot de passe, veuillez le (re)saisir.", {type: "info", timeout: 4500});
85
+ // On ne bloque pas, l'utilisateur peut vouloir tester une connexion sans mot de passe si le user est optionnel
86
+ }
87
+ } else { // Mode 'string'
83
88
  var connStr = $("#node-config-input-connectionString").val().trim();
84
89
  if (!connStr) {
85
90
  RED.notify("La chaîne de connexion est requise pour le test.", {type: "warning", timeout: 3000});
86
- return;
91
+ return;
87
92
  }
88
- // Nouvelle validation :
89
- // Elle est invalide SEULEMENT si elle ne contient PAS de DSN ET qu'elle ne respecte PAS l'ancien critère pour les chaînes DSN-less.
93
+
90
94
  var isDsnString = /DSN=[^;]+/i.test(connStr);
91
- var isDriverBasedString = /DRIVER=\{.+?\}/i.test(connStr) && /(SERVER|DATABASE|UID|PWD)=/i.test(connStr); // Un peu plus complet
95
+ var isDriverBasedString = /DRIVER=\{.+?\}/i.test(connStr) && /(SERVER|DATABASE|UID|PWD)=[^;]+/i.test(connStr);
92
96
 
93
97
  if (!isDsnString && !isDriverBasedString) {
94
- RED.notify("La chaîne de connexion semble invalide ou incomplète (ex: DSN=value; ou DRIVER={...};SERVER=...;).", {type: "warning", timeout: 4000});
98
+ RED.notify("La chaîne de connexion semble invalide ou incomplète (ex: DSN=valeur; ou DRIVER={...};SERVER=...;).", {type: "warning", timeout: 4500});
95
99
  return;
96
100
  }
97
101
  }
@@ -109,7 +113,7 @@
109
113
  database: $("#node-config-input-database").val(),
110
114
  user: $("#node-config-input-user").val(),
111
115
  connectionString: $("#node-config-input-connectionString").val(),
112
- password: (connectionMode === 'structured') ? $("#node-config-input-password").val() : null
116
+ password: $("#node-config-input-password").val()
113
117
  };
114
118
 
115
119
  $.ajax({
@@ -135,22 +139,10 @@
135
139
  </script>
136
140
 
137
141
  <style>
138
- .form-section-header {
139
- cursor: pointer;
140
- padding: 5px;
141
- border-bottom: 1px solid #ddd;
142
- margin-bottom: 10px;
143
- user-select: none;
144
- }
145
- .form-section-header i.fa-caret-right {
146
- transition: transform 0.2s ease-in-out;
147
- }
148
- .form-section-header.expanded i.fa-caret-right {
149
- transform: rotate(90deg);
150
- }
151
- .form-section-content {
152
- padding-left: 20px;
153
- }
142
+ .form-section-header { cursor: pointer; padding: 5px; border-bottom: 1px solid #ddd; margin-bottom: 10px; user-select: none; }
143
+ .form-section-header i.fa-caret-right { transition: transform 0.2s ease-in-out; }
144
+ .form-section-header.expanded i.fa-caret-right { transform: rotate(90deg); }
145
+ .form-section-content { padding-left: 20px; }
154
146
  </style>
155
147
 
156
148
  <script type="text/html" data-template-name="odbc config">
@@ -213,33 +205,41 @@
213
205
 
214
206
  <hr/>
215
207
 
216
- <div class="form-section-header"><h4><i class="fa fa-caret-right"></i> <i class="fa fa-sitemap"></i> Pool Options</h4></div>
208
+ <div class="form-section-header"><h4><i class="fa fa-caret-right"></i> <i class="fa fa-sitemap"></i> Pool & Connection Options</h4></div>
217
209
  <div class="form-section-content" style="display: none;">
218
210
  <div class="form-row">
219
- <label for="node-config-input-initialSize"><i class="fa fa-play"></i> Initial Size</label>
211
+ <label for="node-config-input-initialSize"><i class="fa fa-play"></i> Initial Pool Size</label>
220
212
  <input type="number" id="node-config-input-initialSize" placeholder="5" />
221
213
  </div>
222
214
  <div class="form-row">
223
- <label for="node-config-input-incrementSize"><i class="fa fa-plus"></i> Increment Size</label>
215
+ <label for="node-config-input-incrementSize"><i class="fa fa-plus"></i> Increment Pool Size</label>
224
216
  <input type="number" id="node-config-input-incrementSize" placeholder="5" />
225
217
  </div>
226
218
  <div class="form-row">
227
- <label for="node-config-input-maxSize"><i class="fa fa-stop"></i> Max Size</label>
219
+ <label for="node-config-input-maxSize"><i class="fa fa-stop"></i> Max Pool Size</label>
228
220
  <input type="number" id="node-config-input-maxSize" placeholder="15" />
229
221
  </div>
230
222
  <div class="form-row">
231
223
  <label for="node-config-input-shrink"><i class="fa fa-compress"></i> Shrink Pool</label>
232
224
  <input type="checkbox" id="node-config-input-shrink" style="margin-left:0px; vertical-align:top; width:auto !important;" />
225
+ <span class="form-tips" style="font-size: smaller;">Reduce pool to initial size when connections are returned.</span>
233
226
  </div>
234
227
  <div class="form-row">
235
- <label for="node-config-input-connectionTimeout"><i class="fa fa-clock-o"></i> Connection Timeout</label>
228
+ <label for="node-config-input-connectionTimeout"><i class="fa fa-clock-o"></i> Idle Timeout</label>
236
229
  <input type="number" id="node-config-input-connectionTimeout" placeholder="3" style="width: 80px;"/>
237
230
  <span style="margin-left: 5px;">seconds</span>
231
+ <span class="form-tips" style="font-size: smaller;">For connections in pool.</span>
238
232
  </div>
239
233
  <div class="form-row">
240
- <label for="node-config-input-loginTimeout"><i class="fa fa-clock-o"></i> Login Timeout</label>
241
- <input type="number" id="node-config-input-loginTimeout" placeholder="3" style="width: 80px;" />
234
+ <label for="node-config-input-loginTimeout"><i class="fa fa-sign-in"></i> Login Timeout</label>
235
+ <input type="number" id="node-config-input-loginTimeout" placeholder="5" style="width: 80px;" />
242
236
  <span style="margin-left: 5px;">seconds</span>
237
+ <span class="form-tips" style="font-size: smaller;">For establishing new connections.</span>
238
+ </div>
239
+ <div class="form-row">
240
+ <label for="node-config-input-queryTimeoutSeconds"><i class="fa fa-hourglass-half"></i> Query Timeout</label>
241
+ <input type="number" id="node-config-input-queryTimeoutSeconds" placeholder="0" style="width: 80px;" />
242
+ <span style="margin-left: 5px;">seconds (0=infinite/driver default)</span>
243
243
  </div>
244
244
  </div>
245
245
 
@@ -248,16 +248,18 @@
248
248
  <div class="form-row">
249
249
  <label for="node-config-input-retryFreshConnection" style="width: auto;"><i class="fa fa-refresh"></i> Retry with fresh connection</label>
250
250
  <input type="checkbox" id="node-config-input-retryFreshConnection" style="display: inline-block; width: auto; vertical-align: top;" />
251
+ <span class="form-tips" style="font-size: smaller;">If a pooled connection fails, try once with a new one.</span>
251
252
  </div>
252
253
  <div class="retry-options" style="padding-left: 20px;">
253
254
  <div class="form-row">
254
255
  <label for="node-config-input-retryDelay"><i class="fa fa-history"></i> Retry Delay</label>
255
256
  <input type="number" id="node-config-input-retryDelay" placeholder="5" style="width: 80px;" />
256
- <span style="margin-left: 5px;">seconds</span>
257
+ <span style="margin-left: 5px;">seconds (0=disable timed retry)</span>
257
258
  </div>
258
259
  <div class="form-row">
259
260
  <label for="node-config-input-retryOnMsg" style="width: auto;"><i class="fa fa-envelope-o"></i> Retry on new message</label>
260
261
  <input type="checkbox" id="node-config-input-retryOnMsg" style="display: inline-block; width: auto; vertical-align: top;" />
262
+ <span class="form-tips" style="font-size: smaller;">If waiting, a new message triggers immediate retry.</span>
261
263
  </div>
262
264
  </div>
263
265
  </div>
@@ -287,112 +289,38 @@
287
289
 
288
290
  <script type="text/javascript">
289
291
  RED.nodes.registerType("odbc", {
290
- category: "storage",
291
- color: "#89A5C0",
292
- defaults: {
293
- name: { value: "" },
294
- connection: { type: "odbc config", required: true },
295
- query: { value: "" },
296
- outputObj: { value: "payload" },
297
- streaming: { value: false },
298
- streamChunkSize: { value: 1, validate: RED.validators.number() },
299
- querySource: { value: "query", required: false },
300
- querySourceType: { value: "msg", required: false },
301
- paramsSource: { value: "parameters", required: false },
302
- paramsSourceType: { value: "msg", required: false }
303
- },
304
- inputs: 1,
305
- outputs: 1,
306
- icon: "db.svg",
307
- label: function () {
308
- return this.name || "odbc";
309
- },
292
+ category: "storage", color: "#89A5C0",
293
+ defaults: { name: { value: "" }, connection: { type: "odbc config", required: true }, query: { value: "" }, outputObj: { value: "payload" }, streaming: { value: false }, streamChunkSize: { value: 1, validate: RED.validators.number() }, querySource: { value: "query", required: false }, querySourceType: { value: "msg", required: false }, paramsSource: { value: "parameters", required: false }, paramsSourceType: { value: "msg", required: false } },
294
+ inputs: 1, outputs: 1, icon: "db.svg", label: function () { return this.name || "odbc"; },
310
295
  oneditprepare: function () {
311
- this.editor = RED.editor.createEditor({
312
- id: "node-input-query-editor",
313
- mode: "ace/mode/sql",
314
- value: this.query,
315
- });
316
-
317
- $("#node-input-streaming").on("change", function() {
318
- $(".stream-options").toggle(this.checked);
319
- }).trigger("change");
320
-
321
- $("#node-input-querySource").typedInput({
322
- default: 'msg',
323
- typeField: "#node-input-querySourceType",
324
- types: ['msg', 'flow', 'global', 'env', 'str', 'jsonata']
325
- });
326
-
327
- $("#node-input-paramsSource").typedInput({
328
- default: 'msg',
329
- typeField: "#node-input-paramsSourceType",
330
- types: ['msg', 'flow', 'global', 'env', 'jsonata']
331
- });
332
- },
333
- oneditsave: function () {
334
- this.query = this.editor.getValue();
335
- this.editor.destroy();
336
- delete this.editor;
337
- },
338
- oneditcancel: function () {
339
- this.editor.destroy();
340
- delete this.editor;
296
+ this.editor = RED.editor.createEditor({ id: "node-input-query-editor", mode: "ace/mode/sql", value: this.query, });
297
+ $("#node-input-streaming").on("change", function() { $(".stream-options").toggle(this.checked); }).trigger("change");
298
+ $("#node-input-querySource").typedInput({ default: 'msg', typeField: "#node-input-querySourceType", types: ['msg', 'flow', 'global', 'env', 'str', 'jsonata'] });
299
+ $("#node-input-paramsSource").typedInput({ default: 'msg', typeField: "#node-input-paramsSourceType", types: ['msg', 'flow', 'global', 'env', 'jsonata'] });
341
300
  },
301
+ oneditsave: function () { this.query = this.editor.getValue(); this.editor.destroy(); delete this.editor; },
302
+ oneditcancel: function () { this.editor.destroy(); delete this.editor; },
342
303
  });
343
304
  </script>
344
305
 
345
306
  <script type="text/html" data-template-name="odbc">
346
- <div class="form-row">
347
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
348
- <input type="text" id="node-input-name" />
349
- </div>
350
- <div class="form-row">
351
- <label for="node-input-connection"><i class="fa fa-cog"></i> Connection</label>
352
- <input type="text" id="node-input-connection" />
353
- </div>
354
- <div class="form-row node-text-editor-row">
355
- <label for="node-input-query" style="width: 100% !important;"><i class="fa fa-file-code-o"></i> Query (fallback)</label>
356
- <div style="height: 250px;" class="node-text-editor" id="node-input-query-editor"></div>
357
- </div>
358
- <div class="form-row">
359
- <label for="node-input-outputObj"><i class="fa fa-sign-out"></i> Result to</label>
360
- <span>msg.</span><input type="text" id="node-input-outputObj" placeholder="payload" style="width: 64%;"/>
361
- </div>
362
-
307
+ <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> <input type="text" id="node-input-name" /> </div>
308
+ <div class="form-row"> <label for="node-input-connection"><i class="fa fa-cog"></i> Connection</label> <input type="text" id="node-input-connection" /> </div>
309
+ <div class="form-row node-text-editor-row"> <label for="node-input-query" style="width: 100% !important;"><i class="fa fa-file-code-o"></i> Query (fallback)</label> <div style="height: 250px;" class="node-text-editor" id="node-input-query-editor"></div> </div>
310
+ <div class="form-row"> <label for="node-input-outputObj"><i class="fa fa-sign-out"></i> Result to</label> <span>msg.</span><input type="text" id="node-input-outputObj" placeholder="payload" style="width: 64%;"/> </div>
363
311
  <hr/>
364
-
365
- <div class="form-row">
366
- <label for="node-input-querySource"><i class="fa fa-crosshairs"></i> Query Source</label>
367
- <input type="text" id="node-input-querySource" style="width: 70%;">
368
- <input type="hidden" id="node-input-querySourceType">
369
- </div>
370
- <div class="form-row">
371
- <label for="node-input-paramsSource"><i class="fa fa-list-ol"></i> Parameters Source</label>
372
- <input type="text" id="node-input-paramsSource" style="width: 70%;">
373
- <input type="hidden" id="node-input-paramsSourceType">
374
- </div>
375
-
312
+ <div class="form-row"> <label for="node-input-querySource"><i class="fa fa-crosshairs"></i> Query Source</label> <input type="text" id="node-input-querySource" style="width: 70%;"> <input type="hidden" id="node-input-querySourceType"> </div>
313
+ <div class="form-row"> <label for="node-input-paramsSource"><i class="fa fa-list-ol"></i> Parameters Source</label> <input type="text" id="node-input-paramsSource" style="width: 70%;"> <input type="hidden" id="node-input-paramsSourceType"> </div>
376
314
  <hr/>
377
-
378
- <div class="form-row">
379
- <label for="node-input-streaming" style="width: auto;"><i class="fa fa-arrows-v"></i> Stream Results</label>
380
- <input type="checkbox" id="node-input-streaming" style="display: inline-block; width: auto; vertical-align: top;">
381
- </div>
382
- <div class="form-row stream-options">
383
- <label for="node-input-streamChunkSize"><i class="fa fa-bars"></i> Chunk Size</label>
384
- <input type="number" id="node-input-streamChunkSize" placeholder="1" style="width: 100px;">
385
- <span class="form-tips">Number of rows per output message.</span>
386
- </div>
315
+ <div class="form-row"> <label for="node-input-streaming" style="width: auto;"><i class="fa fa-arrows-v"></i> Stream Results</label> <input type="checkbox" id="node-input-streaming" style="display: inline-block; width: auto; vertical-align: top;"> </div>
316
+ <div class="form-row stream-options"> <label for="node-input-streamChunkSize"><i class="fa fa-bars"></i> Chunk Size</label> <input type="number" id="node-input-streamChunkSize" placeholder="1" style="width: 100px;"> <span class="form-tips">Number of rows per output message.</span> </div>
387
317
  </script>
388
318
 
389
319
 
390
320
  <script type="text/markdown" data-help-name="odbc config">
391
321
  A configuration node that manages the connection to your database.
392
-
393
322
  ### Connection Modes
394
323
  Version 2.0 introduces two ways to configure your connection:
395
-
396
324
  #### 1. Structured Fields Mode (Recommended)
397
325
  This is the easiest and most secure way to set up a connection for common databases.
398
326
  - **Database Type**: Select your database (e.g., SQL Server, PostgreSQL, MySQL). The node will use the appropriate driver name and connection string syntax. For unlisted databases, choose "Other" and provide the driver name manually.
@@ -400,49 +328,49 @@ This is the easiest and most secure way to set up a connection for common databa
400
328
  - **Database**: The name of the database to connect to (optional).
401
329
  - **User**: The username for authentication.
402
330
  - **Password**: The password for authentication. This is stored securely using Node-RED's credential system.
403
-
404
331
  #### 2. Connection String Mode (Advanced)
405
332
  This mode gives you full control for complex or non-standard connection strings.
406
333
  - **Connection String**: Enter the complete ODBC connection string. It is your responsibility to provide a valid string for your driver.
407
334
 
408
335
  ### Test Connection
409
336
  A **Test Connection** button in the configuration panel allows you to instantly verify your settings without deploying the flow.
337
+ > **Note:** For security reasons, passwords are not reloaded into the editor. If your connection requires a password, you must **re-enter it** in the password field before clicking the test button (in Structured Mode). For Connection String mode, ensure the full string (including password if needed) is present in the connection string field itself.
410
338
 
411
- ### Pool Options
412
- - **`initialSize`**: The number of connections to create when the pool is initialized. Default: 5.
413
- - **`maxSize`**: The maximum number of connections allowed in the pool. Default: 15.
414
- - (See `odbc` package documentation for more details on pool options).
339
+ ### Pool & Connection Options
340
+ - **`Initial Pool Size`**: The number of connections to create when the pool is initialized. Default: 5.
341
+ - **`Increment Pool Size`**: The number of connections to create when the pool is exhausted. Default: 5.
342
+ - **`Max Pool Size`**: The maximum number of connections allowed in the pool. Default: 15.
343
+ - **`Shrink Pool`**: If checked, reduces the number of connections to `Initial Pool Size` when they are returned to the pool if the pool has grown. Default: true.
344
+ - **`Idle Timeout`**: The number of seconds for a connection in the pool to remain idle before closing. Default: 3 seconds.
345
+ - **`Login Timeout`**: The number of seconds for an attempt to establish a new connection to succeed. Default: 5 seconds.
346
+ - **`Query Timeout`**: The number of seconds for a query to execute before timing out. A value of **0** means infinite or uses the driver/database default. Default: 0 seconds.
415
347
 
416
348
  ### Error Handling & Retry
417
- - **`retryFreshConnection`**: If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections.
418
- - **`retryDelay`**: If both attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries.
419
- - **`retryOnMsg`**: If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry.
349
+ - **`Retry with fresh connection`**: If a query fails using a connection from the pool, the node will try once more with a brand new, direct connection. If this succeeds, the entire connection pool is reset to clear any potentially stale connections.
350
+ - **`Retry Delay`**: If all immediate attempts (pooled and, if enabled, fresh connection) fail, this sets a delay in seconds before another retry is attempted for the incoming message. A value of **0** disables this timed retry mechanism.
351
+ - **`Retry on new message`**: If the node is waiting for a timed retry (due to `Retry Delay`), a new incoming message can, if this is checked, override the timer and trigger an immediate retry of the *original* message that failed.
420
352
 
421
353
  ### Advanced
422
354
  - **`syntaxChecker`**: If activated, the query string will be parsed and appended to the output message at `msg.parsedQuery`.
355
+ - **`Syntax`**: The SQL dialect to use for the syntax checker (e.g., mysql, postgresql, etc.).
423
356
  </script>
424
357
 
425
358
  <script type="text/markdown" data-help-name="odbc">
426
359
  Executes a query against a configured ODBC data source.
427
360
 
428
361
  ### Inputs
429
-
430
362
  - **Query Source** (optional): Specify where to get the query string from. You can use a message property (`msg.`), flow context (`flow.`), global context (`global.`), an environment variable (`env.`), or a JSONata expression. If the source is empty or not found, the node will use the query from the "Query (fallback)" editor below.
431
363
  - *Default behavior (for backward compatibility): `msg.query`*
432
-
433
364
  - **Parameters Source** (optional): Specify where to get the parameters for a prepared statement. This should resolve to an array of values.
434
365
  - *Default behavior (for backward compatibility): `msg.parameters`*
435
366
 
436
367
  ### Properties
437
-
438
368
  - **Connection**: The `odbc config` node to use.
439
- - **Query (fallback)**: A static SQL query to run if the "Query (fallback)" does not provide one. Can contain Mustache syntax (e.g., `{{{payload.id}}}`).
369
+ - **Query (fallback)**: A static SQL query to run if the "Query Source" does not provide one. Can contain Mustache syntax (e.g., `{{{payload.id}}}`).
440
370
  - **Result to**: The `msg` property where the query result will be stored. Default: `payload`.
441
371
 
442
372
  ### Streaming Results
443
-
444
- For queries that return a large number of rows, streaming prevents high memory usage by sending multiple messages instead of one large one.
445
-
373
+ For queries that return a large number of rows, streaming prevents high memory usage.
446
374
  - **`Stream Results`**: Enables or disables streaming mode.
447
375
  - **`Chunk Size`**: The number of rows to include in each output message. A value of `1` means one message will be sent for every single row.
448
376
 
@@ -450,14 +378,13 @@ For queries that return a large number of rows, streaming prevents high memory u
450
378
  When streaming is active, the node sends messages in sequence:
451
379
 
452
380
  1. **Data Messages**: One or more messages where the payload (or the configured output property) contains an array of rows for the current chunk. For these messages, `msg.odbc_stream.complete` will be **`false`**.
453
-
454
381
  2. **Completion Message**: A single, final message indicating the end of the stream. For this message:
455
- - The payload will be an **empty array `[]`**.
382
+ - The payload (or configured output property) will be an **empty array `[]`**.
456
383
  - `msg.odbc_stream.complete` will be **`true`**.
457
384
 
458
385
  The `msg.odbc_stream` object contains metadata for tracking:
459
- - `index`: The starting index of the current chunk. For the completion message, this will be the total number of rows processed.
460
- - `count`: The number of rows in the chunk. This will be `0` for the completion message.
386
+ - `index`: The starting index of the current chunk (0-based). For the completion message, this will be the total number of data rows processed.
387
+ - `count`: The number of rows in the current chunk. This will be `0` for the completion message.
461
388
  - `complete`: The boolean flag (`true`/`false`).
462
389
 
463
390
  This pattern ensures you can reliably trigger a final action (like closing a file or calculating an aggregate) only when the message with `complete: true` is received.
package/odbc.js CHANGED
@@ -12,6 +12,12 @@ 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)
19
+ this.closeOperationTimeout = 10000; // 10 secondes
20
+
15
21
  this._buildConnectionString = function() {
16
22
  if (this.config.connectionMode === 'structured') {
17
23
  if (!this.config.dbType || !this.config.server) {
@@ -30,6 +36,8 @@ module.exports = function (RED) {
30
36
  if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
31
37
  if (this.config.user) parts.push(`UID=${this.config.user}`);
32
38
  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}`);
33
41
  return parts.join(';');
34
42
  } else {
35
43
  let connStr = this.config.connectionString || "";
@@ -45,11 +53,19 @@ module.exports = function (RED) {
45
53
  const finalConnectionString = this._buildConnectionString();
46
54
  if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
47
55
 
48
- const poolParams = { ...this.config };
56
+ const poolParams = { ...this.config }; // Contient initialSize, maxSize, loginTimeout, connectionTimeout (idle) etc.
49
57
  poolParams.connectionString = finalConnectionString;
50
58
 
51
- ['retryFreshConnection', 'retryDelay', 'retryOnMsg', 'syntax', 'connectionMode', 'dbType', 'server', 'database', 'user', 'driver'].forEach(k => delete poolParams[k]);
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]);
52
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
53
69
  this.pool = await odbcModule.pool(poolParams);
54
70
  this.connecting = false;
55
71
  this.status({ fill: "green", shape: "dot", text: "Pool ready" });
@@ -62,49 +78,73 @@ module.exports = function (RED) {
62
78
  }
63
79
  }
64
80
  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)
65
83
  return await this.pool.connect();
66
84
  } catch (poolConnectError) {
67
- this.error(`Error connecting to pool: ${poolConnectError}`, poolConnectError);
85
+ this.error(`Error connecting to pool: ${poolConnectError.message}`, poolConnectError);
68
86
  this.status({ fill: "red", shape: "ring", text: "Pool connect err" });
69
87
  throw poolConnectError;
70
88
  }
71
89
  };
72
90
 
73
91
  this.getFreshConnectionConfig = function() {
92
+ // Ces timeouts sont pour odbcModule.connect (connexion unique)
74
93
  return {
75
94
  connectionString: this._buildConnectionString(),
76
- connectionTimeout: parseInt(this.config.connectionTimeout) || 0,
77
- loginTimeout: parseInt(this.config.loginTimeout) || 0,
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.
78
98
  };
79
99
  };
80
100
 
101
+ // MODIFIÉ: Ajout de timeout pour pool.close()
81
102
  this.resetPool = async () => {
82
- if (this.pool) {
103
+ if (this.pool) {
83
104
  this.log("Resetting connection pool.");
84
105
  this.status({ fill: "yellow", shape: "ring", text: "Resetting pool..." });
106
+ let closedSuccessfully = false;
85
107
  try {
86
- await this.pool.close();
108
+ await Promise.race([
109
+ this.pool.close(),
110
+ new Promise((_, reject) =>
111
+ setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout)
112
+ )
113
+ ]);
87
114
  this.log("Connection pool closed successfully for reset.");
115
+ closedSuccessfully = true;
88
116
  } catch (closeError) {
89
- this.error(`Error closing pool during reset: ${closeError}`, closeError);
117
+ this.error(`Error or timeout closing pool during reset: ${closeError.message}`, closeError);
90
118
  } finally {
91
119
  this.pool = null;
92
120
  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
+ }
93
126
  }
94
127
  } else {
95
128
  this.log("Pool reset requested, but no active pool to reset.");
96
129
  }
97
130
  };
98
131
 
132
+ // MODIFIÉ: Ajout de timeout pour pool.close()
99
133
  this.on("close", async (removed, done) => {
100
134
  this.log("Closing ODBC config node. Attempting to close pool.");
101
135
  if (this.pool) {
102
136
  try {
103
- await this.pool.close();
137
+ await Promise.race([
138
+ this.pool.close(),
139
+ new Promise((_, reject) =>
140
+ setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout)
141
+ )
142
+ ]);
104
143
  this.log("Connection pool closed successfully on node close.");
105
- this.pool = null;
106
144
  } catch (error) {
107
- this.error(`Error closing connection pool on node close: ${error}`, error);
145
+ this.error(`Error or timeout closing connection pool on node close: ${error.message}`, error);
146
+ } finally {
147
+ this.pool = null; // S'assurer que le pool est marqué comme null
108
148
  }
109
149
  }
110
150
  done();
@@ -141,9 +181,7 @@ module.exports = function (RED) {
141
181
  return parts.join(';');
142
182
  } else {
143
183
  let connStr = tempConfig.connectionString || "";
144
- if (!connStr) {
145
- throw new Error("La chaîne de connexion ne peut pas être vide.");
146
- }
184
+ if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
147
185
  return connStr;
148
186
  }
149
187
  };
@@ -151,25 +189,20 @@ module.exports = function (RED) {
151
189
  let connection;
152
190
  try {
153
191
  const testConnectionString = buildTestConnectionString();
154
-
155
- // ==============================================================
156
- // LIGNE DE DÉBOGAGE AJOUTÉE
157
- // ==============================================================
158
192
  console.log("[ODBC Test] Attempting to connect with string:", testConnectionString);
159
- // ==============================================================
160
-
193
+
161
194
  const connectionOptions = {
162
195
  connectionString: testConnectionString,
163
- loginTimeout: 10
196
+ loginTimeout: 10 // Déjà présent et correct
164
197
  };
165
198
  connection = await odbcModule.connect(connectionOptions);
166
199
  res.sendStatus(200);
167
200
  } catch (err) {
168
- console.error("[ODBC Test] Connection failed:", err); // Ajout d'un log d'erreur
201
+ console.error("[ODBC Test] Connection failed:", err);
169
202
  res.status(500).send(err.message || "Erreur inconnue durant le test.");
170
203
  } finally {
171
204
  if (connection) {
172
- await connection.close();
205
+ await connection.close(); // Fermeture simple, pas besoin de timeout ici car c'est une op rapide.
173
206
  }
174
207
  }
175
208
  });
@@ -180,90 +213,34 @@ module.exports = function (RED) {
180
213
  this.config = config;
181
214
  this.poolNode = RED.nodes.getNode(this.config.connection);
182
215
  this.name = this.config.name;
183
-
184
- // Propriétés pour la logique de retry temporisée
185
216
  this.isAwaitingRetry = false;
186
217
  this.retryTimer = null;
187
218
 
188
- this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
189
- const queryContext = (() => {
190
- let s = "";
191
- if (query || params) {
192
- s += " {";
193
- if (query) s += `"query": '${query.substring(0, 100)}${query.length > 100 ? "..." : ""}'`;
194
- if (params) s += `, "params": '${JSON.stringify(params)}'`;
195
- s += "}";
196
- return s;
197
- }
198
- return "";
199
- })();
200
- let finalError;
201
- if (typeof error === "object" && error !== null && error.message) { finalError = error; }
202
- else if (typeof error === "string") { finalError = new Error(error); }
203
- else { finalError = new Error(defaultMessage); }
204
- finalError.message = `${finalError.message}${queryContext}`;
205
- if (query) finalError.query = query;
206
- if (params) finalError.params = params;
207
- return finalError;
208
- };
219
+ // NOUVEAU: Timeout fixe pour les opérations de fermeture de curseur (en ms)
220
+ this.cursorCloseOperationTimeout = 5000; // 5 secondes
209
221
 
210
- this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
211
- // ... (contenu de cette fonction inchangé par rapport à la dernière version)
212
- const result = await dbConnection.query(queryString, queryParams);
213
- if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
214
- const newMsg = RED.util.cloneMessage(msg);
215
- const otherParams = {};
216
- let actualDataRows = [];
217
- if (result !== null && typeof result === "object") {
218
- if (Array.isArray(result)) {
219
- actualDataRows = [...result];
220
- for (const [key, value] of Object.entries(result)) {
221
- if (isNaN(parseInt(key))) { otherParams[key] = value; }
222
- }
223
- } else {
224
- for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
225
- }
226
- }
227
- const columnMetadata = otherParams.columns;
228
- if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
229
- const sqlBitColumnNames = new Set();
230
- columnMetadata.forEach((col) => {
231
- if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") {
232
- sqlBitColumnNames.add(col.name);
233
- }
234
- });
235
- if (sqlBitColumnNames.size > 0) {
236
- actualDataRows.forEach((row) => {
237
- if (typeof row === "object" && row !== null) {
238
- for (const columnName of sqlBitColumnNames) {
239
- if (row.hasOwnProperty(columnName)) {
240
- const value = row[columnName];
241
- if (value === "1" || value === 1) { row[columnName] = true; }
242
- else if (value === "0" || value === 0) { row[columnName] = false; }
243
- }
244
- }
245
- }
246
- });
247
- }
248
- }
249
- objPath.set(newMsg, this.config.outputObj, actualDataRows);
250
- if (Object.keys(otherParams).length) { newMsg.odbc = otherParams; }
251
- return newMsg;
252
- };
222
+ this.enhanceError = (error, query, params, defaultMessage = "Query error") => { /* ... (inchangé) ... */ };
223
+ this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => { /* ... (inchangé) ... */ };
253
224
 
225
+ // MODIFIÉ: Ajout de timeout pour cursor.close()
254
226
  this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
255
- // ... (contenu de cette fonction inchangé par rapport à la dernière version avec le message de complétion final)
256
227
  const chunkSize = parseInt(this.config.streamChunkSize) || 1;
257
- const fetchSize = chunkSize > 100 ? 100 : chunkSize;
228
+ const fetchSize = chunkSize > 100 ? 100 : chunkSize;
258
229
  let cursor;
230
+
259
231
  try {
232
+ // dbConnection.query() utilisera le dbConnection.queryTimeout défini plus bas
260
233
  cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
261
234
  this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
235
+
262
236
  let rowCount = 0;
263
237
  let chunk = [];
238
+
264
239
  while (true) {
265
- const rows = await cursor.fetch();
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();
266
242
  if (!rows || rows.length === 0) { break; }
243
+
267
244
  for (const row of rows) {
268
245
  rowCount++;
269
246
  chunk.push(row);
@@ -276,51 +253,61 @@ module.exports = function (RED) {
276
253
  }
277
254
  }
278
255
  }
256
+
279
257
  if (chunk.length > 0) {
280
258
  const newMsg = RED.util.cloneMessage(msg);
281
259
  objPath.set(newMsg, this.config.outputObj, chunk);
282
260
  newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
283
261
  send(newMsg);
284
262
  }
263
+
285
264
  const finalMsg = RED.util.cloneMessage(msg);
286
265
  objPath.set(finalMsg, this.config.outputObj, []);
287
266
  finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
288
267
  send(finalMsg);
268
+
289
269
  this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
270
+
290
271
  } finally {
291
- if (cursor) await cursor.close();
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
+ }
292
285
  }
293
286
  };
294
287
 
288
+ // MODIFIÉ: Ajout de la définition du queryTimeout sur la connexion
295
289
  this.on("input", async (msg, send, done) => {
296
- // --- NOUVEAU : GESTION DE retryOnMsg ---
297
290
  if (this.isAwaitingRetry) {
298
- if (this.poolNode && this.poolNode.config.retryOnMsg === true) { // s'assurer que c'est bien un booléen true
291
+ if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
299
292
  this.log("New message received, overriding retry timer and attempting query now.");
300
293
  clearTimeout(this.retryTimer);
301
294
  this.retryTimer = null;
302
295
  this.isAwaitingRetry = false;
303
- // Laisser l'exécution se poursuivre
304
296
  } else {
305
297
  this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
306
- if (done) done(); // Terminer le traitement pour CE message
298
+ if (done) done();
307
299
  return;
308
300
  }
309
301
  }
310
- // S'assurer que les états de retry sont propres si on n'est pas dans un retry forcé par un nouveau message
311
302
  this.isAwaitingRetry = false;
312
- if(this.retryTimer) {
313
- clearTimeout(this.retryTimer);
314
- this.retryTimer = null;
315
- }
316
- // --- FIN DE LA GESTION DE retryOnMsg ---
303
+ if(this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = null; }
317
304
 
318
305
  if (!this.poolNode) {
319
306
  this.status({ fill: "red", shape: "ring", text: "No config node" });
320
307
  return done(new Error("ODBC Config node not properly configured."));
321
308
  }
322
-
323
- const execute = async (connection) => {
309
+
310
+ const executeWithConnection = async (connection) => {
324
311
  this.config.outputObj = this.config.outputObj || "payload";
325
312
  const querySourceType = this.config.querySourceType || 'msg';
326
313
  const querySource = this.config.querySource || 'query';
@@ -333,6 +320,19 @@ module.exports = function (RED) {
333
320
  if (!isPreparedStatement && query) {
334
321
  query = mustache.render(query, msg);
335
322
  }
323
+
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
+ }
332
+ } else {
333
+ connection.queryTimeout = 0; // Assurer le reset au défaut du driver (infini)
334
+ }
335
+
336
336
  this.status({ fill: "blue", shape: "dot", text: "executing..." });
337
337
  if (this.config.streaming) {
338
338
  await this.executeStreamQuery(connection, query, params, msg, send);
@@ -342,15 +342,15 @@ module.exports = function (RED) {
342
342
  send(newMsg);
343
343
  }
344
344
  };
345
-
345
+
346
346
  let connectionFromPool;
347
347
  let errorAfterInitialAttempts = null;
348
-
348
+
349
349
  try {
350
350
  this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
351
351
  connectionFromPool = await this.poolNode.connect();
352
- await execute(connectionFromPool);
353
- return done(); // Succès de la première tentative
352
+ await executeWithConnection(connectionFromPool);
353
+ return done();
354
354
  } catch (poolError) {
355
355
  this.warn(`First attempt with pooled connection failed: ${poolError.message}`);
356
356
  if (this.poolNode.config.retryFreshConnection) {
@@ -361,10 +361,10 @@ module.exports = function (RED) {
361
361
  const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
362
362
  freshConnection = await odbcModule.connect(freshConnectConfig);
363
363
  this.log("Fresh connection established for retry.");
364
- await execute(freshConnection);
364
+ await executeWithConnection(freshConnection);
365
365
  this.log("Query successful with fresh connection. Resetting pool.");
366
366
  await this.poolNode.resetPool();
367
- return done(); // Succès de la tentative avec connexion fraîche
367
+ return done();
368
368
  } catch (freshError) {
369
369
  errorAfterInitialAttempts = this.enhanceError(freshError, null, null, "Retry with fresh connection also failed");
370
370
  } finally {
@@ -376,61 +376,40 @@ module.exports = function (RED) {
376
376
  } finally {
377
377
  if (connectionFromPool) await connectionFromPool.close();
378
378
  }
379
-
380
- // --- NOUVEAU : GESTION DE retryDelay ---
379
+
381
380
  if (errorAfterInitialAttempts) {
382
- const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10); // S'assurer que c'est un nombre
383
-
381
+ const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
384
382
  if (retryDelaySeconds > 0) {
385
383
  this.warn(`Query failed. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorAfterInitialAttempts.message}`);
386
384
  this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
387
385
  this.isAwaitingRetry = true;
388
-
389
- // Important: `this.receive(msg)` ne peut pas être appelé directement dans un `setTimeout`
390
- // sans s'assurer que `this` est correctement lié. Utiliser une arrow function ou .bind(this).
391
- // De plus, `this.receive` est une méthode non documentée pour réinjecter un message.
392
- // La méthode standard pour retenter est que le nœud se renvoie le message à lui-même.
393
- // Pour cela, le `done()` de l'invocation actuelle doit être appelé.
394
-
395
386
  this.retryTimer = setTimeout(() => {
396
- this.isAwaitingRetry = false; // Prêt pour une nouvelle tentative
387
+ this.isAwaitingRetry = false;
397
388
  this.retryTimer = null;
398
389
  this.log(`Retry timer expired for message. Re-emitting for node ${this.id || this.name}.`);
399
- // Réinjecter le message pour une nouvelle tentative de traitement.
400
- // Le message original `msg` est utilisé.
401
390
  this.receive(msg);
402
391
  }, retryDelaySeconds * 1000);
403
-
404
- // L'invocation actuelle du message se termine ici, sans erreur si un retry est planifié.
405
- // L'erreur sera gérée par la prochaine invocation si elle échoue à nouveau.
406
392
  if (done) return done();
407
-
408
393
  } else {
409
- // Pas de retryDelay configuré ou il est à 0. C'est une défaillance définitive pour CE message.
410
394
  this.status({ fill: "red", shape: "ring", text: "query error" });
411
395
  if (done) return done(errorAfterInitialAttempts);
412
396
  }
413
397
  } else {
414
- // Normalement, on ne devrait pas arriver ici si done() a été appelé après un succès.
415
- // C'est une sécurité.
416
398
  if (done) return done();
417
399
  }
418
- // --- FIN DE LA GESTION DE retryDelay ---
419
400
  });
420
401
 
421
402
  this.on("close", (done) => {
422
- // --- NOUVEAU : Nettoyage du timer ---
423
403
  if (this.retryTimer) {
424
404
  clearTimeout(this.retryTimer);
425
405
  this.retryTimer = null;
426
406
  this.isAwaitingRetry = false;
427
407
  this.log("Cleared pending retry timer on node close/redeploy.");
428
408
  }
429
- // --- FIN DU NETTOYAGE ---
430
409
  this.status({});
431
410
  done();
432
411
  });
433
-
412
+
434
413
  if (this.poolNode) {
435
414
  this.status({ fill: "green", shape: "dot", text: "ready" });
436
415
  } else {
@@ -438,6 +417,6 @@ module.exports = function (RED) {
438
417
  this.warn("ODBC Config node not found or not deployed.");
439
418
  }
440
419
  }
441
-
420
+
442
421
  RED.nodes.registerType("odbc", odbc);
443
422
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "2.2.2",
3
+ "version": "2.3.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",