@bkmj/node-red-contrib-odbcmj 2.0.3 → 2.1.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 +16 -11
- package/odbc.html +105 -9
- package/odbc.js +106 -84
- 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, and result set streaming.
|
|
3
|
+
A powerful and robust Node-RED node to connect to any ODBC data source. It features connection pooling, advanced retry logic, secure credential management, dynamic query sources, and result set streaming.
|
|
4
4
|
|
|
5
5
|
This node is a fork with significant enhancements to provide stability and advanced features for enterprise use cases.
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ This node is a fork with significant enhancements to provide stability and advan
|
|
|
10
10
|
- **Hybrid Configuration**: Configure connections using simple structured fields or a full connection string for maximum flexibility.
|
|
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
|
+
- **Dynamic Inputs**: Source your SQL query and parameters from message properties, flow/global context, or environment variables.
|
|
13
14
|
- **Advanced Retry Logic**: Automatically handles connection errors with configurable delays and retries to ensure flow resilience.
|
|
14
15
|
- **Result Streaming**: Process queries with millions of rows without exhausting memory by streaming results as chunks.
|
|
15
16
|
- **Syntax Checker**: Optionally parse the SQL query to validate its structure.
|
|
@@ -38,11 +39,9 @@ This is the easiest and most secure way to set up a connection for common databa
|
|
|
38
39
|
|
|
39
40
|
##### 2. Connection String Mode (Advanced)
|
|
40
41
|
|
|
41
|
-
This mode gives you full control for complex or non-standard connection strings.
|
|
42
|
+
This mode gives you full control for complex or non-standard connection strings. In this mode, you are responsible for the entire content of the string.
|
|
42
43
|
|
|
43
44
|
- **Connection String**: Enter the complete ODBC connection string.
|
|
44
|
-
- **Password Handling**: For security, **do not** write your password directly in the string. Instead, use the `{{{password}}}` placeholder. The node will automatically replace it with the password entered in the secure `Password` field below.
|
|
45
|
-
- Example: `DRIVER={...};SERVER=...;UID=myuser;PWD={{{password}}};`
|
|
46
45
|
|
|
47
46
|
#### Test Connection
|
|
48
47
|
|
|
@@ -60,7 +59,7 @@ A **Test Connection** button in the configuration panel allows you to instantly
|
|
|
60
59
|
#### Error Handling & Retry
|
|
61
60
|
|
|
62
61
|
- **`retryFreshConnection`** `<boolean>` (optional): If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections. Default: false.
|
|
63
|
-
- **`retryDelay`** `<number>` (optional): If both the pooled and the fresh connection attempts fail, this sets a delay in seconds before another retry is attempted.
|
|
62
|
+
- **`retryDelay`** `<number>` (optional): If both the pooled and the fresh connection attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries. Default: 5.
|
|
64
63
|
- **`retryOnMsg`** `<boolean>` (optional): If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry. Default: true.
|
|
65
64
|
|
|
66
65
|
#### Advanced
|
|
@@ -77,14 +76,20 @@ This node executes a query against the configured database when it receives a me
|
|
|
77
76
|
#### Properties
|
|
78
77
|
|
|
79
78
|
- **`connection`** `<odbc config>` (**required**): The configuration node that defines the connection settings.
|
|
80
|
-
- **`
|
|
81
|
-
- **`
|
|
79
|
+
- **`Query`** `<string>` (optional): A default SQL query to execute if no query is provided dynamically from an input source. Can contain Mustache syntax (e.g., `{{{payload.id}}}`).
|
|
80
|
+
- **`Result to`** `<string>` (**required**): The property of the output message where the results will be stored (e.g., `payload`). Default: `payload`.
|
|
82
81
|
|
|
83
|
-
#### Inputs
|
|
82
|
+
#### Dynamic Inputs
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- **`
|
|
84
|
+
To make the node highly flexible, the SQL query and its parameters can be sourced dynamically using **Typed Inputs**.
|
|
85
|
+
|
|
86
|
+
- **`Query Source`**: A Typed Input that specifies where to find the query string at runtime. This value **overrides** the static query defined in the editor.
|
|
87
|
+
- *Default*: `msg.query` (for backward compatibility).
|
|
88
|
+
- *Example*: Set to `flow.mySqlQuery` to read the query from a flow context variable.
|
|
89
|
+
|
|
90
|
+
- **`Parameters Source`**: A Typed Input that specifies where to find the array or object of parameters for prepared statements.
|
|
91
|
+
- *Default*: `msg.parameters`.
|
|
92
|
+
- *Example*: Set to `msg.payload.bindings` to use the array found in that property.
|
|
88
93
|
|
|
89
94
|
#### Streaming Results
|
|
90
95
|
|
package/odbc.html
CHANGED
|
@@ -31,13 +31,11 @@
|
|
|
31
31
|
oneditprepare: function () {
|
|
32
32
|
var node = this;
|
|
33
33
|
|
|
34
|
-
// Logique pour les sections déroulantes
|
|
35
34
|
$('.form-section-header').on('click', function() {
|
|
36
35
|
$(this).toggleClass('expanded');
|
|
37
36
|
$(this).next('.form-section-content').slideToggle();
|
|
38
37
|
});
|
|
39
38
|
|
|
40
|
-
// Logique pour le mode de connexion hybride
|
|
41
39
|
function toggleConnectionMode(mode) {
|
|
42
40
|
if (mode === 'structured') {
|
|
43
41
|
$(".config-mode-structured").show();
|
|
@@ -63,7 +61,6 @@
|
|
|
63
61
|
toggleDriverField($(this).val());
|
|
64
62
|
}).trigger("change");
|
|
65
63
|
|
|
66
|
-
// Logiques pour les cases à cocher
|
|
67
64
|
$("#node-config-input-syntaxtick").on("change", function () {
|
|
68
65
|
$(".input-syntax").toggle(this.checked);
|
|
69
66
|
}).trigger("change");
|
|
@@ -72,7 +69,6 @@
|
|
|
72
69
|
$(".retry-options").toggle(this.checked);
|
|
73
70
|
}).trigger("change");
|
|
74
71
|
|
|
75
|
-
// Logique pour le bouton de test
|
|
76
72
|
$('#node-config-test-connection').on('click', function() {
|
|
77
73
|
var button = $(this);
|
|
78
74
|
var originalText = "Test Connection";
|
|
@@ -81,7 +77,6 @@
|
|
|
81
77
|
button.text(' Testing...').prop('disabled', true);
|
|
82
78
|
|
|
83
79
|
var connectionMode = $("#node-config-input-connectionMode").val();
|
|
84
|
-
|
|
85
80
|
var configData = {
|
|
86
81
|
connectionMode: connectionMode,
|
|
87
82
|
dbType: $("#node-config-input-dbType").val(),
|
|
@@ -90,7 +85,6 @@
|
|
|
90
85
|
database: $("#node-config-input-database").val(),
|
|
91
86
|
user: $("#node-config-input-user").val(),
|
|
92
87
|
connectionString: $("#node-config-input-connectionString").val(),
|
|
93
|
-
// Le mot de passe n'est envoyé que si on est en mode structuré
|
|
94
88
|
password: (connectionMode === 'structured') ? $("#node-config-input-password").val() : null
|
|
95
89
|
};
|
|
96
90
|
|
|
@@ -277,7 +271,12 @@
|
|
|
277
271
|
query: { value: "" },
|
|
278
272
|
outputObj: { value: "payload" },
|
|
279
273
|
streaming: { value: false },
|
|
280
|
-
streamChunkSize: { value: 1, validate: RED.validators.number() }
|
|
274
|
+
streamChunkSize: { value: 1, validate: RED.validators.number() },
|
|
275
|
+
// Nouveaux `defaults` pour les Typed Inputs
|
|
276
|
+
querySource: { value: "query", required: false },
|
|
277
|
+
querySourceType: { value: "msg", required: false },
|
|
278
|
+
paramsSource: { value: "parameters", required: false },
|
|
279
|
+
paramsSourceType: { value: "msg", required: false }
|
|
281
280
|
},
|
|
282
281
|
inputs: 1,
|
|
283
282
|
outputs: 1,
|
|
@@ -295,6 +294,19 @@
|
|
|
295
294
|
$("#node-input-streaming").on("change", function() {
|
|
296
295
|
$(".stream-options").toggle(this.checked);
|
|
297
296
|
}).trigger("change");
|
|
297
|
+
|
|
298
|
+
// Initialisation des Typed Inputs
|
|
299
|
+
$("#node-input-querySource").typedInput({
|
|
300
|
+
default: 'msg',
|
|
301
|
+
typeField: "#node-input-querySourceType",
|
|
302
|
+
types: ['msg', 'flow', 'global', 'env', 'str', 'jsonata']
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
$("#node-input-paramsSource").typedInput({
|
|
306
|
+
default: 'msg',
|
|
307
|
+
typeField: "#node-input-paramsSourceType",
|
|
308
|
+
types: ['msg', 'flow', 'global', 'env', 'jsonata']
|
|
309
|
+
});
|
|
298
310
|
},
|
|
299
311
|
oneditsave: function () {
|
|
300
312
|
this.query = this.editor.getValue();
|
|
@@ -318,14 +330,29 @@
|
|
|
318
330
|
<input type="text" id="node-input-connection" />
|
|
319
331
|
</div>
|
|
320
332
|
<div class="form-row node-text-editor-row">
|
|
321
|
-
<label for="node-input-query" style="width: 100% !important;"><i class="fa fa-
|
|
333
|
+
<label for="node-input-query" style="width: 100% !important;"><i class="fa fa-file-code-o"></i> Query (fallback)</label>
|
|
322
334
|
<div style="height: 250px;" class="node-text-editor" id="node-input-query-editor"></div>
|
|
323
335
|
</div>
|
|
324
336
|
<div class="form-row">
|
|
325
|
-
<label for="node-input-outputObj"><i class="fa fa-
|
|
337
|
+
<label for="node-input-outputObj"><i class="fa fa-sign-out"></i> Result to</label>
|
|
326
338
|
<span>msg.</span><input type="text" id="node-input-outputObj" placeholder="payload" style="width: 64%;"/>
|
|
327
339
|
</div>
|
|
340
|
+
|
|
341
|
+
<hr/>
|
|
342
|
+
|
|
343
|
+
<div class="form-row">
|
|
344
|
+
<label for="node-input-querySource"><i class="fa fa-crosshairs"></i> Query Source</label>
|
|
345
|
+
<input type="text" id="node-input-querySource" style="width: 70%;">
|
|
346
|
+
<input type="hidden" id="node-input-querySourceType">
|
|
347
|
+
</div>
|
|
348
|
+
<div class="form-row">
|
|
349
|
+
<label for="node-input-paramsSource"><i class="fa fa-list-ol"></i> Parameters Source</label>
|
|
350
|
+
<input type="text" id="node-input-paramsSource" style="width: 70%;">
|
|
351
|
+
<input type="hidden" id="node-input-paramsSourceType">
|
|
352
|
+
</div>
|
|
353
|
+
|
|
328
354
|
<hr/>
|
|
355
|
+
|
|
329
356
|
<div class="form-row">
|
|
330
357
|
<label for="node-input-streaming" style="width: auto;"><i class="fa fa-arrows-v"></i> Stream Results</label>
|
|
331
358
|
<input type="checkbox" id="node-input-streaming" style="display: inline-block; width: auto; vertical-align: top;">
|
|
@@ -335,4 +362,73 @@
|
|
|
335
362
|
<input type="number" id="node-input-streamChunkSize" placeholder="1" style="width: 100px;">
|
|
336
363
|
<span class="form-tips">Number of rows per output message.</span>
|
|
337
364
|
</div>
|
|
365
|
+
</script>
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
<script type="text/markdown" data-help-name="odbc config">
|
|
369
|
+
A configuration node that manages the connection to your database.
|
|
370
|
+
|
|
371
|
+
### Connection Modes
|
|
372
|
+
Version 2.0 introduces two ways to configure your connection:
|
|
373
|
+
|
|
374
|
+
#### 1. Structured Fields Mode (Recommended)
|
|
375
|
+
This is the easiest and most secure way to set up a connection for common databases.
|
|
376
|
+
- **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.
|
|
377
|
+
- **Server**: The hostname or IP address of the database server, optionally followed by a comma and the port number (e.g., `mydb.server.com,1433`).
|
|
378
|
+
- **Database**: The name of the database to connect to (optional).
|
|
379
|
+
- **User**: The username for authentication.
|
|
380
|
+
- **Password**: The password for authentication. This is stored securely using Node-RED's credential system.
|
|
381
|
+
|
|
382
|
+
#### 2. Connection String Mode (Advanced)
|
|
383
|
+
This mode gives you full control for complex or non-standard connection strings.
|
|
384
|
+
- **Connection String**: Enter the complete ODBC connection string. It is your responsibility to provide a valid string for your driver.
|
|
385
|
+
|
|
386
|
+
### Test Connection
|
|
387
|
+
A **Test Connection** button in the configuration panel allows you to instantly verify your settings without deploying the flow.
|
|
388
|
+
|
|
389
|
+
### Pool Options
|
|
390
|
+
- **`initialSize`**: The number of connections to create when the pool is initialized. Default: 5.
|
|
391
|
+
- **`maxSize`**: The maximum number of connections allowed in the pool. Default: 15.
|
|
392
|
+
- (See `odbc` package documentation for more details on pool options).
|
|
393
|
+
|
|
394
|
+
### Error Handling & Retry
|
|
395
|
+
- **`retryFreshConnection`**: If a query fails, the node will retry once with a brand new connection. If this succeeds, the entire connection pool is reset to clear any stale connections.
|
|
396
|
+
- **`retryDelay`**: If both attempts fail, this sets a delay in seconds before another retry is attempted. A value of **0** disables further automatic retries.
|
|
397
|
+
- **`retryOnMsg`**: If the node is waiting for a timed retry, a new incoming message can override the timer and trigger an immediate retry.
|
|
398
|
+
|
|
399
|
+
### Advanced
|
|
400
|
+
- **`syntaxChecker`**: If activated, the query string will be parsed and appended to the output message at `msg.parsedQuery`.
|
|
401
|
+
</script>
|
|
402
|
+
|
|
403
|
+
<script type="text/markdown" data-help-name="odbc">
|
|
404
|
+
Executes a query against a configured ODBC data source.
|
|
405
|
+
|
|
406
|
+
### Inputs
|
|
407
|
+
|
|
408
|
+
- **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.
|
|
409
|
+
- *Default behavior (for backward compatibility): `msg.query`*
|
|
410
|
+
|
|
411
|
+
- **Parameters Source** (optional): Specify where to get the parameters for a prepared statement. This should resolve to an array of values.
|
|
412
|
+
- *Default behavior (for backward compatibility): `msg.parameters`*
|
|
413
|
+
|
|
414
|
+
### Properties
|
|
415
|
+
|
|
416
|
+
- **Connection**: The `odbc config` node to use.
|
|
417
|
+
- **Query (fallback)**: A static SQL query to run if the "Query Source" does not provide one. Can contain Mustache syntax (e.g., `{{{payload.id}}}`).
|
|
418
|
+
- **Result to**: The `msg` property where the query result will be stored. Default: `payload`.
|
|
419
|
+
|
|
420
|
+
### Streaming Results
|
|
421
|
+
|
|
422
|
+
For queries that return a large number of rows, streaming prevents high memory usage by sending multiple messages instead of one large one.
|
|
423
|
+
|
|
424
|
+
- **`Stream Results`**: Enables or disables streaming mode.
|
|
425
|
+
- **`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.
|
|
426
|
+
|
|
427
|
+
#### Streaming Output Format
|
|
428
|
+
When streaming is active, each output message will contain:
|
|
429
|
+
- A payload (or the configured output property) containing an array of rows for the current chunk.
|
|
430
|
+
- A `msg.odbc_stream` object with metadata for tracking progress:
|
|
431
|
+
- `index`: The starting index of the current chunk (e.g., 0, 100, 200...).
|
|
432
|
+
- `count`: The number of rows in the current chunk.
|
|
433
|
+
- `complete`: A boolean that is `true` only on the very last message of the stream, useful for triggering a final action.
|
|
338
434
|
</script>
|
package/odbc.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
2
|
const odbcModule = require("odbc");
|
|
3
|
-
const mustache = require("mustache");
|
|
4
|
-
const objPath = require("object-path");
|
|
3
|
+
const mustache = require("mustache");
|
|
4
|
+
const objPath = require("object-path");
|
|
5
5
|
|
|
6
6
|
// --- ODBC Configuration Node ---
|
|
7
7
|
function poolConfig(config) {
|
|
@@ -33,9 +33,6 @@ module.exports = function (RED) {
|
|
|
33
33
|
return parts.join(';');
|
|
34
34
|
} else {
|
|
35
35
|
let connStr = this.config.connectionString || "";
|
|
36
|
-
if (this.credentials && this.credentials.password && connStr.includes('{{{password}}}')) {
|
|
37
|
-
connStr = connStr.replace('{{{password}}}', this.credentials.password);
|
|
38
|
-
}
|
|
39
36
|
return connStr;
|
|
40
37
|
}
|
|
41
38
|
};
|
|
@@ -120,16 +117,12 @@ module.exports = function (RED) {
|
|
|
120
117
|
}
|
|
121
118
|
});
|
|
122
119
|
|
|
123
|
-
// --- API ENDPOINT POUR LE BOUTON DE TEST (VERSION CORRIGÉE) ---
|
|
124
120
|
RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
|
|
125
121
|
const tempConfig = req.body;
|
|
126
122
|
|
|
127
|
-
// Cette fonction interne ne doit PAS interagir avec `res`.
|
|
128
|
-
// Elle retourne une chaîne ou lance une erreur.
|
|
129
123
|
const buildTestConnectionString = () => {
|
|
130
124
|
if (tempConfig.connectionMode === 'structured') {
|
|
131
125
|
if (!tempConfig.dbType || !tempConfig.server) {
|
|
132
|
-
// On lance une erreur au lieu d'envoyer une réponse.
|
|
133
126
|
throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
|
|
134
127
|
}
|
|
135
128
|
let driver;
|
|
@@ -141,17 +134,13 @@ module.exports = function (RED) {
|
|
|
141
134
|
default: driver = tempConfig.driver || ''; break;
|
|
142
135
|
}
|
|
143
136
|
if(driver) parts.unshift(`DRIVER={${driver}}`);
|
|
144
|
-
|
|
137
|
+
parts.push(`SERVER=${tempConfig.server}`);
|
|
145
138
|
if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
|
|
146
139
|
if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
|
|
147
|
-
// Le mot de passe est géré dans le bloc try/catch principal
|
|
148
140
|
if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
|
|
149
|
-
|
|
150
141
|
return parts.join(';');
|
|
151
|
-
|
|
152
|
-
} else { // 'string' mode
|
|
142
|
+
} else {
|
|
153
143
|
let connStr = tempConfig.connectionString || "";
|
|
154
|
-
// Le mot de passe est supposé être dans la chaîne ici
|
|
155
144
|
if (!connStr) {
|
|
156
145
|
throw new Error("La chaîne de connexion ne peut pas être vide.");
|
|
157
146
|
}
|
|
@@ -161,14 +150,10 @@ module.exports = function (RED) {
|
|
|
161
150
|
|
|
162
151
|
let connection;
|
|
163
152
|
try {
|
|
164
|
-
// Le bloc try/catch gère maintenant TOUTES les erreurs.
|
|
165
153
|
const testConnectionString = buildTestConnectionString();
|
|
166
|
-
|
|
167
154
|
connection = await odbcModule.connect(testConnectionString);
|
|
168
|
-
res.sendStatus(200);
|
|
169
|
-
|
|
155
|
+
res.sendStatus(200);
|
|
170
156
|
} catch (err) {
|
|
171
|
-
// Qu'il s'agisse d'une erreur de validation ou de connexion, on l'attrape ici.
|
|
172
157
|
res.status(500).send(err.message || "Erreur inconnue durant le test.");
|
|
173
158
|
} finally {
|
|
174
159
|
if (connection) {
|
|
@@ -272,14 +257,23 @@ module.exports = function (RED) {
|
|
|
272
257
|
return newMsg;
|
|
273
258
|
};
|
|
274
259
|
|
|
275
|
-
|
|
260
|
+
// =================================================================
|
|
261
|
+
// DEBUT DE LA SECTION CORRIGÉE
|
|
262
|
+
// =================================================================
|
|
263
|
+
|
|
264
|
+
this.executeStreamQuery = async (queryString, queryParams, msg, send, done) => {
|
|
276
265
|
const chunkSize = parseInt(this.config.streamChunkSize) || 1;
|
|
277
266
|
let cursor;
|
|
278
267
|
let rowCount = 0;
|
|
279
268
|
let chunk = [];
|
|
280
269
|
|
|
281
270
|
try {
|
|
282
|
-
|
|
271
|
+
// CORRECTION : Appeler .cursor() sur le pool, pas sur une connexion individuelle.
|
|
272
|
+
if (!this.poolNode || !this.poolNode.pool) {
|
|
273
|
+
throw new Error("Le pool de connexions n'est pas initialisé pour le streaming.");
|
|
274
|
+
}
|
|
275
|
+
cursor = await this.poolNode.pool.cursor(queryString, queryParams);
|
|
276
|
+
|
|
283
277
|
this.status({ fill: "blue", shape: "dot", text: "streaming rows..." });
|
|
284
278
|
let row = await cursor.fetch();
|
|
285
279
|
while (row) {
|
|
@@ -309,6 +303,7 @@ module.exports = function (RED) {
|
|
|
309
303
|
this.status({ fill: "green", shape: "dot", text: `success (${rowCount} rows)` });
|
|
310
304
|
if(done) done();
|
|
311
305
|
} catch(err) {
|
|
306
|
+
// L'erreur sera transmise à l'appelant (runQuery)
|
|
312
307
|
throw err;
|
|
313
308
|
}
|
|
314
309
|
finally {
|
|
@@ -317,15 +312,32 @@ module.exports = function (RED) {
|
|
|
317
312
|
};
|
|
318
313
|
|
|
319
314
|
this.runQuery = async (msg, send, done) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
let isPreparedStatement = false;
|
|
323
|
-
let connectionFromPool = null;
|
|
315
|
+
let isPreparedStatement = false;
|
|
316
|
+
let connectionFromPool = null;
|
|
324
317
|
|
|
325
|
-
|
|
318
|
+
try {
|
|
326
319
|
this.status({ fill: "blue", shape: "dot", text: "preparing..." });
|
|
327
320
|
this.config.outputObj = msg?.output || this.config?.outputObj || "payload";
|
|
328
321
|
|
|
322
|
+
const querySourceType = this.config.querySourceType || 'msg';
|
|
323
|
+
const querySource = this.config.querySource || 'query';
|
|
324
|
+
const paramsSourceType = this.config.paramsSourceType || 'msg';
|
|
325
|
+
const paramsSource = this.config.paramsSource || 'parameters';
|
|
326
|
+
|
|
327
|
+
let currentQueryParams = await new Promise((resolve) => {
|
|
328
|
+
RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => {
|
|
329
|
+
resolve(err ? undefined : value);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
let currentQueryString = await new Promise((resolve) => {
|
|
334
|
+
RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => {
|
|
335
|
+
resolve(err ? undefined : (value || this.config.query || ""));
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (!currentQueryString) { throw new Error("No query to execute"); }
|
|
340
|
+
|
|
329
341
|
isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
|
|
330
342
|
if (!isPreparedStatement && currentQueryString) {
|
|
331
343
|
for (const parsed of mustache.parse(currentQueryString)) {
|
|
@@ -335,74 +347,80 @@ module.exports = function (RED) {
|
|
|
335
347
|
}
|
|
336
348
|
currentQueryString = mustache.render(currentQueryString, msg);
|
|
337
349
|
}
|
|
338
|
-
if (msg?.query) { currentQueryString = msg.query; }
|
|
339
|
-
if (!currentQueryString) { throw new Error("No query to execute"); }
|
|
340
350
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
351
|
+
// CORRECTION : Logique séparée pour streaming et non-streaming
|
|
352
|
+
if (this.config.streaming) {
|
|
353
|
+
// Le mode Streaming appelle directement la fonction corrigée
|
|
354
|
+
await this.executeStreamQuery(currentQueryString, currentQueryParams, msg, send, done);
|
|
355
|
+
|
|
356
|
+
} else {
|
|
357
|
+
// Le mode non-streaming utilise la logique de connexion/retry existante
|
|
358
|
+
const executeNonQuery = async (conn) => {
|
|
345
359
|
const processedMsg = await this.executeQueryAndProcess(conn, currentQueryString, currentQueryParams, isPreparedStatement, msg);
|
|
346
360
|
this.status({ fill: "green", shape: "dot", text: "success" });
|
|
347
361
|
send(processedMsg);
|
|
348
362
|
if(done) done();
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
let firstAttemptError = null;
|
|
366
|
+
try {
|
|
367
|
+
connectionFromPool = await this.poolNode.connect();
|
|
368
|
+
await executeNonQuery(connectionFromPool);
|
|
369
|
+
return;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
|
|
372
|
+
this.warn(`First attempt failed: ${firstAttemptError.message}`);
|
|
373
|
+
} finally {
|
|
374
|
+
if (connectionFromPool) await connectionFromPool.close();
|
|
349
375
|
}
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
let firstAttemptError = null;
|
|
353
|
-
try {
|
|
354
|
-
connectionFromPool = await this.poolNode.connect();
|
|
355
|
-
await execute(connectionFromPool);
|
|
356
|
-
return;
|
|
357
|
-
} catch (err) {
|
|
358
|
-
firstAttemptError = this.enhanceError(err, currentQueryString, currentQueryParams, "Query failed with pooled connection");
|
|
359
|
-
this.warn(`First attempt failed: ${firstAttemptError.message}`);
|
|
360
|
-
} finally {
|
|
361
|
-
if (connectionFromPool) await connectionFromPool.close();
|
|
362
|
-
}
|
|
363
376
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
377
|
+
if (firstAttemptError) {
|
|
378
|
+
if (this.poolNode && this.poolNode.config.retryFreshConnection) {
|
|
379
|
+
this.log("Attempting retry with a fresh connection.");
|
|
380
|
+
this.status({ fill: "yellow", shape: "dot", text: "Retrying (fresh)..." });
|
|
381
|
+
let freshConnection = null;
|
|
382
|
+
try {
|
|
383
|
+
const freshConnectConfig = this.poolNode.getFreshConnectionConfig();
|
|
384
|
+
freshConnection = await odbcModule.connect(freshConnectConfig);
|
|
385
|
+
this.log("Fresh connection established for retry.");
|
|
386
|
+
await executeNonQuery(freshConnection);
|
|
387
|
+
this.log("Query successful with fresh connection. Resetting pool.");
|
|
388
|
+
await this.poolNode.resetPool();
|
|
389
|
+
return;
|
|
390
|
+
} catch (freshError) {
|
|
391
|
+
this.warn(`Retry with fresh connection also failed: ${freshError.message}`);
|
|
392
|
+
const retryDelay = parseInt(this.poolNode.config.retryDelay) || 0;
|
|
393
|
+
if (retryDelay > 0) {
|
|
394
|
+
this.isAwaitingRetry = true;
|
|
395
|
+
this.status({ fill: "red", shape: "ring", text: `Retry in ${retryDelay}s...` });
|
|
396
|
+
this.log(`Scheduling retry in ${retryDelay} seconds.`);
|
|
397
|
+
this.retryTimer = setTimeout(() => {
|
|
398
|
+
this.isAwaitingRetry = false;
|
|
399
|
+
this.log("Timer expired. Triggering scheduled retry.");
|
|
400
|
+
this.receive(msg);
|
|
401
|
+
}, retryDelay * 1000);
|
|
402
|
+
if (done) done();
|
|
403
|
+
} else {
|
|
404
|
+
throw this.enhanceError(freshError, currentQueryString, currentQueryParams, "Query failed on fresh connection retry");
|
|
405
|
+
}
|
|
406
|
+
} finally {
|
|
407
|
+
if (freshConnection) await freshConnection.close();
|
|
392
408
|
}
|
|
393
|
-
}
|
|
394
|
-
|
|
409
|
+
} else {
|
|
410
|
+
throw firstAttemptError;
|
|
395
411
|
}
|
|
396
|
-
} else {
|
|
397
|
-
throw firstAttemptError;
|
|
398
412
|
}
|
|
399
413
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
414
|
+
} catch (err) {
|
|
415
|
+
const finalError = err instanceof Error ? err : new Error(String(err));
|
|
416
|
+
this.status({ fill: "red", shape: "ring", text: "query error" });
|
|
417
|
+
if (done) { done(finalError); } else { this.error(finalError, msg); }
|
|
418
|
+
}
|
|
405
419
|
};
|
|
420
|
+
|
|
421
|
+
// =================================================================
|
|
422
|
+
// FIN DE LA SECTION CORRIGÉE
|
|
423
|
+
// =================================================================
|
|
406
424
|
|
|
407
425
|
this.checkPool = async function (msg, send, done) {
|
|
408
426
|
try {
|
|
@@ -418,6 +436,10 @@ module.exports = function (RED) {
|
|
|
418
436
|
}, 1000);
|
|
419
437
|
return;
|
|
420
438
|
}
|
|
439
|
+
// S'assure que le pool est créé avant toute requête, y compris en streaming
|
|
440
|
+
if (!this.poolNode.pool) {
|
|
441
|
+
await this.poolNode.connect().then(c => c.close()); // Etablit le pool s'il n'existe pas
|
|
442
|
+
}
|
|
421
443
|
await this.runQuery(msg, send, done);
|
|
422
444
|
} catch (err) {
|
|
423
445
|
const finalError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -431,7 +453,7 @@ module.exports = function (RED) {
|
|
|
431
453
|
if (this.poolNode && this.poolNode.config.retryOnMsg) {
|
|
432
454
|
this.log("New message received, overriding retry timer and attempting query now.");
|
|
433
455
|
clearTimeout(this.retryTimer);
|
|
434
|
-
this.retryTimer = null;
|
|
456
|
+
this.retryTimer = null;
|
|
435
457
|
this.isAwaitingRetry = false;
|
|
436
458
|
} else {
|
|
437
459
|
this.warn("Node is in a retry-wait state. New message ignored as per configuration.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bkmj/node-red-contrib-odbcmj",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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",
|