@bkmj/node-red-contrib-odbcmj 1.2.4 → 1.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.
Files changed (4) hide show
  1. package/README.md +58 -55
  2. package/odbc.html +241 -200
  3. package/odbc.js +263 -216
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -1,39 +1,38 @@
1
1
  # node-red-contrib-odbcmj
2
2
 
3
- A Node Red implementation of odbc.js (https://www.npmjs.com/package/odbc). This Node allows to make queries to a database through an ODBC connection. Additionally, parameters can be passed to the SQL query using Mustache syntax or prepared statements.
3
+ A Node-RED implementation of odbc.js (https://www.npmjs.com/package/odbc). This node allows you to make queries to a database through an ODBC connection. Additionally, parameters can be passed to the SQL query using Mustache syntax, prepared statements, or directly in the query string.
4
4
 
5
5
  ---
6
- ## Acknowledgement
6
+ ## Acknowledgment
7
7
 
8
- This node is an unofficial fork of node-red-contrib-odbc from Mark Irish (https://github.com/markdirish/node-red-contrib-odbc) and vastly inspires from it. It also takes ideas from node-red-contrib-odbc2 from AIS Automation (https://github.com/AISAutomation/node-red-contrib-odbc2). Overall changes:
8
+ This node is an unofficial fork of node-red-contrib-odbc by Mark Irish (https://github.com/markdirish/node-red-contrib-odbc) and is vastly inspired by it. It also takes ideas from node-red-contrib-odbc2 by AIS Automation (https://github.com/AISAutomation/node-red-contrib-odbc2).
9
9
 
10
- - Can use mustache as well as a parameter array.
11
- - Warnings when mustache will render an undefined variable.
12
- - Fixes the output field option so that nested object can be used.
13
- - Fixes the checkbox for the pool shrink option.
14
- - Uses ace/mode/sql for the SQL input field.
15
- - Connection nodes can have individually defined names
16
- - Selectable SQL syntax checker
10
+ **Overall changes:**
17
11
 
12
+ * Can use Mustache as well as a parameter array.
13
+ * Warnings when Mustache will render an undefined variable.
14
+ * Fixes the output field option so that nested objects can be used.
15
+ * Fixes the checkbox for the pool shrink option.
16
+ * Uses ace/mode/sql for the SQL input field.
17
+ * Connection nodes can have individually defined names.
18
+ * Selectable SQL syntax checker.
19
+ * Allows parameters to be passed as an object, mapping values to named parameters in the query.
18
20
 
19
21
  ## Installation
20
22
 
21
- This package is not available from within the Node Red palette tool. Instead, in your Node-RED user directory (usually ~/.node-red/), download through the `npm` utility:
23
+ This package is not available from within the Node-RED palette tool. Instead, in your Node-RED user directory (usually `~/.node-red/`), download through the `npm` utility:
22
24
  ```
23
25
  npm install node-red-contrib-odbcmj
24
26
  ```
25
27
 
26
28
  For the `odbc` connector requirements, please see [the documentation for that package](https://www.npmjs.com/package/odbc#requirements).
27
29
 
28
-
29
30
  ## Usage
30
31
 
31
32
  `node-red-contrib-odbcmj` provides two nodes:
32
33
 
33
- * **`odbc config`**: A configuration node for defining your connection string and managing your connection parameters
34
-
35
- * **`odbc`**: A node for running queries with or without parameters passed using Mustache syntax or using a parameter array.
36
-
34
+ * **`odbc config`**: A configuration node for defining your connection string and managing your connection parameters.
35
+ * **`odbc`**: A node for running queries with or without parameters passed using Mustache syntax, a parameter array, or a parameter object.
37
36
 
38
37
  ### `odbc config`
39
38
 
@@ -43,43 +42,45 @@ A configuration node that manages connections in an `odbc.pool` object. [Can tak
43
42
 
44
43
  * (**required**) **`connectionString`**: <`string`>
45
44
 
46
- An ODBC connection string that defines your DSN and/or connection string options. Check your ODBC driver documentation for more information about valid connection strings.
45
+ An ODBC connection string that defines your DSN and/or connection string options. Check your ODBC driver documentation for more information about valid connection strings.
46
+
47
+ Example:
48
+ ```
49
+ DSN=MyDSN;DFT=2;
50
+ ```
47
51
 
48
- Example:
49
- ```
50
- DSN=MyDSN;DFT=2;
51
- ```
52
52
  * (optional) **`initialSize`**: <`number`>
53
53
 
54
- The number of connections created in the Pool when it is initialized. Default: 5.
54
+ The number of connections created in the pool when it is initialized. Default: 5.
55
55
 
56
56
  * (optional) **`incrementSize`**: <`number`>
57
57
 
58
- The number of connections that are created when the pool is exhausted. Default: 5.
58
+ The number of connections that are created when the pool is exhausted. Default: 5.
59
59
 
60
60
  * (optional) **`maxSize`**: <`number`>
61
61
 
62
- The maximum number of connections allowed in the pool before it won't create any more. Default: 15.
62
+ The maximum number of connections allowed in the pool before it won't create any more. Default: 15.
63
63
 
64
64
  * (optional) **`shrinkPool`**: <`boolean`>
65
65
 
66
- Whether the number of connections should be reduced to `initialSize` when they are returned to the pool. Default: true.
66
+ Whether the number of connections should be reduced to `initialSize` when they are returned to the pool. Default: true.
67
67
 
68
68
  * (optional) **`connectionTimeout`**: <`number`>
69
69
 
70
- The number of seconds for a connection to remain idle before closing. Default: 3.
70
+ The number of seconds for a connection to remain idle before closing. Default: 3.
71
71
 
72
72
  * (optional) **`loginTimeout`**: <`number`>
73
73
 
74
- The number of seconds for an attempt to create a connection before returning to the application. Default: 3.
74
+ The number of seconds for an attempt to create a connection before returning to the application. Default: 3.
75
75
 
76
76
  * (optional) **`syntaxChecker`**: <`boolean`>
77
77
 
78
- Whether the syntax validator is activeted or not. If activated, the query string will be [parsed](https://www.npmjs.com/package/node-sql-parser#create-ast-for-sql-statement) and appended as an object to the output message with a key named `parsedSql`. Default: false.
78
+ Whether the syntax validator is activated or not. If activated, the query string will be [parsed](https://www.npmjs.com/package/node-sql-parser#create-ast-for-sql-statement) and appended as an object to the output message with a key named `parsedSql`. Default: false.
79
79
 
80
80
  * (optional) **`syntax`**: <`string`>
81
81
 
82
- Dropdown list of the available [SQL flavors available](https://www.npmjs.com/package/node-sql-parser#supported-database-sql-syntax). Default: mysql.
82
+ Dropdown list of the available [SQL flavors available](https://www.npmjs.com/package/node-sql-parser#supported-database-sql-syntax). Default: mysql.
83
+
83
84
 
84
85
  ### `odbc`
85
86
 
@@ -89,46 +90,48 @@ A node that runs a query when input is received. Each instance of the node can d
89
90
 
90
91
  * (**required**) **`connection`**: <`odbc config`>
91
92
 
92
- The ODBC pool node that defines the connection settings and manages the connection pool used by this node.
93
+ The ODBC pool node that defines the connection settings and manages the connection pool used by this node.
93
94
 
94
- * (optional) **`query`**: <`string`>
95
+ * (optional) **`queryType`**: <`string`>
95
96
 
96
- A valid SQL query string that can optionally contains parameters inserted using the Mustache syntax. For exemple, msg.payload can be inserted anywhere in the string using triple curly brackets: `{{{payload}}}`. The node will accept a query that is passed either as msg.query, msg.payload.query or msg.payload if payload is a stringified JSON containing a query key/value pair. A query string passed from the input will override any query defined in the node properties. Mustache syntax cannot be used with a query string passed from the input.
97
+ Selects the type of query to execute. Options are:
98
+ * `query`: A regular SQL query. Parameters can be passed using Mustache templating or an array in `msg.parameters`.
99
+ * `statement`: A prepared statement. Requires `msg.parameters`.
97
100
 
98
- Alternatively, the query string can be constructed as a prepared statement; that is with variables replaced by question marks: `SELECT * FROM test WHERE id = ?`. The variables must then be passed to the input using `msg.parameters`. This object must be an array containing the same number of element that there are `?` in the query string. The parameters are inserted one by one, from left to right.
101
+ * (optional) **`query`**: <`string`>
99
102
 
100
- * (**required**) **`result to`**: <`dot-notation string`>
103
+ A valid SQL query string.
104
+ * For `queryType: "query"`, it can contain parameters inserted using Mustache syntax (e.g., `{{{payload}}}`). You can also use placeholders (`?`) and provide an array of values in `msg.parameters`.
105
+ * For `queryType: "statement"`, it should use placeholders (`?`) for parameters.
101
106
 
102
- The JSON nested element structure that will contain the result output. The string must be a valid JSON object structure using dot-notation, minus the `msg.` (ex: `payload.results`) and must not start or end with a period. Square braquet notation is not allowed. The node input object is carried out to the output, as long as the output object name does not conflict it. If the targeted output JSON object was already present in the input, the result from the query will be appended to it if it was itself an object (but not an array), otherwise the original key/value pair will be overwritten.
107
+ * (**required**) **`result to`**: <`dot-notation string`>
103
108
 
104
- Example:
109
+ The JSON nested element structure that will contain the result output. The string must be a valid JSON object structure using dot-notation, minus the `msg.` (e.g., `payload.results`) and must not start or end with a period. Square bracket notation is not allowed. The node input object is carried out to the output, as long as the output object name does not conflict with it. If the targeted output JSON object was already present in the input, the result from the query will be appended to it if it was itself an object (but not an array); otherwise, the original key/value pair will be overwritten.
105
110
 
106
- - `input msg: {"payload": {"result": {"othervalue": 10} } };`
111
+ Example:
107
112
 
108
- - `result to: payload.results.values`
113
+ * `input msg: {"payload": {"result": {"othervalue": 10} } };`
114
+ * `result to: payload.results.values`
109
115
 
110
- In this case, `values` will be appended to `result` wihout overwriting `othervalue`. If `result` had been a string, then it would have replaced by `values`.
116
+ In this case, `values` will be appended to `result` without overwriting `othervalue`. If `result` had been a string, then it would have been replaced by `values`.
111
117
 
112
118
  #### Inputs
113
119
 
114
- The `odbc` node accepts a message input that is either:
115
-
116
- - a `payload` that contains a valid JSON string which itself contains a nested `query` key/value pair where the value is the SQL string. Ex: `msg.payload = "{'query':'<sql string>'}"`
117
- - a `payload` object with a nested `query` key/value pair where the value is the SQL string. Ex: `msg.payload.query = '<sql string>'`
118
- - a `query` key/value pair where the value is the SQL string. Ex: `msg.query = '<sql string>'`
120
+ The `odbc` node accepts a message input that can contain:
119
121
 
120
- * (optional) **`parameters`** <`array`>:
121
-
122
- An array containing the same number of element that there are `?` in the query string. The array is optionnal only if there are no variables markers in the query string.
122
+ * **`query`**: <`string`> A valid SQL query string. This overrides the query defined in the node properties.
123
+ * **`payload`**:
124
+ * A JSON string containing a `query` property with the SQL string.
125
+ * An object with a `query` property containing the SQL string.
126
+ * **`parameters`**: <`array` or `object`>
127
+ * Required for prepared statements (`queryType: "statement"`).
128
+ * Can be an array of values or an object mapping parameter names to values.
129
+ * For regular queries (`queryType: "query"`) with placeholders (`?`), provide an array of values.
123
130
 
124
131
  #### Outputs
125
132
 
126
- Returns a message containing an `output object` matching the `result to` dot-notation string as described above.
127
-
128
- * **`output object`**: <`array`>
129
-
130
- The [`odbc` result array](https://www.npmjs.com/package/odbc#result-array) returned from the query.
131
-
132
- * **`odbc`**: <`object`>
133
+ Returns a message containing:
133
134
 
134
- Contains any key/value pairs that were in the original output and were the key was not an integer. The module odbc.js returns a [few useful parameters](https://www.npmjs.com/package/odbc#result-array) but these parameters are not part of the output array and are thus segregated into `msg.odbc`. This is to avoid potential issues if looping through the output array using `Object.entries`.
135
+ * **`output object`**: <`array`> The `odbc` result array returned from the query.
136
+ * **`odbc`**: <`object`> Contains additional information returned by the `odbc` module.
137
+ * **`parsedQuery`**: <`object`> (Optional) The parsed SQL query if the syntax checker is enabled.
package/odbc.html CHANGED
@@ -1,249 +1,290 @@
1
1
  <script type="text/javascript">
2
- RED.nodes.registerType('odbc config',{
3
- category: 'config',
4
- defaults: {
5
- connectionString: {value:"", required:true},
6
- name: {value:""},
7
- initialSize: {value:5},
8
- incrementSize: {value:5},
9
- maxSize: {value:15},
10
- shrink:{value:true},
11
- syntaxtick:{value:false},
12
- syntax: {value:"mysql"},
13
- connectionTimeout:{value:3},
14
- loginTimeout:{value:3}
15
- },
16
- label: function() {
17
- return this.name || 'odbc config';
18
- },
19
- oneditprepare: function() {
20
- $(".input-syntax").hide();
21
- $("#node-config-input-syntaxtick").change(function() {
22
- if (this.checked){
23
- $(".input-syntax").show();
24
- } else {
25
- $(".input-syntax").hide();
2
+ RED.nodes.registerType('odbc config',{
3
+ category: 'config',
4
+ defaults: {
5
+ connectionString: {value:"", required:true},
6
+ name: {value:""},
7
+ initialSize: {value:5},
8
+ incrementSize: {value:5},
9
+ maxSize: {value:15},
10
+ shrink:{value:true},
11
+ syntaxtick:{value:false},
12
+ syntax: {value:"mysql"},
13
+ connectionTimeout:{value:3},
14
+ loginTimeout:{value:3}
15
+ },
16
+ label: function() {
17
+ return this.name || 'odbc config';
18
+ },
19
+ oneditprepare: function() {
20
+ $(".input-syntax").hide();
21
+ $("#node-config-input-syntaxtick").change(function() {
22
+ if (this.checked){
23
+ $(".input-syntax").show();
24
+ } else {
25
+ $(".input-syntax").hide();
26
+ }
27
+ });
26
28
  }
27
- });
28
- }
29
- });
29
+ });
30
30
  </script>
31
31
 
32
32
  <script type="text/html" data-template-name="odbc config">
33
33
 
34
- <div class="form-row">
35
- <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
36
- <input type="text" id="node-config-input-name">
37
- </div>
38
-
39
- <div class="form-row">
40
- <label for="node-config-input-connectionString"><i class="icon-bookmark"></i>Connection String</label>
41
- <input type="text" id="node-config-input-connectionString" placeholder="DSN=...;">
42
- </div>
43
-
44
- <div class="form-row">
45
- <label for="node-config-input-initialSize"><i class="icon-bookmark"></i>Initial Size</label>
46
- <input type="number" id="node-config-input-initialSize" placeholder="5">
47
- </div>
48
-
49
- <div class="form-row">
50
- <label for="node-config-input-incrementSize"><i class="icon-bookmark"></i>Increment Size</label>
51
- <input type="number" id="node-config-input-incrementSize" placeholder="5">
52
- </div>
53
-
54
- <div class="form-row">
55
- <label for="node-config-input-maxSize"><i class="icon-bookmark"></i>Max Size</label>
56
- <input type="number" id="node-config-input-maxSize" placeholder="15">
57
- </div>
58
-
59
- <div class="form-row">
60
- <label for="node-config-input-shrink"><i class="icon-bookmark"></i>Shrink Pool</label>
61
- <input type="checkbox" id="node-config-input-shrink" style="margin-left:0px; vertical-align:top; width:auto !important;">
62
- </div>
63
-
64
- <div class="form-row">
65
- <label for="node-config-input-connectionTimeout"><i class="icon-bookmark"></i>Connection Timeout (sec)</label>
66
- <input type="number" id="node-config-input-connectionTimeout" placeholder="3">
67
- </div>
68
-
69
- <div class="form-row">
70
- <label for="node-config-input-loginTimeout"><i class="icon-bookmark"></i>Login Timeout (sec)</label>
71
- <input type="number" id="node-config-input-loginTimeout" placeholder="3">
72
- </div>
73
-
74
- <div class="form-row">
75
- <label for="node-config-input-syntaxtick" style="width: auto;"><i class="icon-bookmark"></i>Syntax Checker</label>
76
- <input type="checkbox" id="node-config-input-syntaxtick" style="display: inline-block; width: auto; vertical-align: top;">
77
- </div>
78
-
79
- <div class="form-row input-syntax">
80
- <label for=""><i class="icon-bookmark"></i> Syntax</label>
81
- <select id="node-config-input-syntax" style="width: 70%">
82
- <option value="bigquery">BigQuery</option>
83
- <option value="db2">DB2</option>
84
- <option value="hive">Hive</option>
85
- <option value="mariadb">MariaDB</option>
86
- <option value="mysql">Mysql</option>
87
- <option value="postgresql">PostgresQL</option>
88
- <option value="sqlite">Sqlite</option>
89
- <option value="transactsql">TransactSQL</option>
90
- <option value="flinksql">FlinkSQL</option>
91
- </select>
92
- </div>
34
+ <div class="form-row">
35
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
36
+ <input type="text" id="node-config-input-name">
37
+ </div>
38
+
39
+ <div class="form-row">
40
+ <label for="node-config-input-connectionString"><i class="icon-bookmark"></i>Connection String</label>
41
+ <input type="text" id="node-config-input-connectionString" placeholder="DSN=...;">
42
+ </div>
43
+
44
+ <div class="form-row">
45
+ <label for="node-config-input-initialSize"><i class="icon-bookmark"></i>Initial Size</label>
46
+ <input type="number" id="node-config-input-initialSize" placeholder="5">
47
+ </div>
48
+
49
+ <div class="form-row">
50
+ <label for="node-config-input-incrementSize"><i class="icon-bookmark"></i>Increment Size</label>
51
+ <input type="number" id="node-config-input-incrementSize" placeholder="5">
52
+ </div>
53
+
54
+ <div class="form-row">
55
+ <label for="node-config-input-maxSize"><i class="icon-bookmark"></i>Max Size</label>
56
+ <input type="number" id="node-config-input-maxSize" placeholder="15">
57
+ </div>
58
+
59
+ <div class="form-row">
60
+ <label for="node-config-input-shrink"><i class="icon-bookmark"></i>Shrink Pool</label>
61
+ <input type="checkbox" id="node-config-input-shrink" style="margin-left:0px; vertical-align:top; width:auto !important;">
62
+ </div>
63
+
64
+ <div class="form-row">
65
+ <label for="node-config-input-connectionTimeout"><i class="icon-bookmark"></i>Connection Timeout (sec)</label>
66
+ <input type="number" id="node-config-input-connectionTimeout" placeholder="3">
67
+ </div>
68
+
69
+ <div class="form-row">
70
+ <label for="node-config-input-loginTimeout"><i class="icon-bookmark"></i>Login Timeout (sec)</label>
71
+ <input type="number" id="node-config-input-loginTimeout" placeholder="3">
72
+ </div>
73
+
74
+ <div class="form-row">
75
+ <label for="node-config-input-syntaxtick" style="width: auto;"><i class="icon-bookmark"></i>Syntax Checker</label>
76
+ <input type="checkbox" id="node-config-input-syntaxtick" style="display: inline-block; width: auto; vertical-align: top;">
77
+ </div>
78
+
79
+ <div class="form-row input-syntax">
80
+ <label for=""><i class="icon-bookmark"></i> Syntax</label>
81
+ <select id="node-config-input-syntax" style="width: 70%">
82
+ <option value="bigquery">BigQuery</option>
83
+ <option value="db2">DB2</option>
84
+ <option value="hive">Hive</option>
85
+ <option value="mariadb">MariaDB</option>
86
+ <option value="mysql">Mysql</option>
87
+ <option value="postgresql">PostgresQL</option>
88
+ <option value="sqlite">Sqlite</option>
89
+ <option value="transactsql">TransactSQL</option>
90
+ <option value="flinksql">FlinkSQL</option>
91
+ </select>
92
+ </div>
93
93
  </script>
94
94
 
95
95
  <script type="text/javascript">
96
- RED.nodes.registerType('odbc',{
97
- category: 'storage',
98
- color: '#89A5C0',
99
- defaults: {
100
- name: {value:""},
101
- connection: {type:"odbc config", required:true},
102
- query: {value: ""},
103
- parameters: {value: ""},
104
- outputObj: {value:"payload"}
105
- },
106
- inputs:1,
107
- outputs:1,
108
- icon: "db.svg",
109
- label: function() {
110
- return this.name||"odbc";
111
- },
112
- oneditprepare: function() {
113
- this.editor = RED.editor.createEditor({
114
- id: 'node-input-query-editor',
115
- mode: 'ace/mode/sql',
116
- value: this.query
117
- });
118
- },
119
- oneditsave: function() {
120
- this.query = this.editor.getValue();
121
- this.editor.destroy();
122
- delete this.editor;
123
- },
124
- oneditcancel: function() {
125
- this.editor.destroy();
126
- delete this.editor;
127
- }
96
+ RED.nodes.registerType('odbc',{
97
+ category: 'storage',
98
+ color: '#89A5C0',
99
+ defaults: {
100
+ name: {value:""},
101
+ connection: {type:"odbc config", required:true},
102
+ queryType: { value: "query" },
103
+ query: {value: ""},
104
+ parameters: {value: ""},
105
+ outputObj: {value:"payload"}
106
+ },
107
+ inputs:1,
108
+ outputs:1,
109
+ icon: "db.svg",
110
+ label: function() {
111
+ return this.name||"odbc";
112
+ },
113
+ oneditprepare: function() {
114
+ this.editor = RED.editor.createEditor({
115
+ id: 'node-input-query-editor',
116
+ mode: 'ace/mode/sql',
117
+ value: this.query
118
+ });
119
+ $("#node-input-queryType").on("change", function() {
120
+ if ($(this).val() === "statement") {
121
+ $("#node-input-parameters").show();
122
+ } else {
123
+ $("#node-input-parameters").hide();
124
+ }
125
+ });
126
+
127
+ // Trigger the change event initially to set the visibility
128
+ $("#node-input-queryType").trigger("change");
129
+ },
130
+ oneditsave: function() {
131
+ this.query = this.editor.getValue();
132
+ this.editor.destroy();
133
+ delete this.editor;
134
+ },
135
+ oneditcancel: function() {
136
+ this.editor.destroy();
137
+ delete this.editor;
138
+ }
128
139
  });
129
140
  </script>
130
141
 
131
142
  <script type="text/html" data-template-name="odbc">
132
143
 
133
- <div class="form-row">
134
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
135
- <input type="text" id="node-input-name">
136
- </div>
137
-
138
- <div class="form-row">
139
- <label for="node-input-connection"><i class="fa fa-cog"></i> Connection</label>
140
- <input type="text" id="node-input-connection">
141
- </div>
142
-
143
- <div class="form-row node-text-editor-row">
144
- <label for="node-input-query" style="width: 100% !important;"><i class="fa fa-search"></i> Query</label>
145
- <div style="height: 250px;" class="node-text-editor" id="node-input-query-editor" ></div>
146
- </div>
147
- <div class="form-row">
148
- <label for="node-input-outputObj"><i class="fa fa-edit"></i> Result to</label>
149
- <span>msg.</span><input type="text" id="node-input-outputObj" placeholder="payload" style="width: 64%;">
150
- </div>
144
+ <div class="form-row">
145
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
146
+ <input type="text" id="node-input-name">
147
+ </div>
148
+
149
+ <div class="form-row">
150
+ <label for="node-input-connection"><i class="fa fa-cog"></i> Connection</label>
151
+ <input type="text" id="node-input-connection">
152
+ </div>
153
+
154
+ <div class="form-row">
155
+ <label for="node-input-queryType"><i class="fa fa-code"></i> Query Type</label>
156
+ <select id="node-input-queryType">
157
+ <option value="query">Query</option>
158
+ <option value="statement">Prepared Statement</option>
159
+ </select>
160
+ </div>
161
+
162
+ <div class="form-row node-text-editor-row">
163
+ <label for="node-input-query" style="width: 100% !important;"><i class="fa fa-search"></i> Query</label>
164
+ <div style="height: 250px;" class="node-text-editor" id="node-input-query-editor" ></div>
165
+ </div>
166
+
167
+ <div class="form-row" id="node-input-parameters">
168
+ <label for="node-input-parameters-editor"><i class="fa fa-list"></i> Parameters</label>
169
+ <input type="text" id="node-input-parameters-editor">
170
+ </div>
171
+
172
+ <div class="form-row">
173
+ <label for="node-input-outputObj"><i class="fa fa-edit"></i> Result to</label>
174
+ <span>msg.</span><input type="text" id="node-input-outputObj" placeholder="payload" style="width: 64%;">
175
+ </div>
151
176
  </script>
152
-
153
177
  <script type="text/markdown" data-help-name="odbc config">
154
- A configuration node that manages connections in an `odbc.pool` object. [Can take any configuration property recognized by `odbc.pool()`](https://www.npmjs.com/package/odbc/v/2.4.8#constructor-odbcconnectconnectionstring). The connection pool will initialize the first time an `odbc` node receives an input message.
155
-
156
- ## Properties
157
-
158
- * (**required**) **`connectionString`**: <`string`>
159
-
160
- An ODBC connection string that defines your DSN and/or connection string options. Check your ODBC driver documentation for more information about valid connection strings.
161
-
178
+ A configuration node that manages connections in an `odbc.pool` object.
179
+ [Can take any configuration property recognized by `odbc.pool()`](https://www.npmjs.com/package/odbc/v/2.4.8#constructor-odbcconnectconnectionstring).
180
+ The connection pool will initialize the first time an `odbc` node receives an input message.
181
+
182
+ ## Properties
183
+
184
+ * (**required**) **`connectionString`**: <`string`>
185
+
186
+ An ODBC connection string that defines your DSN and/or connection string options.
187
+ Check your ODBC driver documentation for more information about valid connection strings.
188
+
162
189
  Example:
163
190
  ```
164
191
  DSN=MyDSN;DFT=2;
165
192
  ```
166
- * (optional) **`initialSize`**: <`number`>
167
-
168
- The number of connections created in the Pool when it is initialized. Default: 5.
169
-
170
- * (optional) **`incrementSize`**: <`number`>
171
-
193
+
194
+ * (optional) **`initialSize`**: <`number`>
195
+
196
+ The number of connections created in the pool when it is initialized. Default: 5.
197
+
198
+ * (optional) **`incrementSize`**: <`number`>
199
+
172
200
  The number of connections that are created when the pool is exhausted. Default: 5.
173
-
174
- * (optional) **`maxSize`**: <`number`>
175
-
201
+
202
+ * (optional) **`maxSize`**: <`number`>
203
+
176
204
  The maximum number of connections allowed in the pool before it won't create any more. Default: 15.
177
-
178
- * (optional) **`shrinkPool`**: <`boolean`>
179
-
205
+
206
+ * (optional) **`shrinkPool`**: <`boolean`>
207
+
180
208
  Whether the number of connections should be reduced to `initialSize` when they are returned to the pool. Default: true.
181
-
182
- * (optional) **`connectionTimeout`**: <`number`>
183
-
184
- The number of seconds for a connection to remain idle before closing. Default: 3.
185
-
186
- * (optional) **`loginTimeout`**: <`number`>
187
-
209
+
210
+ * (optional) **`connectionTimeout`**: <`number`>
211
+
212
+ The number of seconds for a connection to remain idle before closing. Default: 3.
213
+
214
+ * (optional) **`loginTimeout`**: <`number`>
215
+
188
216
  The number of seconds for an attempt to create a connection before returning to the application. Default: 3.
189
-
190
- * (optional) **`syntaxChecker`**: <`boolean`>
191
-
192
- Whether the syntax validator is activeted or not. If activated, the query string will be [parsed](https://www.npmjs.com/package/node-sql-parser#create-ast-for-sql-statement) and appended as an object to the output message with a key named `parsedQuery`. Default: false.
193
-
194
- * (optional) **`syntax`**: <`string`>
195
-
196
- Dropdown list of the available [SQL flavors available](https://www.npmjs.com/package/node-sql-parser#supported-database-sql-syntax). Default: mysql.
217
+
218
+ * (optional) **`syntaxChecker`**: <`boolean`>
219
+
220
+ Whether the syntax validator is activated or not. If activated, the query string will be
221
+ [parsed](https://www.npmjs.com/package/node-sql-parser#create-ast-for-sql-statement)
222
+ and appended as an object to the output message with a key named `parsedQuery`. Default: false.
223
+
224
+ * (optional) **`syntax`**: <`string`>
225
+
226
+ Dropdown list of the available [SQL flavors available](https://www.npmjs.com/package/node-sql-parser#supported-database-sql-syntax).
227
+ Default: mysql.
197
228
  </script>
198
229
 
199
230
  <script type="text/markdown" data-help-name="odbc">
200
- A node that runs a query when input is received. Each instance of the node can define its own query string, as well as take a query and/or parameters as input. A query sent as an input message will override any query defined in the node properties.
231
+ A node that runs a query when input is received. Each instance of the node can define its own query string,
232
+ as well as take a query and/or parameters as input. A query sent as an input message will override any query
233
+ defined in the node properties.
201
234
 
202
235
  ## Properties
203
236
 
204
237
  * (**required**) **`connection`**: <`odbc config`>
205
238
 
206
- The ODBC pool node that defines the connection settings and manages the connection pool used by this node.
239
+ The ODBC pool node that defines the connection settings and manages the connection pool used by this node.
207
240
 
208
- * (optional) **`query`**: <`string`>
241
+ * (optional) **`queryType`**: <`string`>
242
+
243
+ Selects the type of query to execute. Options are:
244
+ * `query`: A regular SQL query. Parameters can be passed using Mustache templating, a parameter array in `msg.parameters`, or directly in the query string.
245
+ * `statement`: A prepared statement (requires `msg.parameters`).
209
246
 
210
- A valid SQL query string that can optionally contains parameters inserted using the Mustache syntax. For exemple, msg.payload can be inserted anywhere in the string using triple curly brackets: `{{{payload}}}`. The node will accept a query that is passed either as msg.query, msg.payload.query or msg.payload if payload is a stringified JSON containing a query key/value pair. A query string passed from the input will override any query defined in the node properties. Mustache syntax cannot be used with a query string passed from the input.
247
+ * (optional) **`query`**: <`string`>
211
248
 
212
- Alternatively, the query string can be constructed as a prepared statement; that is with variables replaced by question marks: `SELECT * FROM test WHERE id = ?`. The variables must then be passed to the input using `msg.parameters`. This object must be an array containing the same number of element that there are `?` in the query string. The parameters are inserted one by one, from left to right.
249
+ A valid SQL query string.
250
+ * For `queryType: "query"`, it can contain parameters inserted using Mustache syntax (e.g., `{{{payload}}}`). You can also use placeholders (`?`) and provide an array of values in `msg.parameters`, or embed the parameters directly in the query string.
251
+ * For `queryType: "statement"`, it should use placeholders (`?`) for parameters.
213
252
 
214
253
  * (**required**) **`result to`**: <`dot-notation string`>
215
254
 
216
- The JSON nested element structure that will contain the result output. The string must be a valid JSON object structure using dot-notation, minus the `msg.` (ex: `payload.results`) and must not start or end with a period. Square braquet notation is not allowed. The node input object is carried out to the output, as long as the output object name does not conflict it. If the targeted output JSON object was already present in the input, the result from the query will be appended to it if it was itself an object (but not an array), otherwise the original key/value pair will be overwritten.
217
-
218
- Example:
255
+ The JSON nested element structure that will contain the result output. The string must be a valid
256
+ JSON object structure using dot-notation, minus the `msg.` (e.g., `payload.results`) and must not
257
+ start or end with a period. Square bracket notation is not allowed. The node input object is carried
258
+ out to the output, as long as the output object name does not conflict with it. If the targeted output
259
+ JSON object was already present in the input, the result from the query will be appended to it if it
260
+ was itself an object (but not an array); otherwise, the original key/value pair will be overwritten.
219
261
 
220
- - `input msg: {"payload": {"result": {"othervalue": 10} } };`
262
+ Example:
221
263
 
222
- - `result to: payload.results.values`
264
+ * `input msg: {"payload": {"result": {"othervalue": 10} } };`
265
+ * `result to: payload.results.values`
223
266
 
224
- In this case, `values` will be appended to `result` wihout overwriting `othervalue`. If `result` had been a string, then it would have replaced by `values`.
267
+ In this case, `values` will be appended to `result` without overwriting `othervalue`.
268
+ If `result` had been a string, then it would have been replaced by `values`.
225
269
 
226
270
  ## Inputs
227
271
 
228
- The `odbc` node accepts a message input that is either:
272
+ The `odbc` node accepts a message input that can contain:
229
273
 
230
- - a `payload` that contains a valid JSON string which itself contains a nested `query` key/value pair where the value is the SQL string. Ex: `msg.payload = "{'query':'<sql string>'}"`
231
- - a `payload` object with a nested `query` key/value pair where the value is the SQL string. Ex: `msg.payload.query = '<sql string>'`
232
- - a `query` key/value pair where the value is the SQL string. Ex: `msg.query = '<sql string>'`
233
-
234
- * (optional) **`parameters`** <`array`>:
235
-
236
- An array containing the same number of element that there are `?` in the query string. The array is optionnal only if there are no variables markers in the query string.
274
+ * **`query`**: <`string`> A valid SQL query string. This overrides the query defined in the node properties.
275
+ * **`payload`**:
276
+ * A JSON string containing a `query` property with the SQL string.
277
+ * An object with a `query` property containing the SQL string.
278
+ * **`parameters`**: <`array` or `object`>
279
+ * Required for prepared statements (`queryType: "statement"`).
280
+ * Can be an array of values or an object mapping parameter names to values.
281
+ * For regular queries (`queryType: "query"`) with placeholders (`?`), provide an array of values.
237
282
 
238
283
  ## Outputs
239
284
 
240
- Returns a message containing an `output object` matching the `result to` dot-notation string as described above.
241
-
242
- * **`output object`**: <`array`>
285
+ Returns a message containing:
243
286
 
244
- The [`odbc` result array](https://www.npmjs.com/package/odbc#result-array) returned from the query.
245
-
246
- * **`odbc`**: <`object`>
247
-
248
- Contains any key/value pairs that were in the original output and were the key was not an integer. The module odbc.js returns a [few useful parameters](https://www.npmjs.com/package/odbc#result-array) but these parameters are not part of the output array and are thus segregated into `msg.odbc`. This is to avoid potential issues if looping through the output array using `Object.entries`.
249
- </script>
287
+ * **`output object`**: <`array`> The `odbc` result array returned from the query.
288
+ * **`odbc`**: <`object`> Contains additional information returned by the `odbc` module.
289
+ * **`parsedQuery`**: <`object`> (Optional) The parsed SQL query if the syntax checker is enabled.
290
+ </script>
package/odbc.js CHANGED
@@ -1,223 +1,270 @@
1
1
  module.exports = function(RED) {
2
- const odbcModule = require('odbc');
3
- const mustache = require('mustache');
4
- const objPath = require('object-path');
5
- //----poolConfig is the function associated to the odbc config node.
6
- function poolConfig(config){
7
- RED.nodes.createNode(this, config);
8
- this.config = config;
9
- this.pool = null;
10
- this.connecting = false;
11
- const thick = this.config.syntaxtick;
12
- const syntax = this.config.syntax;
13
- delete this.config.syntaxtick;
14
- delete this.config.syntax;
15
- //----Check if the user ticked the syntax checker and if so we create a parser object based on the specified syntax.
16
- this.parser = ((thick) => {
17
- if(thick){
18
- const { Parser } = require('node-sql-parser/build/' + syntax);
19
- return new Parser();
20
- } else {return null}
21
- })(thick)
22
- //----Converting numeric configuration parameters to integers instead of strings.
23
- for (let [key,value] of Object.entries(this.config)){
24
- if (!isNaN(parseInt(value))) this.config[key] = parseInt(value);
2
+ const odbcModule = require('odbc');
3
+ const mustache = require('mustache');
4
+ const objPath = require('object-path');
5
+
6
+ // --- ODBC Configuration Node ---
7
+ function poolConfig(config) {
8
+ RED.nodes.createNode(this, config);
9
+ this.config = config;
10
+ this.pool = null;
11
+ this.connecting = false;
12
+
13
+ const enableSyntaxChecker = this.config.syntaxtick; // Renamed for clarity
14
+ const syntax = this.config.syntax;
15
+ delete this.config.syntaxtick;
16
+ delete this.config.syntax;
17
+
18
+ // Create a SQL parser if syntax check is enabled
19
+ this.parser = enableSyntaxChecker
20
+ ? new require('node-sql-parser/build/' + syntax).Parser()
21
+ : null;
22
+
23
+ // Convert numeric config params to integers
24
+ for (const [key, value] of Object.entries(this.config)) {
25
+ if (!isNaN(parseInt(value))) {
26
+ this.config[key] = parseInt(value);
27
+ }
28
+ }
29
+
30
+ // Connect to the database and create a connection pool
31
+ this.connect = async () => {
32
+ if (!this.pool) {
33
+ try {
34
+ this.pool = await odbcModule.pool(this.config);
35
+ this.connecting = false;
36
+ } catch (error) {
37
+ // Handle connection errors (e.g., log the error, set node status)
38
+ this.error(`Error creating connection pool: ${error}`);
39
+ this.status({ fill: "red", shape: "ring", text: "Connection error" });
40
+ throw error; // Re-throw to prevent further execution
41
+ }
42
+ }
43
+ return await this.pool.connect();
44
+ };
45
+
46
+ // Close the connection pool when the node is closed
47
+ this.on('close', async (removed, done) => {
48
+ if (removed && this.pool) {
49
+ try {
50
+ await this.pool.close();
51
+ } catch (error) {
52
+ // Handle errors during pool closure
53
+ this.error(`Error closing connection pool: ${error}`);
54
+ }
55
+ }
56
+ done();
57
+ });
25
58
  }
26
- this.connect = async () => {
27
- if (this.pool == null) { //----Request connection pool.
28
- this.pool = await odbcModule.pool(this.config);
29
- this.connecting = false;
30
- }
31
- let connection = await this.pool.connect();
32
- return connection;
33
- };
34
- this.on('close', async (removed, done) => {
35
- if (removed && this?.pool) { await this.pool.close() }
36
- done();
37
- })
38
- }
39
- //----Registering the odbc config node to the poolConfig function.
40
- RED.nodes.registerType('odbc config', poolConfig);
41
-
42
- //----odbc is the function associated to the odbc node.
43
- function odbc(config){
44
- RED.nodes.createNode(this, config);
45
- this.config = config;
46
- //----Retrieving the specific configuration node.
47
-
48
- this.poolNode = RED.nodes.getNode(this.config.connection);
49
- this.name = this.config.name;
50
-
51
- //The runQuery function handles the actual query to the database using odbc.js.
52
- this.runQuery = async function(msg, send, done) {
53
- try {
54
- this.status({fill:"blue",shape:"dot",text:"querying..."});
55
- //---If the output object was passed in the msg, it overwrites to output in the config.
56
- this.config.outputObj = msg?.output || this.config?.outputObj;
57
- //---Re-fetching queryString here since it can change because of Mustache even if the node itself doesn't change.
58
- this.queryString = this.config.query;
59
- if(this.queryString.length){
60
- //---Testing if all mustache tags were provided.
61
- for(var parsed of mustache.parse(this.queryString)){
62
- if(parsed[0] == "name" || parsed[0] == "&"){
63
- if(!objPath.has(msg, parsed[1])){
64
- this.warn(`mustache parameter "${parsed[1]}" is absent and will render to undefined`);
65
- }
66
- }
67
- }
68
- //---Replace the mustaches tags with the appropriate values from the input msg.
69
- this.queryString = mustache.render(this.queryString, msg);
70
- }
71
- //----Case were the query is in the message, not in the payload.
72
- if (msg?.query) this.queryString = msg.query || this.queryString
73
- //---Handles the case were the received message is a stringified JSON containing a valid query.
74
- else if (msg?.payload) {
75
- if (typeof msg.payload == 'string'){
76
- let payloadJson = null;
77
- try { payloadJson = JSON.parse(msg.payload) }
78
- catch (err) {}//---Do nothing, if the payload was not a JSON it will be handled below.
79
- //----If the payload, which is now equivalent to a non-stringified incoming message, contains a query string.
80
- if(typeof payloadJson == 'object'){
81
- if(payloadJson?.query){
82
- if(typeof payloadJson.query != 'string'){
83
- this.status({fill: "red", shape: "ring",text: "Invalid query"});
84
- throw new Error("object msg.payload.query must be a string");
59
+
60
+ RED.nodes.registerType('odbc config', poolConfig);
61
+
62
+ // --- ODBC Query Node ---
63
+ function odbc(config) {
64
+ RED.nodes.createNode(this, config);
65
+ this.config = config;
66
+ this.poolNode = RED.nodes.getNode(this.config.connection);
67
+ this.name = this.config.name;
68
+
69
+ this.runQuery = async function(msg, send, done) {
70
+ try {
71
+ this.status({ fill: "blue", shape: "dot", text: "querying..." });
72
+ this.config.outputObj = msg?.output || this.config?.outputObj;
73
+
74
+ const isPreparedStatement = this.config.queryType === "statement";
75
+ this.queryString = this.config.query;
76
+
77
+ // --- Construct the query string ---
78
+ if (!isPreparedStatement && this.queryString) {
79
+ // Handle Mustache templating for regular queries
80
+ for (const parsed of mustache.parse(this.queryString)) {
81
+ if (parsed[0] === "name" || parsed[0] === "&") {
82
+ if (!objPath.has(msg, parsed[1])) {
83
+ this.warn(`Mustache parameter "${parsed[1]}" is absent and will render to undefined`);
84
+ }
85
+ }
86
+ }
87
+ this.queryString = mustache.render(this.queryString, msg);
88
+ }
89
+
90
+ // Handle cases where the query is provided in the message
91
+ if (msg?.query) {
92
+ this.queryString = msg.query;
93
+ } else if (msg?.payload) {
94
+ if (typeof msg.payload === 'string') {
95
+ try {
96
+ const payloadJson = JSON.parse(msg.payload);
97
+ if (payloadJson?.query && typeof payloadJson.query === 'string') {
98
+ this.queryString = payloadJson.query;
99
+ }
100
+ } catch (err) {} // Ignore JSON parsing errors
101
+ } else if (msg.payload?.query && typeof msg.payload.query === 'string') {
102
+ this.queryString = msg.payload.query;
103
+ }
104
+ }
105
+
106
+ if (!this.queryString) {
107
+ throw new Error("No query to execute");
108
+ }
109
+
110
+ // --- Parameter validation for prepared statements ---
111
+ if (isPreparedStatement) {
112
+ if (this.queryString.includes('?')) {
113
+ if (!msg?.parameters) {
114
+ throw new Error("Prepared statement requires msg.parameters");
115
+ } else if (!Array.isArray(msg.parameters)) {
116
+ throw new Error("msg.parameters must be an array");
117
+ } else if ((this.queryString.match(/\?/g) || []).length !== msg.parameters.length) {
118
+ throw new Error("Incorrect number of parameters");
119
+ }
120
+ } else {
121
+ this.warn("Prepared statement without parameters. Consider using a regular query.");
122
+ }
123
+ }
124
+
125
+ // --- Syntax check ---
126
+ if (this.poolNode?.parser) {
127
+ try {
128
+ this.parseSql = this.poolNode.parser.astify(structuredClone(this.queryString));
129
+ } catch (error) {
130
+ throw new Error("SQL syntax error");
131
+ }
132
+ }
133
+
134
+ // --- Output object validation ---
135
+ if (!this.config.outputObj) {
136
+ throw new Error("Invalid output object definition");
137
+ }
138
+
139
+ const reg = new RegExp('^((?![,;:`\\[\\]{}+=()!"$%?&*|<>\\/^¨`\\s]).)*$');
140
+ if (!this.config.outputObj.match(reg) ||
141
+ this.config.outputObj.charAt(0) === "." ||
142
+ this.config.outputObj.charAt(this.config.outputObj.length - 1) === ".") {
143
+ throw new Error("Invalid output field");
144
+ }
145
+
146
+ // --- Get a connection from the pool ---
147
+ try {
148
+ this.connection = await this.poolNode.connect();
149
+ if (!this.connection) {
150
+ throw new Error("No connection available");
151
+ }
152
+ } catch (error) {
153
+ // Handle connection errors (e.g., log the error, set node status)
154
+ this.error(`Error getting connection: ${error}`);
155
+ this.status({ fill: "red", shape: "ring", text: "Connection error" });
156
+ throw error; // Re-throw to prevent further execution
157
+ }
158
+
159
+ try {
160
+ let result;
161
+ if (isPreparedStatement) {
162
+ const stmt = await this.connection.prepare(this.queryString);
163
+ let values = msg.parameters;
164
+
165
+ if (typeof values === 'object' && !Array.isArray(values)) {
166
+ // Assuming your parameters are like this: (param1, param2, param3)
167
+ const paramNames = this.queryString.match(/\(([^)]*)\)/)[1].split(',').map(el => el.trim());
168
+
169
+ // Create an ordered array of values
170
+ values = paramNames.map(name => msg.parameters[name]);
171
+ }
172
+
173
+ // Execute the prepared statement
174
+ const result = await stmt.execute(values);
175
+ stmt.finalize();
176
+ } else {
177
+ // --- Execute regular query ---
178
+ result = await this.connection.query(this.queryString, msg?.parameters);
179
+ }
180
+
181
+ if (result) {
182
+ // --- Process and send the result ---
183
+ const otherParams = {};
184
+ for (const [key, value] of Object.entries(result)) {
185
+ if (isNaN(parseInt(key))) {
186
+ otherParams[key] = value;
187
+ delete result[key];
188
+ }
189
+ }
190
+ objPath.set(msg, this.config.outputObj, result);
191
+ if (this.parseSql) {
192
+ msg.parsedQuery = this.parseSql;
193
+ }
194
+ if (Object.keys(otherParams).length) {
195
+ msg.odbc = otherParams;
196
+ }
197
+ this.status({ fill: 'green', shape: 'dot', text: 'success' });
198
+ send(msg);
199
+ } else {
200
+ throw new Error("The query returned no results");
201
+ }
202
+ } catch (error) {
203
+ // Handle query errors (e.g., log the error, set node status)
204
+ this.error(`Error executing query: ${error}`);
205
+ this.status({ fill: "red", shape: "ring", text: "Query error" });
206
+ throw error; // Re-throw to trigger the outer catch block
207
+ } finally {
208
+ await this.connection.close();
209
+ }
210
+
211
+ if (done) {
212
+ done();
213
+ }
214
+ } catch (err) {
215
+ this.status({ fill: "red", shape: "ring", text: err.message || "query error" });
216
+ if (done) {
217
+ done(err);
85
218
  } else {
86
- //---A query string coming from an input message as priority over the query defined in Node.
87
- this.queryString = payloadJson.query || this.queryString;
219
+ this.error(err, msg);
88
220
  }
89
- }
90
221
  }
91
- } else {
92
- if (msg?.payload?.query){
93
- if(typeof msg.payload.query != 'string'){
94
- this.status({fill: "red", shape: "ring",text: "Invalid query"});
95
- throw new Error("object msg.payload.query must be a string");
96
- } else { this.queryString = msg.payload.query || this.queryString }
222
+ };
223
+
224
+ // --- Check connection pool before running query ---
225
+ this.checkPool = async function(msg, send, done) {
226
+ try {
227
+ if (this.poolNode.connecting) {
228
+ this.warn("Waiting for connection pool...");
229
+ this.status({ fill: "yellow", shape: "ring", text: "requesting pool" });
230
+ setTimeout(() => {
231
+ this.checkPool(msg, send, done);
232
+ }, 1000);
233
+ return;
234
+ }
235
+
236
+ if (!this.poolNode.pool) {
237
+ this.poolNode.connecting = true;
238
+ }
239
+
240
+ await this.runQuery(msg, send, done);
241
+ } catch (err) {
242
+ this.status({ fill: "red", shape: "dot", text: "operation failed" });
243
+ if (done) {
244
+ done(err);
245
+ } else {
246
+ this.error(err, msg);
247
+ }
97
248
  }
98
- }
99
- }
100
- //----Case were the query is in the payload, not in the messsage.
101
-
102
- //----Case were there was no query pased to the node.
103
- if(!this.queryString){
104
- this.status({fill: "red", shape: "ring",text: "Invalid query"});
105
- throw new Error("no query to execute");
106
- }
107
- if(this.queryString.includes('\?') && !msg?.parameters){
108
- this.status({fill: "red", shape: "ring",text: "Invalid statement"});
109
- throw new Error("the query string includes ? markers but no msg.params was provided");
110
- }
111
- else if(this.queryString.includes('\?') && !Array.isArray(msg.parameters)){
112
- this.status({fill: "red", shape: "ring",text: "Invalid statement"});
113
- throw new Error("the query string includes question marks but msg.params is not an array");
114
- }
115
- if(this.queryString.includes('\?')){
116
- if((this.queryString.match(/\?/g) || []).length != msg.parameters.length){
117
- this.status({fill: "red", shape: "ring",text: "Invalid statement"});
118
- throw new Error("the number of parameters provided doesn't match the number of ? markers");
119
- }
120
- }
121
- //----Check if the query string is a syntaxically valid SQL query
122
- if(this.poolNode?.parser){
123
- let clone = structuredClone(this.queryString);
124
- try { this.parseSql = this.poolNode.parser.astify(clone) }
125
- catch (error) {
126
- this.status({fill: "red", shape: "ring",text: "Invalid query"});
127
- throw new Error("the query failed the sql syntax check");
128
- }
129
- }
130
- //---If no output object was specified.
131
- if (!this.config.outputObj){
132
- this.status({fill: "red", shape: "ring",text: "Invalid output field"});
133
- throw new Error("invalid output object definition");
134
- }
135
- //----If the output string contains illegal punctuation.
136
- const reg = new RegExp('^((?![,;:`\[\]{}+=()!"$%?&*|<>\/^¨`\s]).)*$')
137
- if (!this.config.outputObj.match(reg)){
138
- this.status({fill: "red", shape: "ring",text: "Invalid output field"});
139
- throw new Error("invalid output field");
140
- }
141
- //----If ouput string starts or ends with a period (usually because the user left one there inadvertently)
142
- if (this.config.outputObj.charAt(0) == "." || this.config.outputObj.charAt(this.config.outputObj.length-1) == "."){
143
- this.status({fill: "red", shape: "ring",text: "Invalid output field"});
144
- throw new Error("invalid output field");
145
- }
146
- //---Retrieving one connection from the pool.
147
- this.connection = null;
148
- try { this.connection = await this.poolNode.connect() }
149
- catch (error) {
150
- this.status({fill: "red", shape: "ring",text: "connection error"});
151
- throw new Error(error);
152
- }
153
- if(!this.connection){
154
- this.status({fill: "red", shape: "ring",text: "no connection"});
155
- throw new Error("no connection");
156
- }
157
- //----Actual attempt to send the query to the ODBC driver.
158
- try {
159
- const result = await this.connection.query(this.queryString, msg?.parameters);
160
- if(result){
161
- //----Sending the results to the defined dot notation object.
162
- let otherParams = {};
163
- for(const [key, value] of Object.entries(result)){
164
- if(isNaN(parseInt(key))){
165
- otherParams[key] = value;
166
- delete result[key];
167
- }
249
+ };
250
+
251
+ this.on('input', this.checkPool);
252
+
253
+ // --- Close the connection when the node is closed ---
254
+ this.on('close', async (done) => {
255
+ if (this.connection) {
256
+ try {
257
+ await this.connection.close();
258
+ } catch (error) {
259
+ // Handle connection close errors
260
+ this.error(`Error closing connection: ${error}`);
261
+ }
168
262
  }
169
- objPath.set(msg,this.config.outputObj,result);
170
- if(this?.parseSql) msg.parsedQuery = this.parseSql;
171
- if(Object.keys(otherParams).length) msg.odbc = otherParams;
172
- this.status({fill:'blue',shape:'dot',text:'finish'});
173
- send(msg);
174
- } else {
175
- this.status({fill: "red", shape: "ring",text: "query error"});
176
- throw new Error("the query returns a falsy");
177
- }
178
- } catch (error) {
179
- this.status({fill: "red", shape: "ring",text: "query error"});
180
- throw new Error(error);
181
- }
182
- finally {
183
- //----Close the connection.
184
- await this.connection.close();
185
- }
186
- if (done) done();
187
- } catch (err) {
188
- if (done) {done(err)} else {this.error(err, msg)}
189
- }
190
-
191
- };
192
- //----This function runs following an input message.
193
- this.checkPool = async function(msg, send, done) {
194
- //----Loops while other nodes are awaiting connections. See poolConfig.
195
- try {
196
- if (this.poolNode.connecting) {
197
- this.warn("received msg while requesting pool");
198
- this.status({fill: "yellow", shape: "ring",text: "requesting pool"});
199
- setTimeout(() => {
200
- this.checkPool(msg, send, done);
201
- }, 1000);
202
- return;
203
- }
204
- //----On initialization, the pool will be null. Set connecting to true so that other nodes are immediately blocked, then call runQuery (which will actually do the pool initialization)
205
- if (this.poolNode.pool == null) this.poolNode.connecting = true;
206
- //----If a connection is available, run the runQuery function.
207
- await this.runQuery(msg, send, done);
208
- } catch (err) {
209
- this.status({fill: "red",shape: "dot", text: "operation failed"});
210
- if (done) {done(err)} else {this.error(err, msg)}
211
- }
212
-
213
- };
214
- this.on('input', this.checkPool);
215
- this.on('close', async (done) => {
216
- if(this?.connection) await this.connection.close();
217
- done();
218
- })
219
- this.status({fill:'green',shape:'dot',text:'ready'});
220
- }
221
- //----Registering the odbc node to the odbc function.
222
- RED.nodes.registerType("odbc", odbc);
223
- };
263
+ done();
264
+ });
265
+
266
+ this.status({ fill: 'green', shape: 'dot', text: 'ready' });
267
+ }
268
+
269
+ RED.nodes.registerType("odbc", odbc);
270
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkmj/node-red-contrib-odbcmj",
3
- "version": "1.2.4",
3
+ "version": "1.3.1",
4
4
  "description": "Node Red implementation of odbc.js",
5
5
  "keywords": [
6
6
  "node-red",
@@ -10,9 +10,9 @@
10
10
  ],
11
11
  "dependencies": {
12
12
  "mustache": "^4.2.0",
13
- "node-sql-parser": "^4.17.0",
13
+ "node-sql-parser": "^5.3.5",
14
14
  "object-path": "^0.11.8",
15
- "odbc": "^2.4.8"
15
+ "odbc": "^2.4.9"
16
16
  },
17
17
  "node-red": {
18
18
  "nodes": {