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