@bkmj/node-red-contrib-odbcmj 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +16 -11
  2. package/odbc.html +105 -9
  3. package/odbc.js +35 -20
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Node-RED Contrib ODBC MJ
2
2
 
3
- A powerful and robust Node-RED node to connect to any ODBC data source. It features connection pooling, advanced retry logic, secure credential management, 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. This prevents infinite loops. A value of **0** disables further automatic retries. Default: 5.
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
- - **`query`** `<string>` (optional): The SQL query to execute. Can contain Mustache syntax (e.g., `{{{payload.id}}}`) which will be rendered using the incoming message object.
81
- - **`result to`** `<string>` (**required**): The property of the output message where the results will be stored (e.g., `payload`). Default: `payload`.
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
- The node can be configured dynamically using the incoming `msg` object:
86
- - **`msg.query`**: A query string that will override the one configured in the node.
87
- - **`msg.parameters`**: An array of values for prepared statements (when the query contains `?` placeholders).
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-search"></i> Query</label>
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-edit"></i> Result to</label>
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"); // Utilisé dans runQuery
4
- const objPath = require("object-path"); // Utilisé pour mustache et le positionnement du résultat
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
  };
@@ -122,12 +119,12 @@ module.exports = function (RED) {
122
119
 
123
120
  RED.httpAdmin.post("/odbc_config/:id/test", RED.auth.needsPermission("odbc.write"), async function(req, res) {
124
121
  const tempConfig = req.body;
125
- const tempCredentials = { password: tempConfig.password };
126
- delete tempConfig.password;
127
122
 
128
123
  const buildTestConnectionString = () => {
129
- if (tempConfig.connectionMode === 'structured') {
130
- if (!tempConfig.dbType || !tempConfig.server) return res.status(400).send("Mode structuré : le type de BD et le serveur sont requis.");
124
+ if (tempConfig.connectionMode === 'structured') {
125
+ if (!tempConfig.dbType || !tempConfig.server) {
126
+ throw new Error("En mode structuré, le type de base de données et le serveur sont requis.");
127
+ }
131
128
  let driver;
132
129
  let parts = [];
133
130
  switch (tempConfig.dbType) {
@@ -140,12 +137,12 @@ module.exports = function (RED) {
140
137
  parts.push(`SERVER=${tempConfig.server}`);
141
138
  if (tempConfig.database) parts.push(`DATABASE=${tempConfig.database}`);
142
139
  if (tempConfig.user) parts.push(`UID=${tempConfig.user}`);
143
- if (tempCredentials.password) parts.push(`PWD=${tempCredentials.password}`);
140
+ if (tempConfig.password) parts.push(`PWD=${tempConfig.password}`);
144
141
  return parts.join(';');
145
142
  } else {
146
143
  let connStr = tempConfig.connectionString || "";
147
- if (tempCredentials.password && connStr.includes('{{{password}}}')) {
148
- connStr = connStr.replace('{{{password}}}', tempCredentials.password);
144
+ if (!connStr) {
145
+ throw new Error("La chaîne de connexion ne peut pas être vide.");
149
146
  }
150
147
  return connStr;
151
148
  }
@@ -154,13 +151,14 @@ module.exports = function (RED) {
154
151
  let connection;
155
152
  try {
156
153
  const testConnectionString = buildTestConnectionString();
157
- if (!testConnectionString) return res.status(400).send("La chaîne de connexion est vide.");
158
154
  connection = await odbcModule.connect(testConnectionString);
159
155
  res.sendStatus(200);
160
156
  } catch (err) {
161
157
  res.status(500).send(err.message || "Erreur inconnue durant le test.");
162
158
  } finally {
163
- if (connection) await connection.close();
159
+ if (connection) {
160
+ await connection.close();
161
+ }
164
162
  }
165
163
  });
166
164
 
@@ -304,8 +302,6 @@ module.exports = function (RED) {
304
302
  };
305
303
 
306
304
  this.runQuery = async (msg, send, done) => {
307
- let currentQueryString = this.config.query || "";
308
- let currentQueryParams = msg.parameters;
309
305
  let isPreparedStatement = false;
310
306
  let connectionFromPool = null;
311
307
 
@@ -313,6 +309,27 @@ module.exports = function (RED) {
313
309
  this.status({ fill: "blue", shape: "dot", text: "preparing..." });
314
310
  this.config.outputObj = msg?.output || this.config?.outputObj || "payload";
315
311
 
312
+ const querySourceType = this.config.querySourceType || 'msg';
313
+ const querySource = this.config.querySource || 'query';
314
+ const paramsSourceType = this.config.paramsSourceType || 'msg';
315
+ const paramsSource = this.config.paramsSource || 'parameters';
316
+
317
+ let currentQueryParams = await new Promise((resolve, reject) => {
318
+ RED.util.evaluateNodeProperty(paramsSource, paramsSourceType, this, msg, (err, value) => {
319
+ if (err) { resolve(undefined); }
320
+ else { resolve(value); }
321
+ });
322
+ });
323
+
324
+ let currentQueryString = await new Promise((resolve, reject) => {
325
+ RED.util.evaluateNodeProperty(querySource, querySourceType, this, msg, (err, value) => {
326
+ if (err) { resolve(undefined); }
327
+ else { resolve(value || this.config.query || ""); }
328
+ });
329
+ });
330
+
331
+ if (!currentQueryString) { throw new Error("No query to execute"); }
332
+
316
333
  isPreparedStatement = currentQueryParams || (currentQueryString && currentQueryString.includes("?"));
317
334
  if (!isPreparedStatement && currentQueryString) {
318
335
  for (const parsed of mustache.parse(currentQueryString)) {
@@ -322,8 +339,6 @@ module.exports = function (RED) {
322
339
  }
323
340
  currentQueryString = mustache.render(currentQueryString, msg);
324
341
  }
325
- if (msg?.query) { currentQueryString = msg.query; }
326
- if (!currentQueryString) { throw new Error("No query to execute"); }
327
342
 
328
343
  const execute = async (conn) => {
329
344
  if (this.config.streaming) {
@@ -335,7 +350,7 @@ module.exports = function (RED) {
335
350
  if(done) done();
336
351
  }
337
352
  };
338
-
353
+
339
354
  let firstAttemptError = null;
340
355
  try {
341
356
  connectionFromPool = await this.poolNode.connect();
@@ -418,7 +433,7 @@ module.exports = function (RED) {
418
433
  if (this.poolNode && this.poolNode.config.retryOnMsg) {
419
434
  this.log("New message received, overriding retry timer and attempting query now.");
420
435
  clearTimeout(this.retryTimer);
421
- this.retryTimer = null;
436
+ this.retryTimer = null;
422
437
  this.isAwaitingRetry = false;
423
438
  } else {
424
439
  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.0.2",
3
+ "version": "2.1.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",