@bkmj/node-red-contrib-odbcmj 2.4.1 → 2.6.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.
- package/README.md +21 -12
- package/odbc.html +44 -17
- package/odbc.js +321 -295
- 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,
|
|
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
|
-
- **
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
- **`Retry
|
|
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
|
|
102
|
-
- **`Chunk Size`** `<number>`:
|
|
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) },
|
|
21
|
-
loginTimeout: { value: 5, validate: RED.validators.number(true) },
|
|
22
|
-
queryTimeoutSeconds: { value: 0, validate: RED.validators.number(true) },
|
|
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-
|
|
84
|
-
RED.notify("
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
@@ -3,7 +3,9 @@ module.exports = function (RED) {
|
|
|
3
3
|
const mustache = require("mustache");
|
|
4
4
|
const objPath = require("object-path");
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// =========================================================================
|
|
7
|
+
// CONFIG NODE (Gère le Pool de Connexion)
|
|
8
|
+
// =========================================================================
|
|
7
9
|
function poolConfig(config) {
|
|
8
10
|
RED.nodes.createNode(this, config);
|
|
9
11
|
this.config = config;
|
|
@@ -11,11 +13,17 @@ module.exports = function (RED) {
|
|
|
11
13
|
this.connecting = false;
|
|
12
14
|
this.credentials = RED.nodes.getCredentials(this.id);
|
|
13
15
|
|
|
16
|
+
// Validation des timeouts
|
|
14
17
|
this.config.queryTimeoutSeconds = parseInt(config.queryTimeoutSeconds, 10);
|
|
15
18
|
if (isNaN(this.config.queryTimeoutSeconds) || this.config.queryTimeoutSeconds < 0) {
|
|
16
|
-
this.config.queryTimeoutSeconds = 0;
|
|
19
|
+
this.config.queryTimeoutSeconds = 0; // 0 = infini / défaut driver
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
|
|
22
|
+
// Timeout de sécurité interne pour forcer la fermeture si le driver pend
|
|
23
|
+
this.closeOperationTimeout = 10000;
|
|
24
|
+
|
|
25
|
+
// Option Fire-and-Forget
|
|
26
|
+
this.config.fireAndForgetOnClose = config.fireAndForgetOnClose === true;
|
|
19
27
|
|
|
20
28
|
this._buildConnectionString = function() {
|
|
21
29
|
if (this.config.connectionMode === 'structured') {
|
|
@@ -24,17 +32,24 @@ module.exports = function (RED) {
|
|
|
24
32
|
}
|
|
25
33
|
let driver;
|
|
26
34
|
let parts = [];
|
|
35
|
+
// Mapping des drivers communs
|
|
27
36
|
switch (this.config.dbType) {
|
|
28
37
|
case 'sqlserver': driver = 'ODBC Driver 17 for SQL Server'; break;
|
|
29
38
|
case 'postgresql': driver = 'PostgreSQL Unicode'; break;
|
|
30
39
|
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
31
40
|
default: driver = this.config.driver || ''; break;
|
|
32
41
|
}
|
|
33
|
-
|
|
42
|
+
|
|
43
|
+
if(driver) parts.push(`DRIVER={${driver}}`);
|
|
34
44
|
parts.push(`SERVER=${this.config.server}`);
|
|
35
45
|
if (this.config.database) parts.push(`DATABASE=${this.config.database}`);
|
|
36
|
-
|
|
37
|
-
if (this.
|
|
46
|
+
|
|
47
|
+
if (this.config.user) {
|
|
48
|
+
parts.push(`UID=${this.config.user}`);
|
|
49
|
+
if (this.credentials && typeof this.credentials.password === 'string') {
|
|
50
|
+
parts.push(`PWD=${this.credentials.password}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
38
53
|
return parts.join(';');
|
|
39
54
|
} else {
|
|
40
55
|
return this.config.connectionString || "";
|
|
@@ -48,20 +63,24 @@ module.exports = function (RED) {
|
|
|
48
63
|
try {
|
|
49
64
|
const finalConnectionString = this._buildConnectionString();
|
|
50
65
|
if (!finalConnectionString) throw new Error("La chaîne de connexion est vide.");
|
|
66
|
+
|
|
51
67
|
const poolParams = {
|
|
52
68
|
connectionString: finalConnectionString,
|
|
53
|
-
initialSize: parseInt(this.config.initialSize, 10) ||
|
|
54
|
-
incrementSize: parseInt(this.config.incrementSize, 10) ||
|
|
55
|
-
maxSize: parseInt(this.config.maxSize, 10) ||
|
|
69
|
+
initialSize: parseInt(this.config.initialSize, 10) || 5,
|
|
70
|
+
incrementSize: parseInt(this.config.incrementSize, 10) || 5,
|
|
71
|
+
maxSize: parseInt(this.config.maxSize, 10) || 15,
|
|
56
72
|
shrink: typeof this.config.shrink === 'boolean' ? this.config.shrink : true,
|
|
57
|
-
connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) ||
|
|
58
|
-
loginTimeout: parseInt(this.config.loginTimeout, 10) ||
|
|
73
|
+
connectionTimeout: (parseInt(this.config.connectionTimeout, 10) * 1000) || 3000,
|
|
74
|
+
loginTimeout: parseInt(this.config.loginTimeout, 10) || 5
|
|
59
75
|
};
|
|
76
|
+
|
|
77
|
+
// Nettoyage des undefined
|
|
60
78
|
Object.keys(poolParams).forEach(key => poolParams[key] === undefined && delete poolParams[key]);
|
|
79
|
+
|
|
61
80
|
this.pool = await odbcModule.pool(poolParams);
|
|
62
81
|
this.connecting = false;
|
|
63
82
|
this.status({ fill: "green", shape: "dot", text: "Pool ready" });
|
|
64
|
-
this.log(
|
|
83
|
+
this.log(`Pool initialized for ${this.name || this.id}`);
|
|
65
84
|
} catch (error) {
|
|
66
85
|
this.connecting = false;
|
|
67
86
|
this.error(`Error creating connection pool: ${error.message}`, error);
|
|
@@ -69,15 +88,16 @@ module.exports = function (RED) {
|
|
|
69
88
|
throw error;
|
|
70
89
|
}
|
|
71
90
|
}
|
|
91
|
+
// Récupération d'une connexion du pool
|
|
72
92
|
try {
|
|
73
93
|
return await this.pool.connect();
|
|
74
94
|
} catch (poolConnectError) {
|
|
75
|
-
this.error(`Error connecting to pool: ${poolConnectError.message}`, poolConnectError);
|
|
76
95
|
this.status({ fill: "red", shape: "ring", text: "Pool connect err" });
|
|
77
96
|
throw poolConnectError;
|
|
78
97
|
}
|
|
79
98
|
};
|
|
80
99
|
|
|
100
|
+
// Configuration pour une connexion "fraîche" (hors pool)
|
|
81
101
|
this.getFreshConnectionConfig = function() {
|
|
82
102
|
return {
|
|
83
103
|
connectionString: this._buildConnectionString(),
|
|
@@ -88,53 +108,74 @@ module.exports = function (RED) {
|
|
|
88
108
|
|
|
89
109
|
this.resetPool = async () => {
|
|
90
110
|
if (this.pool) {
|
|
91
|
-
this.log("Resetting connection pool.");
|
|
111
|
+
this.log("Resetting connection pool requested.");
|
|
92
112
|
this.status({ fill: "yellow", shape: "ring", text: "Resetting pool..." });
|
|
93
|
-
|
|
113
|
+
|
|
114
|
+
const oldPool = this.pool;
|
|
115
|
+
this.pool = null; // On détache immédiatement pour forcer la recréation au prochain appel
|
|
116
|
+
this.connecting = false;
|
|
117
|
+
|
|
94
118
|
try {
|
|
95
119
|
await Promise.race([
|
|
96
|
-
|
|
120
|
+
oldPool.close(),
|
|
97
121
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout))
|
|
98
122
|
]);
|
|
99
|
-
this.log("
|
|
100
|
-
closedSuccessfully = true;
|
|
123
|
+
this.log("Old connection pool closed successfully.");
|
|
101
124
|
} catch (closeError) {
|
|
102
|
-
this.
|
|
125
|
+
this.warn(`Error closing old pool during reset: ${closeError.message}`);
|
|
103
126
|
} finally {
|
|
104
|
-
|
|
105
|
-
this.connecting = false;
|
|
106
|
-
this.status({ fill: closedSuccessfully ? "grey" : "red", shape: "ring", text: closedSuccessfully ? "Pool reset" : "Pool reset failed" });
|
|
127
|
+
this.status({ fill: "grey", shape: "ring", text: "Pool reset" });
|
|
107
128
|
}
|
|
108
|
-
} else {
|
|
109
|
-
this.log("Pool reset requested, but no active pool to reset.");
|
|
110
129
|
}
|
|
111
130
|
};
|
|
112
131
|
|
|
113
132
|
this.on("close", async (removed, done) => {
|
|
114
|
-
|
|
133
|
+
const nodeName = this.name || this.id;
|
|
134
|
+
|
|
115
135
|
if (this.pool) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
const currentPool = this.pool;
|
|
137
|
+
this.pool = null;
|
|
138
|
+
this.connecting = false;
|
|
139
|
+
|
|
140
|
+
const closePromise = Promise.race([
|
|
141
|
+
currentPool.close(),
|
|
142
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Pool close timeout')), this.closeOperationTimeout))
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
if (this.config.fireAndForgetOnClose) {
|
|
146
|
+
// FAST CLOSE: On libère Node-RED immédiatement
|
|
147
|
+
this.log(`[${nodeName}] Fire-and-forget close initiated.`);
|
|
148
|
+
done();
|
|
149
|
+
// On continue en arrière-plan (best effort)
|
|
150
|
+
closePromise.catch(e => this.warn(`[${nodeName}] Background pool close error: ${e.message}`));
|
|
151
|
+
} else {
|
|
152
|
+
// NORMAL CLOSE: On attend la fermeture propre
|
|
153
|
+
this.log(`[${nodeName}] Closing pool...`);
|
|
154
|
+
try {
|
|
155
|
+
await closePromise;
|
|
156
|
+
this.log(`[${nodeName}] Pool closed.`);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
this.error(`[${nodeName}] Error closing pool: ${e.message}`);
|
|
159
|
+
}
|
|
160
|
+
done();
|
|
126
161
|
}
|
|
162
|
+
} else {
|
|
163
|
+
done();
|
|
127
164
|
}
|
|
128
|
-
done();
|
|
129
165
|
});
|
|
130
166
|
}
|
|
131
|
-
RED.nodes.registerType("odbc config", poolConfig, { credentials: { password: { type: "password" } } });
|
|
132
167
|
|
|
168
|
+
RED.nodes.registerType("odbc config", poolConfig, {
|
|
169
|
+
credentials: { password: { type: "password" } }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Endpoint pour le bouton "Test Connection"
|
|
133
173
|
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
134
174
|
const tempConfig = req.body;
|
|
175
|
+
// Reconstitution locale de la chaîne de connexion pour le test
|
|
135
176
|
const buildTestConnectionString = () => {
|
|
136
177
|
if (tempConfig.connectionMode === 'structured') {
|
|
137
|
-
if (!tempConfig.dbType || !tempConfig.server)
|
|
178
|
+
if (!tempConfig.dbType || !tempConfig.server) throw new Error("Missing params.");
|
|
138
179
|
let driver;
|
|
139
180
|
let parts = [];
|
|
140
181
|
switch (tempConfig.dbType) {
|
|
@@ -143,373 +184,358 @@ module.exports = function (RED) {
|
|
|
143
184
|
case 'mysql': driver = 'MySQL ODBC 8.0 Unicode Driver'; break;
|
|
144
185
|
default: driver = tempConfig.driver || ''; break;
|
|
145
186
|
}
|
|
146
|
-
if(driver) parts.
|
|
187
|
+
if(driver) parts.push(`DRIVER={${driver}}`);
|
|
147
188
|
parts.push(`SERVER=${tempConfig.server}`);
|
|
148
189
|
if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
|
|
149
|
-
if (tempConfig.user)
|
|
150
|
-
|
|
190
|
+
if (tempConfig.user) {
|
|
191
|
+
parts.push(`UID=${tempConfig.user}`);
|
|
192
|
+
if (typeof tempConfig.password === 'string') parts.push(`PWD=${tempConfig.password}`);
|
|
193
|
+
}
|
|
151
194
|
return parts.join(';');
|
|
152
195
|
} else {
|
|
153
|
-
|
|
154
|
-
if (!connStr) { throw new Error("La chaîne de connexion ne peut pas être vide."); }
|
|
155
|
-
return connStr;
|
|
196
|
+
return tempConfig.connectionString || "";
|
|
156
197
|
}
|
|
157
198
|
};
|
|
199
|
+
|
|
158
200
|
let connection;
|
|
159
201
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
const cs = buildTestConnectionString();
|
|
203
|
+
if (!cs) throw new Error("Connection string empty");
|
|
204
|
+
// Connexion directe (sans pool) pour le test
|
|
205
|
+
connection = await odbcModule.connect({
|
|
206
|
+
connectionString: cs,
|
|
207
|
+
loginTimeout: 10
|
|
208
|
+
});
|
|
209
|
+
await connection.query("SELECT 1"); // Test SQL simple
|
|
163
210
|
res.sendStatus(200);
|
|
164
211
|
} catch (err) {
|
|
165
|
-
res.status(500).send(err.message
|
|
212
|
+
res.status(500).send(err.message);
|
|
166
213
|
} finally {
|
|
167
|
-
if (connection)
|
|
214
|
+
if (connection) {
|
|
215
|
+
try { await connection.close(); } catch(e){}
|
|
216
|
+
}
|
|
168
217
|
}
|
|
169
218
|
});
|
|
170
219
|
|
|
171
|
-
//
|
|
220
|
+
// =========================================================================
|
|
221
|
+
// QUERY NODE (Le noeud fonctionnel)
|
|
222
|
+
// =========================================================================
|
|
172
223
|
function odbc(config) {
|
|
173
224
|
RED.nodes.createNode(this, config);
|
|
174
225
|
this.config = config;
|
|
175
226
|
this.poolNode = RED.nodes.getNode(this.config.connection);
|
|
176
227
|
this.name = this.config.name;
|
|
228
|
+
|
|
229
|
+
// Gestion des retry
|
|
177
230
|
this.isAwaitingRetry = false;
|
|
178
231
|
this.retryTimer = null;
|
|
179
|
-
|
|
232
|
+
|
|
233
|
+
// Contextes d'erreur
|
|
180
234
|
this.currentQueryForErrorContext = null;
|
|
181
235
|
this.currentParamsForErrorContext = null;
|
|
182
236
|
|
|
183
|
-
|
|
237
|
+
// Helper pour enrichir les erreurs
|
|
238
|
+
this.enhanceError = (error, query, params, contextMsg) => {
|
|
184
239
|
const q = query || this.currentQueryForErrorContext;
|
|
185
240
|
const p = params || this.currentParamsForErrorContext;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (q || p) {
|
|
189
|
-
s += " {";
|
|
190
|
-
if (q) s += `"query": '${String(q).substring(0, 100)}${String(q).length > 100 ? "..." : ""}'`;
|
|
191
|
-
if (p) s += `, "params": '${JSON.stringify(p)}'`;
|
|
192
|
-
s += "}";
|
|
193
|
-
return s;
|
|
194
|
-
}
|
|
195
|
-
return "";
|
|
196
|
-
})();
|
|
241
|
+
let msg = contextMsg ? `[${contextMsg}] ` : "";
|
|
242
|
+
|
|
197
243
|
let finalError;
|
|
198
|
-
if (typeof error === "object" && error !== null && error.message) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
244
|
+
if (typeof error === "object" && error !== null && error.message) {
|
|
245
|
+
finalError = error;
|
|
246
|
+
finalError.message = msg + finalError.message;
|
|
247
|
+
} else {
|
|
248
|
+
finalError = new Error(msg + (error || "Unknown error"));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (q) finalError.query = String(q).substring(0, 500);
|
|
204
252
|
return finalError;
|
|
205
253
|
};
|
|
206
254
|
|
|
255
|
+
// Exécution standard (Non-streaming)
|
|
207
256
|
this.executeQueryAndProcess = async (dbConnection, queryString, queryParams, msg) => {
|
|
208
257
|
const result = await dbConnection.query(queryString, queryParams);
|
|
209
|
-
|
|
258
|
+
|
|
210
259
|
const newMsg = RED.util.cloneMessage(msg);
|
|
211
260
|
const outputProperty = this.config.outputObj || "payload";
|
|
212
261
|
const otherParams = {};
|
|
213
262
|
let actualDataRows = [];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
263
|
+
|
|
264
|
+
// Traitement du Result Set
|
|
265
|
+
if (Array.isArray(result)) {
|
|
266
|
+
// Conversion des objets Row en objets JS purs
|
|
267
|
+
actualDataRows = result.map(row => (typeof row === 'object' && row !== null) ? { ...row } : row);
|
|
268
|
+
// Extraction des métadonnées (Statement, count, columns, etc.)
|
|
269
|
+
for (const [key, value] of Object.entries(result)) {
|
|
270
|
+
if (isNaN(parseInt(key))) { otherParams[key] = value; }
|
|
222
271
|
}
|
|
272
|
+
} else if (typeof result === 'object' && result !== null) {
|
|
273
|
+
// Cas rares ou updates sans retour
|
|
274
|
+
for (const [key, value] of Object.entries(result)) { otherParams[key] = value; }
|
|
223
275
|
}
|
|
276
|
+
|
|
277
|
+
// Gestion spécifique SQL_BIT -> Boolean
|
|
224
278
|
const columnMetadata = otherParams.columns;
|
|
225
|
-
if (Array.isArray(columnMetadata) &&
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
279
|
+
if (Array.isArray(columnMetadata) && actualDataRows.length > 0) {
|
|
280
|
+
const sqlBitCols = columnMetadata
|
|
281
|
+
.filter(c => c.dataTypeName === "SQL_BIT")
|
|
282
|
+
.map(c => c.name);
|
|
283
|
+
|
|
284
|
+
if (sqlBitCols.length > 0) {
|
|
229
285
|
actualDataRows.forEach(row => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (value === "1" || value === 1) row[columnName] = true;
|
|
235
|
-
else if (value === "0" || value === 0) row[columnName] = false;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
286
|
+
sqlBitCols.forEach(col => {
|
|
287
|
+
if (row[col] == 1) row[col] = true;
|
|
288
|
+
else if (row[col] == 0) row[col] = false;
|
|
289
|
+
});
|
|
239
290
|
});
|
|
240
291
|
}
|
|
241
292
|
}
|
|
293
|
+
|
|
242
294
|
objPath.set(newMsg, outputProperty, actualDataRows);
|
|
295
|
+
// Ajout des métadonnées (optionnel, dans msg.odbc)
|
|
243
296
|
if (Object.keys(otherParams).length) newMsg.odbc = otherParams;
|
|
297
|
+
|
|
244
298
|
return newMsg;
|
|
245
299
|
};
|
|
246
|
-
|
|
300
|
+
|
|
301
|
+
// Exécution Streaming
|
|
247
302
|
this.executeStreamQuery = async (dbConnection, queryString, queryParams, msg, send) => {
|
|
248
303
|
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
249
|
-
|
|
304
|
+
// FetchSize un peu plus grand pour l'efficacité réseau
|
|
305
|
+
const fetchSize = chunkSize < 100 ? 100 : chunkSize;
|
|
306
|
+
|
|
307
|
+
// Préparation d'un squelette de message pour éviter de tout cloner en boucle
|
|
308
|
+
const skeletonMsg = RED.util.cloneMessage(msg);
|
|
309
|
+
const outProp = this.config.outputObj || "payload";
|
|
310
|
+
// On vide le payload du squelette pour ne pas le traîner
|
|
311
|
+
objPath.set(skeletonMsg, outProp, null);
|
|
312
|
+
|
|
250
313
|
let cursor;
|
|
251
314
|
try {
|
|
252
315
|
cursor = await dbConnection.query(queryString, queryParams, { cursor: true, fetchSize: fetchSize });
|
|
253
|
-
this.status({ fill: "blue", shape: "dot", text: "streaming
|
|
316
|
+
this.status({ fill: "blue", shape: "dot", text: "streaming..." });
|
|
317
|
+
|
|
254
318
|
let rowCount = 0;
|
|
255
|
-
let
|
|
319
|
+
let buffer = [];
|
|
320
|
+
|
|
256
321
|
while (true) {
|
|
257
|
-
const rows = await cursor.fetch();
|
|
322
|
+
const rows = await cursor.fetch();
|
|
258
323
|
if (!rows || rows.length === 0) break;
|
|
324
|
+
|
|
259
325
|
for (const row of rows) {
|
|
260
326
|
rowCount++;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
327
|
+
buffer.push({ ...row }); // Copie propre de la ligne
|
|
328
|
+
|
|
329
|
+
if (buffer.length >= chunkSize) {
|
|
330
|
+
// Envoi du chunk
|
|
331
|
+
const chunkMsg = RED.util.cloneMessage(skeletonMsg);
|
|
332
|
+
objPath.set(chunkMsg, outProp, buffer);
|
|
333
|
+
chunkMsg.odbc_stream = { index: rowCount - buffer.length, count: buffer.length, complete: false };
|
|
334
|
+
send(chunkMsg);
|
|
335
|
+
buffer = [];
|
|
269
336
|
}
|
|
270
337
|
}
|
|
271
338
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
339
|
+
|
|
340
|
+
// Reliquat
|
|
341
|
+
if (buffer.length > 0) {
|
|
342
|
+
const chunkMsg = RED.util.cloneMessage(skeletonMsg);
|
|
343
|
+
objPath.set(chunkMsg, outProp, buffer);
|
|
344
|
+
chunkMsg.odbc_stream = { index: rowCount - buffer.length, count: buffer.length, complete: false };
|
|
345
|
+
send(chunkMsg);
|
|
277
346
|
}
|
|
278
|
-
|
|
279
|
-
|
|
347
|
+
|
|
348
|
+
// Message de fin
|
|
349
|
+
const finalMsg = RED.util.cloneMessage(skeletonMsg);
|
|
350
|
+
objPath.set(finalMsg, outProp, []);
|
|
280
351
|
finalMsg.odbc_stream = { index: rowCount, count: 0, complete: true };
|
|
281
352
|
send(finalMsg);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (cursor) {
|
|
285
|
-
try {
|
|
286
|
-
await Promise.race([
|
|
287
|
-
cursor.close(),
|
|
288
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Cursor close timeout')), this.cursorCloseOperationTimeout))
|
|
289
|
-
]);
|
|
290
|
-
} catch (cursorCloseError) { this.warn(`Error or timeout closing cursor: ${cursorCloseError.message}`); }
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
};
|
|
353
|
+
|
|
354
|
+
this.status({ fill: "green", shape: "dot", text: `done (${rowCount} rows)` });
|
|
294
355
|
|
|
295
|
-
this.testBasicConnectivity = async function(connection) {
|
|
296
|
-
if (!connection || typeof connection.query !== 'function') {
|
|
297
|
-
this.warn("Test de connectivité basique : connexion invalide fournie.");
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
let originalTimeout;
|
|
301
|
-
try {
|
|
302
|
-
originalTimeout = connection.queryTimeout;
|
|
303
|
-
connection.queryTimeout = 5;
|
|
304
|
-
await connection.query("SELECT 1");
|
|
305
|
-
this.log("Test de connectivité basique (SELECT 1) : Réussi.");
|
|
306
|
-
return true;
|
|
307
|
-
} catch (testError) {
|
|
308
|
-
this.warn(`Test de connectivité basique (SELECT 1) : Échoué - ${testError.message}`);
|
|
309
|
-
return false;
|
|
310
356
|
} finally {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
catch(e) { this.warn("
|
|
357
|
+
// Fermeture garantie du curseur
|
|
358
|
+
if (cursor) {
|
|
359
|
+
try { await cursor.close(); } catch(e) { this.warn("Cursor close warning: " + e.message); }
|
|
314
360
|
}
|
|
315
361
|
}
|
|
316
362
|
};
|
|
317
363
|
|
|
364
|
+
// Récupération dynamique de la Query et des Params
|
|
318
365
|
this.getRenderedQueryAndParams = async function(msg) {
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
this.currentParamsForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, val) => resolve(err ? undefined : val)));
|
|
325
|
-
this.currentQueryForErrorContext = await new Promise(resolve => RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, val) => resolve(err ? undefined : (val || this.config.query || ""))));
|
|
326
|
-
|
|
327
|
-
if (!this.currentQueryForErrorContext) {
|
|
328
|
-
throw new Error("No query to execute. Please provide a query in the node's configuration or via msg." + (querySourceType === 'msg' ? querySource : 'querySource (non-msg)'));
|
|
329
|
-
}
|
|
366
|
+
const qSource = this.config.querySource || 'query';
|
|
367
|
+
const qType = this.config.querySourceType || 'msg';
|
|
368
|
+
const pSource = this.config.paramsSource || 'parameters';
|
|
369
|
+
const pType = this.config.paramsSourceType || 'msg';
|
|
330
370
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
371
|
+
const paramsVal = await new Promise((resolve, reject) => {
|
|
372
|
+
RED.util.evaluateNodeProperty(pSource, pType, this, msg, (err, res) => {
|
|
373
|
+
if(err) resolve(undefined); else resolve(res);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const queryVal = await new Promise((resolve, reject) => {
|
|
378
|
+
RED.util.evaluateNodeProperty(qSource, qType, this, msg, (err, res) => {
|
|
379
|
+
if(err) resolve(undefined); else resolve(res || this.config.query || "");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
this.currentParamsForErrorContext = paramsVal;
|
|
384
|
+
this.currentQueryForErrorContext = queryVal;
|
|
385
|
+
|
|
386
|
+
if (!queryVal) throw new Error("No SQL query provided via config or input msg.");
|
|
387
|
+
|
|
388
|
+
let finalQuery = queryVal;
|
|
389
|
+
// Si pas de ? (paramétré) et qu'on a un msg, on tente Mustache
|
|
390
|
+
// Note: Si paramsVal existe, on suppose que c'est une requête préparée, on évite Mustache pour la sécurité
|
|
391
|
+
if (!paramsVal && finalQuery.includes("{{")) {
|
|
334
392
|
finalQuery = mustache.render(finalQuery, msg);
|
|
335
393
|
}
|
|
336
|
-
return { query: finalQuery, params: this.currentParamsForErrorContext };
|
|
337
|
-
};
|
|
338
394
|
|
|
339
|
-
|
|
340
|
-
const configuredTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
|
|
341
|
-
if (configuredTimeout > 0) {
|
|
342
|
-
try { connection.queryTimeout = configuredTimeout; }
|
|
343
|
-
catch (e) { this.warn(`Could not set queryTimeout on connection: ${e.message}`); }
|
|
344
|
-
} else {
|
|
345
|
-
connection.queryTimeout = 0;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
this.status({ fill: "blue", shape: "dot", text: "executing..." });
|
|
349
|
-
if (this.config.streaming) {
|
|
350
|
-
await this.executeStreamQuery(connection, query, params, msg, send);
|
|
351
|
-
} else {
|
|
352
|
-
const newMsg = await this.executeQueryAndProcess(connection, query, params, msg);
|
|
353
|
-
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
354
|
-
send(newMsg);
|
|
355
|
-
}
|
|
395
|
+
return { query: finalQuery, params: paramsVal };
|
|
356
396
|
};
|
|
357
397
|
|
|
398
|
+
// =====================================================================
|
|
399
|
+
// LOGIQUE PRINCIPALE (SAFE PATTERN & NON-BLOCKING UI)
|
|
400
|
+
// =====================================================================
|
|
358
401
|
this.on("input", async (msg, send, done) => {
|
|
359
|
-
|
|
360
|
-
this.
|
|
361
|
-
|
|
362
|
-
if (this.isAwaitingRetry) {
|
|
363
|
-
if (this.poolNode && this.poolNode.config.retryOnMsg === true) {
|
|
364
|
-
this.log("New message received, overriding retry timer and attempting query now.");
|
|
365
|
-
clearTimeout(this.retryTimer); this.retryTimer = null; this.isAwaitingRetry = false;
|
|
366
|
-
} else {
|
|
367
|
-
this.warn("Node is in a retry-wait state. New message ignored.");
|
|
368
|
-
if (done) done(); return;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
402
|
+
// Nettoyage état précédent
|
|
403
|
+
if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = null; }
|
|
371
404
|
this.isAwaitingRetry = false;
|
|
372
|
-
if(this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = null; }
|
|
373
405
|
|
|
374
406
|
if (!this.poolNode) {
|
|
375
|
-
this.status({ fill: "red", shape: "ring", text: "
|
|
376
|
-
return done(
|
|
407
|
+
this.status({ fill: "red", shape: "ring", text: "Config missing" });
|
|
408
|
+
return done(new Error("ODBC Config node not configured"));
|
|
377
409
|
}
|
|
378
410
|
|
|
379
|
-
|
|
380
|
-
let
|
|
411
|
+
// 1. Préparation Données
|
|
412
|
+
let queryData;
|
|
381
413
|
try {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
paramsToExecute = queryData.params;
|
|
385
|
-
} catch (inputValidationError) {
|
|
414
|
+
queryData = await this.getRenderedQueryAndParams(msg);
|
|
415
|
+
} catch (e) {
|
|
386
416
|
this.status({ fill: "red", shape: "ring", text: "Input Error" });
|
|
387
|
-
return done(
|
|
417
|
+
return done(e);
|
|
388
418
|
}
|
|
389
|
-
|
|
390
|
-
let activeConnection = null;
|
|
391
|
-
let errorForUser = null;
|
|
392
|
-
let shouldProceedToTimedRetry = false;
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
this.status({ fill: "yellow", shape: "dot", text: "connecting (pool)..." });
|
|
396
|
-
activeConnection = await this.poolNode.connect();
|
|
397
|
-
await this.executeUserQuery(activeConnection, queryToExecute, paramsToExecute, msg, send);
|
|
398
|
-
|
|
399
|
-
done();
|
|
400
|
-
|
|
401
|
-
if (activeConnection) {
|
|
402
|
-
try { await activeConnection.close(); } catch(e) { this.warn("Error closing pool connection after success: " + e.message); }
|
|
403
|
-
activeConnection = null;
|
|
404
|
-
}
|
|
405
|
-
return;
|
|
406
419
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
420
|
+
let connection = null;
|
|
421
|
+
let isFreshConnection = false;
|
|
422
|
+
let errorOccurred = null;
|
|
423
|
+
let shouldScheduleRetry = false;
|
|
411
424
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
425
|
+
// 2. Exécution avec bloc FINALLY pour anti-fuite
|
|
426
|
+
try {
|
|
427
|
+
// A. Obtention de la connexion
|
|
428
|
+
try {
|
|
429
|
+
this.status({ fill: "yellow", shape: "dot", text: "connecting..." });
|
|
430
|
+
connection = await this.poolNode.connect();
|
|
431
|
+
} catch (poolErr) {
|
|
432
|
+
// Si le pool échoue et que l'option RetryFresh est active
|
|
433
|
+
if (this.poolNode.config.retryFreshConnection) {
|
|
434
|
+
this.warn("Pool connection failed, attempting fresh connection...");
|
|
435
|
+
const freshCfg = this.poolNode.getFreshConnectionConfig();
|
|
436
|
+
connection = await odbcModule.connect(freshCfg);
|
|
437
|
+
isFreshConnection = true;
|
|
438
|
+
this.log("Fresh connection established.");
|
|
439
|
+
} else {
|
|
440
|
+
throw poolErr;
|
|
420
441
|
}
|
|
421
442
|
}
|
|
443
|
+
|
|
444
|
+
// B. Configuration du Timeout (si supporté par l'objet connexion)
|
|
445
|
+
const qTimeout = parseInt(this.poolNode.config.queryTimeoutSeconds, 10);
|
|
446
|
+
if (qTimeout > 0) {
|
|
447
|
+
connection.queryTimeout = qTimeout;
|
|
448
|
+
} else {
|
|
449
|
+
connection.queryTimeout = 0; // Défaut
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// C. Exécution (NON-BLOCKING UI)
|
|
453
|
+
this.status({ fill: "blue", shape: "dot", text: "querying..." });
|
|
422
454
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
428
|
-
activeConnection = await odbcModule.connect(freshConnectConfig);
|
|
429
|
-
this.log("Fresh connection established.");
|
|
455
|
+
// FORCE UI REFRESH: On rend la main à l'Event Loop pour afficher "querying"
|
|
456
|
+
// avant que le driver ODBC (C++) ne bloque le thread lors de la préparation.
|
|
457
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
430
458
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
459
|
+
if (this.config.streaming) {
|
|
460
|
+
await this.executeStreamQuery(connection, queryData.query, queryData.params, msg, send);
|
|
461
|
+
} else {
|
|
462
|
+
const resultMsg = await this.executeQueryAndProcess(connection, queryData.query, queryData.params, msg);
|
|
463
|
+
send(resultMsg);
|
|
464
|
+
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// D. Si on a réussi avec une Fresh Connection, le pool est probablement cassé, on le reset
|
|
468
|
+
if (isFreshConnection) {
|
|
469
|
+
this.poolNode.resetPool();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
done(); // Succès
|
|
473
|
+
|
|
474
|
+
} catch (err) {
|
|
475
|
+
errorOccurred = err;
|
|
476
|
+
// Analyse de l'erreur pour voir si c'est une erreur de connexion (réseau)
|
|
477
|
+
const isConnectionError = err.message && (
|
|
478
|
+
err.message.match(/connection|network|timeout|communication|link|provider/i)
|
|
479
|
+
|| (err.odbcErrors && err.odbcErrors.length > 0)
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
if (isConnectionError || this.poolNode.config.retryFreshConnection) {
|
|
483
|
+
shouldScheduleRetry = true;
|
|
484
|
+
}
|
|
485
|
+
} finally {
|
|
486
|
+
// E. NETTOYAGE CRITIQUE (Le "Fix" principal)
|
|
487
|
+
if (connection) {
|
|
488
|
+
try {
|
|
489
|
+
// Si c'était une connexion du pool, on réinitialise le timeout pour ne pas polluer le pool
|
|
490
|
+
if (!isFreshConnection) {
|
|
491
|
+
connection.queryTimeout = 0;
|
|
458
492
|
}
|
|
493
|
+
await connection.close();
|
|
494
|
+
} catch (closeErr) {
|
|
495
|
+
this.warn("Error ensuring connection closed: " + closeErr.message);
|
|
459
496
|
}
|
|
460
|
-
} else {
|
|
461
|
-
errorForUser = this.enhanceError(initialDbError, null, null, "Connection Error (no fresh retry)");
|
|
462
|
-
shouldProceedToTimedRetry = true;
|
|
463
497
|
}
|
|
464
498
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.warn(`Connection issue. Scheduling retry in ${retryDelaySeconds}s. Error: ${errorForUser.message}`);
|
|
475
|
-
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelaySeconds}s...` });
|
|
499
|
+
|
|
500
|
+
// 3. Gestion post-exécution des erreurs et Retry
|
|
501
|
+
if (errorOccurred) {
|
|
502
|
+
const retryDelay = parseInt(this.poolNode.config.retryDelay, 10);
|
|
503
|
+
|
|
504
|
+
if (shouldScheduleRetry && retryDelay > 0) {
|
|
505
|
+
this.warn(`Query failed (${errorOccurred.message}). Retrying in ${retryDelay}s...`);
|
|
506
|
+
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s` });
|
|
507
|
+
|
|
476
508
|
this.isAwaitingRetry = true;
|
|
477
509
|
this.retryTimer = setTimeout(() => {
|
|
478
|
-
this.isAwaitingRetry = false;
|
|
479
|
-
this.log(
|
|
480
|
-
this.
|
|
481
|
-
},
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
510
|
+
this.isAwaitingRetry = false;
|
|
511
|
+
this.log("Retry timer expired. Re-processing message.");
|
|
512
|
+
this.emit("input", msg, send, done);
|
|
513
|
+
}, retryDelay * 1000);
|
|
514
|
+
|
|
515
|
+
done();
|
|
516
|
+
} else {
|
|
517
|
+
this.status({ fill: "red", shape: "dot", text: "Error" });
|
|
518
|
+
done(this.enhanceError(errorOccurred));
|
|
486
519
|
}
|
|
487
|
-
} else if (errorForUser) {
|
|
488
|
-
this.status({ fill: "red", shape: "ring", text: "Error (No Timed Retry)" });
|
|
489
|
-
return done(errorForUser); // Cas où c'est une erreur SQL identifiée, pas de retry temporisé.
|
|
490
|
-
} else {
|
|
491
|
-
// Ce chemin ne devrait pas être atteint si done() a été appelé dans un chemin de succès.
|
|
492
|
-
this.log("[ODBC Node] DEBUG: Reached unexpected end of on('input') path. Calling done().");
|
|
493
|
-
return done();
|
|
494
520
|
}
|
|
495
521
|
});
|
|
496
|
-
|
|
522
|
+
|
|
497
523
|
this.on("close", (done) => {
|
|
498
524
|
if (this.retryTimer) {
|
|
499
|
-
clearTimeout(this.retryTimer);
|
|
500
|
-
this.
|
|
525
|
+
clearTimeout(this.retryTimer);
|
|
526
|
+
this.retryTimer = null;
|
|
501
527
|
}
|
|
502
528
|
this.status({});
|
|
503
529
|
done();
|
|
504
530
|
});
|
|
505
|
-
|
|
531
|
+
|
|
532
|
+
// Status initial
|
|
506
533
|
if (this.poolNode) {
|
|
507
534
|
this.status({ fill: "green", shape: "dot", text: "ready" });
|
|
508
535
|
} else {
|
|
509
|
-
this.status({ fill: "red", shape: "ring", text: "No config
|
|
510
|
-
this.warn("ODBC Config node not found or not deployed.");
|
|
536
|
+
this.status({ fill: "red", shape: "ring", text: "No config" });
|
|
511
537
|
}
|
|
512
538
|
}
|
|
513
|
-
|
|
539
|
+
|
|
514
540
|
RED.nodes.registerType("odbc", odbc);
|
|
515
541
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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",
|