@bkmj/node-red-contrib-odbcmj 2.4.0 → 2.5.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 +21 -12
  2. package/odbc.html +44 -17
  3. package/odbc.js +284 -120
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Node-RED Contrib ODBC MJ
2
2
 
3
- A powerful and robust Node-RED node to connect to any ODBC data source. It features connection pooling, advanced retry logic, secure credential management, dynamic query sources, and result set streaming.
3
+ A powerful and robust Node-RED node to connect to any ODBC data source. It features connection pooling, advanced retry logic, secure credential management, dynamic query sources, result set streaming, and configurable timeouts.
4
4
 
5
5
  This node is a fork with significant enhancements to provide stability and advanced features for enterprise use cases.
6
6
 
@@ -11,10 +11,11 @@ This node is a fork with significant enhancements to provide stability and advan
11
11
  - **Secure Credential Storage**: Passwords are saved using Node-RED's built-in credential system.
12
12
  - **Connection Tester**: Instantly validate your connection settings from the configuration panel.
13
13
  - **Dynamic Inputs**: Source your SQL query and parameters from message properties, flow/global context, or environment variables.
14
- - **Advanced Retry Logic**: Automatically handles connection errors with configurable delays and retries to ensure flow resilience.
15
- - **Result Streaming**: Process queries with millions of rows without exhausting memory by streaming results as chunks.
14
+ - **Intelligent Error Handling & Retry Logic**: Differentiates between SQL errors and actual connection problems. Automatically handles connection errors with configurable delays and retries to ensure flow resilience.
15
+ - **Result Streaming**: Process queries with millions of rows without exhausting memory by streaming results as chunks, with a clear completion signal.
16
+ - **Configurable Timeouts**: Set timeouts for login, query execution, and internal close operations to prevent hangs.
17
+ - **Optional Fast Shutdown**: Provides a "fire-and-forget" option for pool closing to handle problematic drivers during Node-RED shutdown/deploy.
16
18
  - **Syntax Checker**: Optionally parse the SQL query to validate its structure.
17
- - **Query Timeout**: Configure a timeout for query execution to prevent indefinite hangs.
18
19
 
19
20
  ---
20
21
 
@@ -26,8 +27,6 @@ A configuration node that manages the connection to your database.
26
27
 
27
28
  #### Connection Modes
28
29
 
29
- Version 2.0 introduces two ways to configure your connection:
30
-
31
30
  ##### 1. Structured Fields Mode (Recommended)
32
31
 
33
32
  This is the easiest and most secure way to set up a connection for common databases.
@@ -55,15 +54,25 @@ A **Test Connection** button in the configuration panel allows you to instantly
55
54
  - **`Increment Pool Size`** `<number>` (optional): The number of connections to create when the pool is exhausted. Default: 5.
56
55
  - **`Max Pool Size`** `<number>` (optional): The maximum number of connections allowed in the pool. Default: 15.
57
56
  - **`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).
57
+ - **`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, converted to seconds).
59
58
  - **`Login Timeout`** `<number>` (optional): The number of seconds for an attempt to establish a new connection to succeed. Default: 5 seconds.
60
59
  - **`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.
61
60
 
62
61
  #### Error Handling & Retry
63
62
 
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.
63
+ The node attempts to distinguish between errors related to the SQL query itself (syntax, permissions) and actual connection problems. Connection retry mechanisms are primarily invoked for suspected connection issues.
64
+
65
+ - **`Retry with fresh connection`** `<boolean>` (optional): If a query fails and a basic connectivity test (e.g., `SELECT 1`) also suggests a problem with the current pooled connection, the node will try once more with a brand new, direct connection. If this second attempt (including its own successful basic connectivity test followed by the user's query) succeeds, the entire connection pool is reset to clear any potentially stale connections. Default: false.
66
+ - **`Retry Delay`** `<number>` (optional): If all immediate attempts fail and the problem is diagnosed as a *connection issue* (e.g., the `SELECT 1` test fails on both pooled and fresh connections), this sets a delay in seconds before another attempt to process the incoming message is made (using the pool again). A value of **0** disables this timed retry mechanism for connection issues. Default: 5.
67
+ - **`Retry on new message`** `<boolean>` (optional): If the node is waiting for a timed retry (due to `Retry Delay` for a connection issue), 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.
68
+
69
+ #### Shutdown Options
70
+
71
+ - **`Fast close (Fire-and-forget)`** `<boolean>` (optional):
72
+ - **Warning:** When checked, Node-RED will not wait for the connection pool to properly close during a deploy or shutdown.
73
+ - This option can prevent Node-RED from hanging if specific ODBC drivers have issues closing connections quickly.
74
+ - However, enabling this may result in orphaned connections on the database server, potentially consuming server resources.
75
+ - It is recommended to leave this unchecked unless you are experiencing hangs during Node-RED deploys or shutdowns related to this config node. Default: false.
67
76
 
68
77
  #### Advanced
69
78
 
@@ -98,8 +107,8 @@ To make the node highly flexible, the SQL query and its parameters can be source
98
107
 
99
108
  For queries that return a large number of rows, streaming prevents high memory usage.
100
109
 
101
- - **`Stream Results`** `<boolean>`: Enables or disables streaming mode. When enabled, the node will output multiple messages, one for each chunk of rows. Default: false.
102
- - **`Chunk Size`** `<number>`: The number of rows to include in each output message. A value of `1` means one message will be sent for every single row. Default: 1.
110
+ - **`Stream Results`** `<boolean>`: Enables or disables streaming mode. When enabled, the node will output multiple messages. Default: false.
111
+ - **`Chunk Size`** `<number>`: When streaming, this is the number of rows to include in each data message. A value of `1` means one message will be sent for every single row. Default: 1.
103
112
 
104
113
  ##### Streaming Output Format
105
114
 
package/odbc.html CHANGED
@@ -17,12 +17,13 @@
17
17
  incrementSize: { value: 5, validate: RED.validators.number(true) },
18
18
  maxSize: { value: 15, validate: RED.validators.number(true) },
19
19
  shrink: { value: true },
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
20
+ connectionTimeout: { value: 3, validate: RED.validators.number(true) },
21
+ loginTimeout: { value: 5, validate: RED.validators.number(true) },
22
+ queryTimeoutSeconds: { value: 0, validate: RED.validators.number(true) },
23
23
  retryFreshConnection: { value: false },
24
24
  retryDelay: { value: 5, validate: RED.validators.number(true) },
25
25
  retryOnMsg: { value: true },
26
+ fireAndForgetOnClose: { value: false }, // NOUVELLE OPTION
26
27
  syntaxtick: { value: false },
27
28
  syntax: { value: "mysql" },
28
29
  },
@@ -32,6 +33,11 @@
32
33
  oneditprepare: function () {
33
34
  var node = this;
34
35
 
36
+ // Initialiser tous les accordéons comme repliés par défaut
37
+ $('.form-section-content').hide();
38
+ $('.form-section-header').removeClass('expanded');
39
+
40
+
35
41
  $('.form-section-header').on('click', function() {
36
42
  $(this).toggleClass('expanded');
37
43
  $(this).next('.form-section-content').slideToggle();
@@ -80,9 +86,8 @@
80
86
  RED.notify("Le champ 'Server' est requis pour le test.", {type: "warning", timeout: 3000});
81
87
  return;
82
88
  }
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
89
+ if ($("#node-config-input-user").val().trim() !== "" && $("#node-config-input-password").val() === "") {
90
+ RED.notify("Test avec utilisateur et mot de passe vide. Si un mot de passe est habituellement requis, veuillez le (re)saisir.", {type: "info", timeout: 4500});
86
91
  }
87
92
  } else { // Mode 'string'
88
93
  var connStr = $("#node-config-input-connectionString").val().trim();
@@ -140,9 +145,10 @@
140
145
 
141
146
  <style>
142
147
  .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; }
148
+ .form-section-header i.fa-caret-right { transition: transform 0.2s ease-in-out; margin-right: 5px; }
144
149
  .form-section-header.expanded i.fa-caret-right { transform: rotate(90deg); }
145
- .form-section-content { padding-left: 20px; }
150
+ .form-section-content { padding-left: 20px; /* Sera géré par slideToggle, display:none initialement */ }
151
+ .form-tips { font-size: smaller; color: #777; display: block; margin-top: 2px; }
146
152
  </style>
147
153
 
148
154
  <script type="text/html" data-template-name="odbc config">
@@ -206,7 +212,7 @@
206
212
  <hr/>
207
213
 
208
214
  <div class="form-section-header"><h4><i class="fa fa-caret-right"></i> <i class="fa fa-sitemap"></i> Pool & Connection Options</h4></div>
209
- <div class="form-section-content" style="display: none;">
215
+ <div class="form-section-content">
210
216
  <div class="form-row">
211
217
  <label for="node-config-input-initialSize"><i class="fa fa-play"></i> Initial Pool Size</label>
212
218
  <input type="number" id="node-config-input-initialSize" placeholder="5" />
@@ -222,19 +228,19 @@
222
228
  <div class="form-row">
223
229
  <label for="node-config-input-shrink"><i class="fa fa-compress"></i> Shrink Pool</label>
224
230
  <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>
231
+ <span class="form-tips">Reduce pool to initial size when connections are returned.</span>
226
232
  </div>
227
233
  <div class="form-row">
228
234
  <label for="node-config-input-connectionTimeout"><i class="fa fa-clock-o"></i> Idle Timeout</label>
229
235
  <input type="number" id="node-config-input-connectionTimeout" placeholder="3" style="width: 80px;"/>
230
236
  <span style="margin-left: 5px;">seconds</span>
231
- <span class="form-tips" style="font-size: smaller;">For connections in pool.</span>
237
+ <span class="form-tips">For connections in pool.</span>
232
238
  </div>
233
239
  <div class="form-row">
234
240
  <label for="node-config-input-loginTimeout"><i class="fa fa-sign-in"></i> Login Timeout</label>
235
241
  <input type="number" id="node-config-input-loginTimeout" placeholder="5" style="width: 80px;" />
236
242
  <span style="margin-left: 5px;">seconds</span>
237
- <span class="form-tips" style="font-size: smaller;">For establishing new connections.</span>
243
+ <span class="form-tips">For establishing new connections.</span>
238
244
  </div>
239
245
  <div class="form-row">
240
246
  <label for="node-config-input-queryTimeoutSeconds"><i class="fa fa-hourglass-half"></i> Query Timeout</label>
@@ -244,11 +250,11 @@
244
250
  </div>
245
251
 
246
252
  <div class="form-section-header"><h4><i class="fa fa-caret-right"></i> <i class="fa fa-exclamation-triangle"></i> Error Handling & Retry</h4></div>
247
- <div class="form-section-content" style="display: none;">
253
+ <div class="form-section-content">
248
254
  <div class="form-row">
249
255
  <label for="node-config-input-retryFreshConnection" style="width: auto;"><i class="fa fa-refresh"></i> Retry with fresh connection</label>
250
256
  <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>
257
+ <span class="form-tips">If a pooled connection fails, try once with a new one.</span>
252
258
  </div>
253
259
  <div class="retry-options" style="padding-left: 20px;">
254
260
  <div class="form-row">
@@ -259,13 +265,27 @@
259
265
  <div class="form-row">
260
266
  <label for="node-config-input-retryOnMsg" style="width: auto;"><i class="fa fa-envelope-o"></i> Retry on new message</label>
261
267
  <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>
268
+ <span class="form-tips">If waiting, a new message triggers immediate retry.</span>
263
269
  </div>
264
270
  </div>
265
271
  </div>
266
272
 
273
+ <div class="form-section-header"><h4><i class="fa fa-caret-right"></i> <i class="fa fa-power-off"></i> Shutdown Options</h4></div>
274
+ <div class="form-section-content">
275
+ <div class="form-row">
276
+ <label for="node-config-input-fireAndForgetOnClose" style="width: auto;">
277
+ <i class="fa fa-rocket"></i> Fast close (Fire-and-forget)
278
+ </label>
279
+ <input type="checkbox" id="node-config-input-fireAndForgetOnClose" style="display: inline-block; width: auto; vertical-align: top;">
280
+ <span class="form-tips">
281
+ <b>Warning:</b> If checked, Node-RED will not wait for the pool to close during deploy/shutdown.
282
+ This can prevent hangs with problematic drivers but may leave orphaned connections on the DB server. Use with caution.
283
+ </span>
284
+ </div>
285
+ </div>
286
+
267
287
  <div class="form-section-header"><h4><i class="fa fa-caret-right"></i> <i class="fa fa-wrench"></i> Advanced</h4></div>
268
- <div class="form-section-content" style="display: none;">
288
+ <div class="form-section-content">
269
289
  <div class="form-row">
270
290
  <label for="node-config-input-syntaxtick" style="width: auto;"><i class="fa fa-check-square-o"></i> Syntax Checker</label>
271
291
  <input type="checkbox" id="node-config-input-syntaxtick" style="display: inline-block; width: auto; vertical-align: top;" />
@@ -341,7 +361,7 @@ A **Test Connection** button in the configuration panel allows you to instantly
341
361
  - **`Increment Pool Size`**: The number of connections to create when the pool is exhausted. Default: 5.
342
362
  - **`Max Pool Size`**: The maximum number of connections allowed in the pool. Default: 15.
343
363
  - **`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.
364
+ - **`Idle Timeout`**: 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).
345
365
  - **`Login Timeout`**: The number of seconds for an attempt to establish a new connection to succeed. Default: 5 seconds.
346
366
  - **`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.
347
367
 
@@ -350,6 +370,13 @@ A **Test Connection** button in the configuration panel allows you to instantly
350
370
  - **`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
371
  - **`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.
352
372
 
373
+ ### Shutdown Options
374
+ - **`Fast close (Fire-and-forget)`**:
375
+ - **Warning:** When checked, Node-RED will not wait for the connection pool to properly close during a deploy or shutdown.
376
+ - This option can prevent Node-RED from hanging if specific ODBC drivers have issues closing connections quickly.
377
+ - However, enabling this may result in orphaned connections on the database server, potentially consuming server resources.
378
+ - It is recommended to leave this unchecked unless you are experiencing hangs during Node-RED deploys or shutdowns related to this config node. Default: Unchecked.
379
+
353
380
  ### Advanced
354
381
  - **`syntaxChecker`**: If activated, the query string will be parsed and appended to the output message at `msg.parsedQuery`.
355
382
  - **`Syntax`**: The SQL dialect to use for the syntax checker (e.g., mysql, postgresql, etc.).
package/odbc.js CHANGED
@@ -15,7 +15,10 @@ 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
+
20
+ // NOUVEAU: Récupérer et s'assurer que l'option fireAndForgetOnClose est un booléen
21
+ this.config.fireAndForgetOnClose = config.fireAndForgetOnClose === true;
19
22
 
20
23
  this._buildConnectionString = function() {
21
24
  if (this.config.connectionMode === 'structured') {
@@ -33,8 +36,13 @@ module.exports = function (RED) {
33
36
  if(driver) parts.unshift(`DRIVER={${driver}}`);
34
37
  parts.push(`SERVER=${this.config.server}`);
35
38
  if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
36
- if (this.config.user) parts.push(`UID=${this.config.user}`);
37
- if (this.credentials && this.credentials.password) parts.push(`PWD=${this.credentials.password}`);
39
+
40
+ if (this.config.user) {
41
+ parts.push(`UID=${this.config.user}`);
42
+ if (this.credentials && typeof this.credentials.password === 'string') {
43
+ parts.push(`PWD=${this.credentials.password}`);
44
+ }
45
+ }
38
46
  return parts.join(';');
39
47
  } else {
40
48
  return this.config.connectionString || "";
@@ -110,33 +118,85 @@ module.exports = function (RED) {
110
118
  }
111
119
  };
112
120
 
121
+ // MODIFIÉ pour inclure l'option fireAndForgetOnClose
113
122
  this.on("close", async (removed, done) => {
114
- this.log("Closing ODBC config node. Attempting to close pool.");
123
+ const nodeName = this.name || this.id;
124
+ this.log(`[${nodeName}] Closing ODBC config node. Pool present: ${!!this.pool}. Fire-and-forget: ${this.config.fireAndForgetOnClose}`);
125
+
115
126
  if (this.pool) {
116
- try {
117
- await Promise.race([
118
- this.pool.close(),
119
- new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout on node close')), this.closeOperationTimeout))
120
- ]);
121
- this.log("Connection pool closed successfully on node close.");
122
- } catch (error) {
123
- this.error(`Error or timeout closing connection pool on node close: ${error.message}`, error);
124
- } finally {
125
- this.pool = null;
127
+ const currentPool = this.pool;
128
+ this.pool = null;
129
+ this.connecting = false;
130
+
131
+ const closePromise = Promise.race([
132
+ currentPool.close(),
133
+ new Promise((_, reject) =>
134
+ setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout)
135
+ )
136
+ ]);
137
+
138
+ if (this.config.fireAndForgetOnClose) {
139
+ this.log(`[${nodeName}] Initiating fire-and-forget pool close. Calling done() immediately.`);
140
+ done();
141
+
142
+ closePromise
143
+ .then(() => {
144
+ this.log(`[${nodeName}] Background fire-and-forget pool close for ${nodeName} completed successfully.`);
145
+ })
146
+ .catch(error => {
147
+ this.error(`[${nodeName}] Background fire-and-forget pool close for ${nodeName} failed or timed out: ${error.message}`, error);
148
+ });
149
+ } else {
150
+ this.log(`[${nodeName}] Awaiting pool close operation...`);
151
+ try {
152
+ await closePromise;
153
+ this.log(`[${nodeName}] Pool close operation completed successfully for ${nodeName}.`);
154
+ } catch (error) {
155
+ this.error(`[${nodeName}] Pool close operation for ${nodeName} failed or timed out: ${error.message}`, error);
156
+ }
157
+ this.log(`[${nodeName}] Calling done() after awaiting pool close for ${nodeName}.`);
158
+ done();
126
159
  }
160
+ } else {
161
+ this.log(`[${nodeName}] No pool to close. Calling done().`);
162
+ done();
127
163
  }
128
- done();
129
164
  });
130
165
  }
131
166
  RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
132
167
 
133
168
  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
169
  const tempConfig = req.body;
136
- const buildTestConnectionString = () => { /* ... */ }; // Définition interne
170
+ const buildTestConnectionString = () => {
171
+ if (tempConfig.connectionMode === 'structured') {
172
+ if (!tempConfig.dbType || !tempConfig.server) { throw new Error("En mode structuré, le type de base de données et le serveur sont requis."); }
173
+ let driver;
174
+ let parts = [];
175
+ switch (tempConfig.dbType) {
176
+ case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
177
+ case 'postgresql': driver = 'PostgreSQL Unicode'; break;
178
+ case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
179
+ default: driver = tempConfig.driver || ''; break;
180
+ }
181
+ if(driver) parts.unshift(`DRIVER={${driver}}`);
182
+ parts.push(`SERVER=${tempConfig.server}`);
183
+ if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
184
+ if (tempConfig.user) {
185
+ parts.push(`UID=${tempConfig.user}`);
186
+ if (typeof tempConfig.password === 'string') {
187
+ parts.push(`PWD=${tempConfig.password}`);
188
+ }
189
+ }
190
+ return parts.join(';');
191
+ } else {
192
+ let connStr = tempConfig.connectionString || "";
193
+ if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
194
+ return connStr;
195
+ }
196
+ };
137
197
  let connection;
138
198
  try {
139
- const testConnectionString = buildTestConnectionString(); // Utilise la définition interne
199
+ const testConnectionString = buildTestConnectionString();
140
200
  const connectionOptions = { connectionString: testConnectionString, loginTimeout: 10 };
141
201
  connection = await odbcModule.connect(connectionOptions);
142
202
  res.sendStatus(200);
@@ -156,12 +216,10 @@ module.exports = function (RED) {
156
216
  this.isAwaitingRetry = false;
157
217
  this.retryTimer = null;
158
218
  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
-
219
+ this.currentQueryForErrorContext = null;
220
+ this.currentParamsForErrorContext = null;
162
221
 
163
222
  this.enhanceError = (error, query, params, defaultMessage = "Query error") => {
164
- // Utilise this.currentQueryForErrorContext et this.currentParamsForErrorContext s'ils sont définis
165
223
  const q = query || this.currentQueryForErrorContext;
166
224
  const p = params || this.currentParamsForErrorContext;
167
225
  const queryContext = (() => {
@@ -185,27 +243,156 @@ module.exports = function (RED) {
185
243
  return finalError;
186
244
  };
187
245
 
188
- this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => { /* ... (inchangé) ... */ };
189
- this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => { /* ... (inchangé) ... */ };
246
+ this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
247
+ const result = await dbConnection.query(queryString, queryParams);
248
+ if (typeof result === "undefined") { throw new Error("Query returned undefined."); }
249
+ const newMsg = RED.util.cloneMessage(msg);
250
+ const outputProperty = this.config.outputObj || "payload";
251
+ const otherParams = {};
252
+ let actualDataRows = [];
253
+ if (result !== null && typeof result === "object") {
254
+ if (Array.isArray(result)) {
255
+ actualDataRows = result.map(row => (typeof row === 'object' && row !== null) ? { ...row } : row);
256
+ for (const [key, value] of Object.entries(result)) {
257
+ if (isNaN(parseInt(key))) { otherParams[key] = value; }
258
+ }
259
+ } else {
260
+ for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
261
+ }
262
+ }
263
+ const columnMetadata = otherParams.columns;
264
+ if (Array.isArray(columnMetadata) && Array.isArray(actualDataRows) && actualDataRows.length > 0) {
265
+ const sqlBitColumnNames = new Set();
266
+ columnMetadata.forEach(col => { if (col && typeof col.name === "string" && col.dataTypeName === "SQL_BIT") sqlBitColumnNames.add(col.name); });
267
+ if (sqlBitColumnNames.size > 0) {
268
+ actualDataRows.forEach(row => {
269
+ if (typeof row === "object" && row !== null) {
270
+ for (const columnName of sqlBitColumnNames) {
271
+ if (row.hasOwnProperty(columnName)) {
272
+ const value = row[columnName];
273
+ if (value === "1" || value === 1) row[columnName] = true;
274
+ else if (value === "0" || value === 0) row[columnName] = false;
275
+ }
276
+ }
277
+ }
278
+ });
279
+ }
280
+ }
281
+ objPath.set(newMsg, outputProperty, actualDataRows);
282
+ if (Object.keys(otherParams).length) newMsg.odbc = otherParams;
283
+ return newMsg;
284
+ };
285
+
286
+ this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
287
+ const chunkSize = parseInt(this.config.streamChunkSize) || 1;
288
+ const fetchSize = chunkSize > 100 ? 100 : chunkSize;
289
+ let cursor;
290
+ try {
291
+ cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
292
+ this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
293
+ let rowCount = 0;
294
+ let chunk = [];
295
+ while (true) {
296
+ const rows = await cursor.fetch();
297
+ if (!rows || rows.length === 0) break;
298
+ for (const row of rows) {
299
+ rowCount++;
300
+ const cleanRow = (typeof row === 'object' && row !== null) ? { ...row } : row;
301
+ chunk.push(cleanRow);
302
+ if (chunk.length >= chunkSize) {
303
+ const newMsg = RED.util.cloneMessage(msg);
304
+ objPath.set(newMsg, this.config.outputObj || "payload", chunk);
305
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
306
+ send(newMsg);
307
+ chunk = [];
308
+ }
309
+ }
310
+ }
311
+ if (chunk.length > 0) {
312
+ const newMsg = RED.util.cloneMessage(msg);
313
+ objPath.set(newMsg, this.config.outputObj || "payload", chunk);
314
+ newMsg.odbc_stream = { index: rowCount - chunk.length, count: chunk.length, complete: false };
315
+ send(newMsg);
316
+ }
317
+ const finalMsg = RED.util.cloneMessage(msg);
318
+ objPath.set(finalMsg, this.config.outputObj || "payload", []);
319
+ finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
320
+ send(finalMsg);
321
+ this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
322
+ } finally {
323
+ if (cursor) {
324
+ try {
325
+ await Promise.race([
326
+ cursor.close(),
327
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout))
328
+ ]);
329
+ } catch (cursorCloseError) { this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`); }
330
+ }
331
+ }
332
+ };
190
333
 
191
- // NOUVELLE fonction utilitaire
192
- async function testBasicConnectivity(connection, nodeInstance) {
334
+ this.testBasicConnectivity = async function(connection) {
193
335
  if (!connection || typeof connection.query !== 'function') {
194
- nodeInstance.warn("Test de connectivité basique : connexion invalide fournie.");
336
+ this.warn("Test de connectivité basique : connexion invalide fournie.");
195
337
  return false;
196
338
  }
339
+ let originalTimeout;
197
340
  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.");
341
+ originalTimeout = connection.queryTimeout;
342
+ connection.queryTimeout = 5;
343
+ await connection.query("SELECT 1");
344
+ this.log("Test de connectivité basique (SELECT 1) : Réussi.");
203
345
  return true;
204
346
  } catch (testError) {
205
- nodeInstance.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
347
+ this.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
206
348
  return false;
349
+ } finally {
350
+ if (typeof originalTimeout !== 'undefined' && connection && typeof connection.query === 'function') {
351
+ try { connection.queryTimeout = originalTimeout; }
352
+ catch(e) { this.warn("Impossible de restaurer le queryTimeout original après le test de connectivité.")}
353
+ }
207
354
  }
208
- }
355
+ };
356
+
357
+ this.getRenderedQueryAndParams = async function(msg) {
358
+ const querySourceType = this.config.querySourceType || 'msg';
359
+ const querySource = this.config.querySource || 'query';
360
+ const paramsSourceType = this.config.paramsSourceType || 'msg';
361
+ const paramsSource = this.config.paramsSource || 'parameters';
362
+
363
+ this.currentParamsForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
364
+ this.currentQueryForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
365
+
366
+ if (!this.currentQueryForErrorContext) {
367
+ 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)'));
368
+ }
369
+
370
+ let finalQuery = this.currentQueryForErrorContext;
371
+ const isPreparedStatement = this.currentParamsForErrorContext || (finalQuery && finalQuery.includes("?"));
372
+ if (!isPreparedStatement && finalQuery) {
373
+ finalQuery = mustache.render(finalQuery, msg);
374
+ }
375
+ return { query: finalQuery, params: this.currentParamsForErrorContext };
376
+ };
377
+
378
+ this.executeUserQuery = async function(connection, query, params, msg, send) {
379
+ const configuredTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
380
+ if (configuredTimeout > 0) {
381
+ try { connection.queryTimeout = configuredTimeout; }
382
+ catch (e) { this.warn(`Could not set queryTimeout on connection: ${e.message}`); }
383
+ } else {
384
+ connection.queryTimeout = 0;
385
+ }
386
+
387
+ this.status({ fill: "blue", shape: "dot", text: "executing..." });
388
+ if (this.config.streaming) {
389
+ await this.executeStreamQuery(connection, query, params, msg, send);
390
+ } else {
391
+ const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
392
+ this.status({ fill: "green", shape: "dot", text: "success" });
393
+ send(newMsg);
394
+ }
395
+ };
209
396
 
210
397
  this.on("input", async (msg, send, done) => {
211
398
  this.currentQueryForErrorContext = null;
@@ -225,75 +412,50 @@ module.exports = function (RED) {
225
412
 
226
413
  if (!this.poolNode) {
227
414
  this.status({ fill: "red", shape: "ring", text: "No config node" });
228
- return done(new Error("ODBC Config node not properly configured."));
415
+ return done(this.enhanceError(new Error("ODBC Config node not properly configured.")));
229
416
  }
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
417
 
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
-
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)
418
+ let queryToExecute;
419
+ let paramsToExecute;
420
+ try {
421
+ const queryData = await this.getRenderedQueryAndParams(msg);
422
+ queryToExecute = queryData.query;
423
+ paramsToExecute = queryData.params;
424
+ } catch (inputValidationError) {
425
+ this.status({ fill: "red", shape: "ring", text: "Input Error" });
426
+ return done(this.enhanceError(inputValidationError));
427
+ }
428
+
429
+ let activeConnection = null;
430
+ let errorForUser = null;
271
431
  let shouldProceedToTimedRetry = false;
272
- let errorForTimedRetry = null;
273
-
274
- try { // Tentative Principale (avec connexion du pool)
275
- const { query, params } = await getRenderedQueryAndParams();
276
-
432
+
433
+ try {
277
434
  this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
278
435
  activeConnection = await this.poolNode.connect();
279
- await executeUserQuery(activeConnection, query, params);
436
+ await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
280
437
 
281
- if (activeConnection) { await activeConnection.close(); activeConnection = null; }
282
- return done();
438
+ done();
439
+
440
+ if (activeConnection) {
441
+ try { await activeConnection.close(); } catch(e) { this.warn("Error closing pool connection after success: " + e.message); }
442
+ activeConnection = null;
443
+ }
444
+ return;
283
445
 
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);}
446
+ } catch (initialDbError) {
447
+ this.warn(`Initial DB attempt failed: ${initialDbError.message}`);
448
+ if (activeConnection) {
449
+ const connStillGood = await this.testBasicConnectivity(activeConnection);
450
+ try { await activeConnection.close(); activeConnection = null; }
451
+ catch(e){ this.warn("Error closing pool conn after initial error: "+e.message); activeConnection = null; }
289
452
 
290
- if (connStillGood) { // La connexion est bonne, l'erreur vient de la requête utilisateur
453
+ if (connStillGood) {
291
454
  this.status({ fill: "red", shape: "ring", text: "SQL error" });
292
- return done(this.enhanceError(initialError, this.currentQueryForErrorContext, this.currentParamsForErrorContext, "SQL Query Error"));
455
+ return done(this.enhanceError(initialDbError));
293
456
  }
294
457
  }
295
- // Si on arrive ici, la connexion poolée a eu un problème (soit pour se connecter, soit SELECT 1 a échoué)
296
-
458
+
297
459
  if (this.poolNode.config.retryFreshConnection) {
298
460
  this.warn("Attempting retry with a fresh connection.");
299
461
  this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
@@ -302,47 +464,50 @@ module.exports = function (RED) {
302
464
  activeConnection = await odbcModule.connect(freshConnectConfig);
303
465
  this.log("Fresh connection established.");
304
466
 
305
- const freshConnGood = await testBasicConnectivity(activeConnection, this);
467
+ const freshConnGood = await this.testBasicConnectivity(activeConnection);
306
468
  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");
469
+ errorForUser = this.enhanceError(new Error("Basic connectivity (SELECT 1) failed on fresh connection."), null, null, "Fresh Connection Test Failed");
309
470
  shouldProceedToTimedRetry = true;
310
- throw errorForTimedRetry; // Va au catch externe de ce bloc try-fresh
471
+ throw errorForUser;
311
472
  }
312
473
 
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);
474
+ await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
316
475
 
317
476
  this.log("Query successful with fresh connection. Resetting pool.");
477
+ done();
478
+
318
479
  await this.poolNode.resetPool();
319
- if (activeConnection) { await activeConnection.close(); activeConnection = null; }
320
- return done(); // Succès !
480
+ if (activeConnection) {
481
+ try { await activeConnection.close(); } catch(e) { this.warn("Error closing fresh connection after success: " + e.message); }
482
+ activeConnection = null;
483
+ }
484
+ return;
321
485
 
322
486
  } 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
487
  if (activeConnection) { try { await activeConnection.close(); activeConnection = null; } catch(e){this.warn("Error closing fresh conn after error: "+e.message);} }
326
488
 
327
- if (shouldProceedToTimedRetry) { // Signifie que SELECT 1 sur la connexion fraîche a échoué
328
- // errorForTimedRetry est déjà setté
489
+ if (shouldProceedToTimedRetry) {
490
+ // errorForUser est déjà l'erreur de test de connectivité et sera utilisé ci-dessous
329
491
  } else {
330
- // SELECT 1 sur connexion fraîche a réussi, mais la requête utilisateur a échoué. C'est une erreur SQL.
331
492
  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)"));
493
+ return done(this.enhanceError(freshErrorOrConnectivityFail));
333
494
  }
334
495
  }
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");
496
+ } else {
497
+ errorForUser = this.enhanceError(initialDbError, null, null, "Connection Error (no fresh retry)");
337
498
  shouldProceedToTimedRetry = true;
338
499
  }
339
500
  }
340
501
 
341
- // Logique de Retry Temporisé
342
- if (shouldProceedToTimedRetry && errorForTimedRetry) {
502
+ if (activeConnection) {
503
+ try { await activeConnection.close(); } catch(e) { this.warn("Final cleanup: Error closing activeConnection: " + e.message); }
504
+ activeConnection = null;
505
+ }
506
+
507
+ if (shouldProceedToTimedRetry && errorForUser) {
343
508
  const retryDelaySeconds = parseInt(this.poolNode.config.retryDelay, 10);
344
509
  if (retryDelaySeconds > 0) {
345
- this.warn(`Connection issue suspected. Scheduling retry in ${retryDelaySeconds} seconds. Error: ${errorForTimedRetry.message}`);
510
+ this.warn(`Connection issue. Scheduling retry in ${retryDelaySeconds}s. Error: ${errorForUser.message}`);
346
511
  this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
347
512
  this.isAwaitingRetry = true;
348
513
  this.retryTimer = setTimeout(() => {
@@ -350,17 +515,16 @@ module.exports = function (RED) {
350
515
  this.log(`Retry timer expired. Re-emitting message for node ${this.id || this.name}.`);
351
516
  this.receive(msg);
352
517
  }, retryDelaySeconds * 1000);
353
- return done(); // Termine l'invocation actuelle du message
354
- } else { // Pas de délai de retry, ou délai à 0
518
+ return done();
519
+ } else {
355
520
  this.status({ fill: "red", shape: "ring", text: "Connection Error" });
356
- return done(errorForTimedRetry);
521
+ return done(errorForUser);
357
522
  }
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
523
+ } else if (errorForUser) {
524
+ this.status({ fill: "red", shape: "ring", text: "Error (No Timed Retry)" });
525
+ return done(errorForUser);
361
526
  } 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().");
527
+ this.log("[ODBC Node] DEBUG: Reached end of on('input') path. Calling done().");
364
528
  return done();
365
529
  }
366
530
  });
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.5.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",