@flowfuse/nr-tables-nodes 0.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.
@@ -0,0 +1,291 @@
1
+ [
2
+ {
3
+ "id": "36d7a2e7.38e4de",
4
+ "type": "inject",
5
+ "z": "6bd3da1a.7e2b84",
6
+ "name": "Start",
7
+ "props": [
8
+ {
9
+ "p": "payload"
10
+ },
11
+ {
12
+ "p": "topic",
13
+ "vt": "str"
14
+ }
15
+ ],
16
+ "repeat": "",
17
+ "crontab": "",
18
+ "once": false,
19
+ "onceDelay": 0.1,
20
+ "topic": "",
21
+ "payloadType": "date",
22
+ "x": 190,
23
+ "y": 1620,
24
+ "wires": [
25
+ [
26
+ "1b00f74dc3098005"
27
+ ]
28
+ ]
29
+ },
30
+ {
31
+ "id": "ee38d447.1c13a8",
32
+ "type": "debug",
33
+ "z": "6bd3da1a.7e2b84",
34
+ "name": "Done",
35
+ "active": true,
36
+ "tosidebar": true,
37
+ "console": false,
38
+ "tostatus": true,
39
+ "complete": "true",
40
+ "targetType": "full",
41
+ "statusVal": "complete",
42
+ "statusType": "msg",
43
+ "x": 1210,
44
+ "y": 1620,
45
+ "wires": []
46
+ },
47
+ {
48
+ "id": "12f229bfef5ad2a5",
49
+ "type": "function",
50
+ "z": "6bd3da1a.7e2b84",
51
+ "name": "Ready for next lines",
52
+ "func": "return [\n msg.complete || msg.abort ? msg : null,\n { tick: true },\n];\n",
53
+ "outputs": 2,
54
+ "noerr": 0,
55
+ "initialize": "",
56
+ "finalize": "",
57
+ "libs": [],
58
+ "x": 980,
59
+ "y": 1560,
60
+ "wires": [
61
+ [
62
+ "ee38d447.1c13a8"
63
+ ],
64
+ [
65
+ "1b00f74dc3098005"
66
+ ]
67
+ ]
68
+ },
69
+ {
70
+ "id": "178252a8d3c54b16",
71
+ "type": "function",
72
+ "z": "6bd3da1a.7e2b84",
73
+ "name": "",
74
+ "func": "let payload = `(0, FALSE),`;\nif (msg.payload && msg.payload.length > 0) {\n for (const line of msg.payload) {\n const valid = 'TRUE'; // Call some kind of test\n payload += `(${line['id']}, ${valid}),`;\n }\n}\nmsg.payload = payload.slice(0, - 1);\nreturn msg;\n",
75
+ "outputs": 1,
76
+ "noerr": 0,
77
+ "initialize": "",
78
+ "finalize": "",
79
+ "libs": [],
80
+ "x": 560,
81
+ "y": 1620,
82
+ "wires": [
83
+ [
84
+ "6d2073ec4db26f2f"
85
+ ]
86
+ ]
87
+ },
88
+ {
89
+ "id": "4fd30ba36702842a",
90
+ "type": "debug",
91
+ "z": "6bd3da1a.7e2b84",
92
+ "name": "Progress",
93
+ "active": true,
94
+ "tosidebar": true,
95
+ "console": false,
96
+ "tostatus": true,
97
+ "complete": "true",
98
+ "targetType": "full",
99
+ "statusVal": "parts.index",
100
+ "statusType": "msg",
101
+ "x": 940,
102
+ "y": 1620,
103
+ "wires": []
104
+ },
105
+ {
106
+ "id": "1b00f74dc3098005",
107
+ "type": "postgresql",
108
+ "z": "6bd3da1a.7e2b84",
109
+ "name": "SELECT many",
110
+ "query": "SELECT * FROM mytable\nORDER BY id ASC\nLIMIT 2000;\n",
111
+ "postgreSQLConfig": "20ae1e52d1eef983",
112
+ "split": true,
113
+ "rowsPerMsg": "100",
114
+ "outputs": 1,
115
+ "x": 380,
116
+ "y": 1620,
117
+ "wires": [
118
+ [
119
+ "178252a8d3c54b16"
120
+ ]
121
+ ]
122
+ },
123
+ {
124
+ "id": "6d2073ec4db26f2f",
125
+ "type": "postgresql",
126
+ "z": "6bd3da1a.7e2b84",
127
+ "name": "UPDATE many",
128
+ "query": "UPDATE mytable AS c\nSET validity = v.validity\nFROM (VALUES\n\t{{{ msg.payload }}}\n) AS v (id, validity)\nWHERE v.id = c.id;\n",
129
+ "postgreSQLConfig": "20ae1e52d1eef983",
130
+ "split": false,
131
+ "rowsPerMsg": "1",
132
+ "outputs": 1,
133
+ "x": 740,
134
+ "y": 1620,
135
+ "wires": [
136
+ [
137
+ "12f229bfef5ad2a5",
138
+ "4fd30ba36702842a"
139
+ ]
140
+ ]
141
+ },
142
+ {
143
+ "id": "64a657de3954a4b5",
144
+ "type": "debug",
145
+ "z": "6bd3da1a.7e2b84",
146
+ "name": "Results",
147
+ "active": true,
148
+ "tosidebar": true,
149
+ "console": false,
150
+ "tostatus": true,
151
+ "complete": "true",
152
+ "targetType": "full",
153
+ "statusVal": "pgsql.rowCount",
154
+ "statusType": "msg",
155
+ "x": 560,
156
+ "y": 1700,
157
+ "wires": []
158
+ },
159
+ {
160
+ "id": "adf069475c5e0ba3",
161
+ "type": "postgresql",
162
+ "z": "6bd3da1a.7e2b84",
163
+ "name": "SELECT",
164
+ "query": "SELECT * FROM mytable\nWHERE id < 100;\n",
165
+ "postgreSQLConfig": "20ae1e52d1eef983",
166
+ "split": false,
167
+ "rowsPerMsg": "1",
168
+ "outputs": 1,
169
+ "x": 360,
170
+ "y": 1700,
171
+ "wires": [
172
+ [
173
+ "64a657de3954a4b5"
174
+ ]
175
+ ]
176
+ },
177
+ {
178
+ "id": "3134bfc0f12e13c3",
179
+ "type": "inject",
180
+ "z": "6bd3da1a.7e2b84",
181
+ "name": "Start",
182
+ "props": [
183
+ {
184
+ "p": "payload"
185
+ },
186
+ {
187
+ "p": "topic",
188
+ "vt": "str"
189
+ }
190
+ ],
191
+ "repeat": "",
192
+ "crontab": "",
193
+ "once": false,
194
+ "onceDelay": 0.1,
195
+ "topic": "",
196
+ "payloadType": "date",
197
+ "x": 190,
198
+ "y": 1700,
199
+ "wires": [
200
+ [
201
+ "adf069475c5e0ba3"
202
+ ]
203
+ ]
204
+ },
205
+ {
206
+ "id": "d04c65ee97e3a273",
207
+ "type": "inject",
208
+ "z": "6bd3da1a.7e2b84",
209
+ "name": "Prepare",
210
+ "props": [
211
+ {
212
+ "p": "payload"
213
+ },
214
+ {
215
+ "p": "topic",
216
+ "vt": "str"
217
+ }
218
+ ],
219
+ "repeat": "",
220
+ "crontab": "",
221
+ "once": false,
222
+ "onceDelay": 0.1,
223
+ "topic": "",
224
+ "payloadType": "date",
225
+ "x": 180,
226
+ "y": 1520,
227
+ "wires": [
228
+ [
229
+ "82b7c689d6682f72"
230
+ ]
231
+ ]
232
+ },
233
+ {
234
+ "id": "c5f0b4b2442e3137",
235
+ "type": "debug",
236
+ "z": "6bd3da1a.7e2b84",
237
+ "name": "Done",
238
+ "active": true,
239
+ "tosidebar": true,
240
+ "console": false,
241
+ "tostatus": true,
242
+ "complete": "true",
243
+ "targetType": "full",
244
+ "statusVal": "pgsql",
245
+ "statusType": "msg",
246
+ "x": 550,
247
+ "y": 1520,
248
+ "wires": []
249
+ },
250
+ {
251
+ "id": "82b7c689d6682f72",
252
+ "type": "postgresql",
253
+ "z": "6bd3da1a.7e2b84",
254
+ "name": "ADD COLUMN",
255
+ "query": "ALTER TABLE mytable\n DROP COLUMN IF EXISTS validity;\n\nALTER TABLE mytable\n ADD COLUMN validity BOOLEAN;\n",
256
+ "postgreSQLConfig": "20ae1e52d1eef983",
257
+ "split": false,
258
+ "rowsPerMsg": "10",
259
+ "outputs": 1,
260
+ "x": 380,
261
+ "y": 1520,
262
+ "wires": [
263
+ [
264
+ "c5f0b4b2442e3137"
265
+ ]
266
+ ]
267
+ },
268
+ {
269
+ "id": "20ae1e52d1eef983",
270
+ "type": "postgreSQLConfig",
271
+ "name": "myuser@timescale:5432/iot",
272
+ "host": "timescale",
273
+ "hostFieldType": "str",
274
+ "port": "5432",
275
+ "portFieldType": "num",
276
+ "database": "iot",
277
+ "databaseFieldType": "str",
278
+ "ssl": "false",
279
+ "sslFieldType": "bool",
280
+ "max": "10",
281
+ "maxFieldType": "num",
282
+ "idle": "1000",
283
+ "idleFieldType": "num",
284
+ "connectionTimeout": "10000",
285
+ "connectionTimeoutFieldType": "num",
286
+ "user": "myuser",
287
+ "userFieldType": "str",
288
+ "password": "???",
289
+ "passwordFieldType": "str"
290
+ }
291
+ ]
Binary file
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Rewritten subset of the library https://github.com/bwestergard/node-postgres-named/blob/master/main.js
5
+ * https://github.com/ksteckert/node-postgres-named/tree/patch-1
6
+ */
7
+
8
+ const tokenPattern = /(?<=\$)[a-zA-Z]([a-zA-Z0-9_]*)\b/g;
9
+
10
+ function numericFromNamed(sql, parameters) {
11
+ const fillableTokens = new Set(Object.keys(parameters));
12
+ const matchedTokens = new Set(sql.match(tokenPattern));
13
+
14
+ const unmatchedTokens = Array.from(matchedTokens).filter((token) => !fillableTokens.has(token));
15
+ if (unmatchedTokens.length > 0) {
16
+ throw new Error('Missing Parameters: ' + unmatchedTokens.join(', '));
17
+ }
18
+
19
+ const fillTokens = Array.from(matchedTokens).filter((token) => fillableTokens.has(token)).sort();
20
+ const fillValues = fillTokens.map((token) => parameters[token]);
21
+
22
+ const interpolatedSql = fillTokens.reduce((partiallyInterpolated, token, index) => {
23
+ const replaceAllPattern = new RegExp('\\$' + fillTokens[index] + '\\b', 'g');
24
+ return partiallyInterpolated.replace(replaceAllPattern, '$' + (index + 1)); // PostgreSQL parameters are 1-indexed
25
+ }, sql);
26
+
27
+ return {
28
+ text: interpolatedSql,
29
+ values: fillValues,
30
+ };
31
+ }
32
+
33
+ module.exports.convert = numericFromNamed;
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#fff">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
3
+ </svg>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="90" height="135" version="1.1" viewBox="0 0 90 135" xmlns="http://www.w3.org/2000/svg">
3
+ <g transform="matrix(1.0078 0 0 1.0078 -67.007 -65.914)">
4
+ <path d="m77.77 92.7c-3.497 0-6.311 2.816-6.311 6.313-0.0052 11.55-0.01316 23.11-0.01499 34.66 7.5 0.0444 15.01 0.217 22.49-0.3183 10.43-0.9511 19.61-6.552 29.22-10.23 8.742-3.697 18.13-5.941 27.66-5.741l-1e-3 -18.38c4e-3 -3.498-2.815-6.313-6.311-6.313zm73.05 37.19c-1.055 0.0169-2.111 0.0457-3.166 0.0889-11.64-0.0448-21.96 5.974-32.46 10.19 8.115 3.283 15.95 7.454 24.55 9.389 3.675 0.5576 7.372 0.8292 11.08 0.9085zm-71.13 16.57c-2.747 6e-3 -5.494 0.0346-8.239 0.0486 0.0024 6.416 0.0072 12.83 0.01757 19.25 0.0037 3.498 2.815 6.313 6.311 6.313h66.73c3.497 0 6.311-2.816 6.311-6.313v-2.778c-8.203-0.0537-16.4-1.304-24.04-4.374-11.82-4.124-22.85-11.45-35.68-11.94-3.798-0.1794-7.602-0.2147-11.41-0.2062z" fill="#DA3D0B"/>
5
+ </g>
6
+ </svg>
File without changes
@@ -0,0 +1,37 @@
1
+ {
2
+ "tables-query-config": {
3
+ "label": {
4
+ "host": "Host",
5
+ "port": "Port",
6
+ "database": "Database",
7
+ "ssl": "SSL",
8
+ "user": "User",
9
+ "password": "Password",
10
+ "applicationName": "Application name",
11
+ "max": "Maximum size",
12
+ "idle": "Idle Timeout",
13
+ "connectionTimeout": "Connection Timeout"
14
+ },
15
+ "tab": {
16
+ "connection": "Connection",
17
+ "security": "Security",
18
+ "pool": "Pool"
19
+ },
20
+ "placeholder": {
21
+ "name": "dbConnection",
22
+ "host": "127.0.0.1",
23
+ "port": "5432",
24
+ "database": "dbExample",
25
+ "applicationName": "",
26
+ "max": "10",
27
+ "idle": "1000 (Milliseconds)",
28
+ "connectionTimeout": "10000 (Milliseconds)",
29
+ "user": "dbUser",
30
+ "password": "dbPassword"
31
+ },
32
+ "title": {
33
+ "applicationName": "The name of the application that created this Client instance.",
34
+ "max": "Maximum number of physical database connections that this connection pool can contain."
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,99 @@
1
+ <script type="text/x-red" data-help-name="tables-query">
2
+ <p>
3
+ This node allows you to write and run queries against database tables that are managed by FlowFuse Tables.
4
+ </p>
5
+
6
+ <h3>Outputs</h3>
7
+ <p>The response (rows) is provided in <code>msg.payload</code> as an array.</p>
8
+ <p>
9
+ An exception is if the <em>Split results</em> option is enabled and the <em>Number of rows per message</em> is set to 1,
10
+ then <code>msg.payload</code> is not an array but the single-row response.
11
+ </p>
12
+ <p>
13
+ Additional information is provided as <code>msg.pgsql.rowCount</code> and <code>msg.pgsql.command</code>.
14
+ See the <a href="https://node-postgres.com/apis/result">underlying documentation</a> for details.
15
+ </p>
16
+ <p>In the case of multiple queries, then <code>msg.pgsql</code> is an array.</p>
17
+
18
+ <h3>Inputs</h3>
19
+ <h4>SQL query template</h4>
20
+ <p>This node uses the <a href="https://github.com/janl/mustache.js">Mustache template system</a> to generate queries based on the message:</p>
21
+ <pre>
22
+ -- INTEGER id column
23
+ SELECT * FROM table WHERE id = {{{ msg.id }}}
24
+
25
+ -- TEXT id column
26
+ SELECT * FROM table WHERE id = '{{{ msg.id }}}'
27
+ </pre>
28
+
29
+ <h4>Dynamic SQL queries</h4>
30
+ <p>As an alternative to using the query template above, this node also accepts an SQL query via the <code>msg.query</code> parameter.</p>
31
+
32
+ <h4>Parameterized queries</h4>
33
+ <p>Parameters for parameterized queries can be passed as an array <code>msg.params</code>:</p>
34
+ <pre>
35
+ // In a function, provide parameters for the parameterized query
36
+ msg.params = [ msg.id ];
37
+ </pre>
38
+
39
+ <pre>
40
+ -- In this node, use a parameterized query
41
+ SELECT * FROM table WHERE id = $1
42
+ </pre>
43
+
44
+ <h4>Parameterized queries</h4>
45
+ <p>
46
+ As an alternative to numeric parameters,
47
+ named parameters for parameterized queries can be passed as a parameter object <code>msg.queryParameters</code>:
48
+ </p>
49
+ <pre>
50
+ // In a function, provide parameters for the named parameterized query
51
+ msg.queryParameters.id = msg.id;
52
+ </pre>
53
+
54
+ <pre>
55
+ -- In this node, use a named parameterized query
56
+ SELECT * FROM table WHERE id = $id;
57
+ </pre>
58
+
59
+ <p>
60
+ <em>Note</em>: named parameters are not natively supported by PostgreSQL, and this library just emulates them,
61
+ so this is less robust than numeric parameters.
62
+ </p>
63
+
64
+ <h3>Backpressure</h3>
65
+ <p>
66
+ This node supports <em>backpressure</em> / <em>flow control</em>:
67
+ when the <em>Split results</em> option is enabled, it waits for a <em>tick</em> before releasing the next batch of lines,
68
+ to make sure the rest of your Node-RED flow is ready to process more data
69
+ (instead of risking an out-of-memory condition), and also conveys this information upstream.
70
+ </p><p>
71
+ So when the <em>Split results</em> option is enabled, this node will only output one message at first,
72
+ and then awaits a message containing a truthy <code>msg.tick</code> before releasing the next message.
73
+ </p><p>
74
+ To make this behaviour potentially automatic (avoiding manual wires),
75
+ this node declares its ability by exposing a truthy <code>node.tickConsumer</code> for downstream nodes to detect this feature,
76
+ and a truthy <code>node.tickProvider</code> for upstream nodes.
77
+ Likewise, this node detects upstream nodes using the same back-pressure convention, and automatically sends ticks.
78
+ </p>
79
+
80
+ <h3>Sequences for split results</h3>
81
+ <p>
82
+ When the <em>Split results</em> option is enabled (streaming), the messages contain some information following the conventions
83
+ for <a href="https://nodered.org/docs/user-guide/messages#message-sequences"><em>messages sequences</em></a>.
84
+ </p>
85
+
86
+ <pre>
87
+ {
88
+ payload: '...',
89
+ parts: {
90
+ id: 0.1234, // sequence ID, randomly generated (changes for every sequence)
91
+ index: 5, // incremented for each message of the same sequence
92
+ count: 6, // total number of messages; only available in the last message of a sequence
93
+ parts: {}, // optional upstream parts information
94
+ },
95
+ complete: true, // True only for the last message of a sequence
96
+ }
97
+ </pre>
98
+
99
+ </script>
@@ -0,0 +1,10 @@
1
+ {
2
+ "tables-query": {
3
+ "label": {
4
+ "name": "Name",
5
+ "query": "Query",
6
+ "split": "Split results in multiple messages",
7
+ "rowsPerMsg": "Rows per message"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,91 @@
1
+ <script type="text/javascript">
2
+ /* global RED:false, $:false */
3
+ RED.nodes.registerType('tables-query', {
4
+ category: 'FlowFuse',
5
+ color: 'white',
6
+ defaults: {
7
+ name: {
8
+ value: '',
9
+ },
10
+ query: {
11
+ value: 'SELECT * FROM table_name;',
12
+ },
13
+ split: {
14
+ value: false,
15
+ },
16
+ rowsPerMsg: {
17
+ value: 1,
18
+ }
19
+ },
20
+ inputs: 1,
21
+ outputs: 1,
22
+ icon: 'flowfuse.svg',
23
+ align: 'left',
24
+ paletteLabel: 'query',
25
+ label: function () {
26
+ return this.name || 'query';
27
+ },
28
+ labelStyle: function () {
29
+ const classes = 'nr-tables-query-node';
30
+ return this.name ? `${classes} node_label_italic` : classes;
31
+ },
32
+ oneditprepare: function () {
33
+ $('#node-input-split').prop('checked', this.split);
34
+
35
+ // when node-input-split is modified, react:
36
+ $('#node-input-split').on('change', function () {
37
+ // if checked, show the rowsPerMsg input
38
+ if ($('#node-input-split').is(':checked')) {
39
+ $('#node-input-rowsPerMsg-container').show();
40
+ } else {
41
+ $('#node-input-rowsPerMsg-container').hide();
42
+ }
43
+ });
44
+
45
+ $('#node-input-rowsPerMsg').value = this.split ? this.rowsPerMsg : 1;
46
+ this.editor = RED.editor.createEditor({
47
+ id: 'node-input-editor',
48
+ mode: 'ace/mode/sql',
49
+ value: $('#node-input-query').val(),
50
+ });
51
+ this.editor.focus();
52
+ },
53
+ oneditsave: function () {
54
+ $('#node-input-query').val(this.editor.getValue());
55
+ delete this.editor;
56
+ },
57
+ });
58
+ </script>
59
+
60
+ <script type="text/x-red" data-template-name="tables-query">
61
+ <div class="form-row">
62
+ <label for="node-input-name">
63
+ <i class="icon-tag"></i>
64
+ <span data-i18n="tables-query.label.name"></span>
65
+ </label>
66
+ <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name" />
67
+ </div>
68
+ <div class="form-row" style="position: relative; margin-bottom: 0px;">
69
+ <label for="node-input-query">
70
+ <i class="fa fa-file-code-o"></i>
71
+ <span data-i18n="tables-query.label.query"></span>
72
+ </label>
73
+ <input type="hidden" id="node-input-query" autofocus="autofocus" />
74
+ </div>
75
+ <div class="form-row node-text-editor-row">
76
+ <div style="height: 300px; min-height: 150px;" class="node-text-editor" id="node-input-editor"></div>
77
+ </div>
78
+ <h4 style="margin-bottom: 0.5rem;">Output</h4>
79
+ <div class="form-row">
80
+ <input type="checkbox" id="node-input-split" style="display: inline-block; width: auto; vertical-align: top;" />
81
+ <label for="node-input-split" style="width: auto;">
82
+ <span data-i18n="tables-query.label.split"></span>
83
+ </label>
84
+ </div>
85
+ <div class="form-row" id="node-input-rowsPerMsg-container">
86
+ <label for="node-input-rowsPerMsg">
87
+ <span data-i18n="tables-query.label.rowsPerMsg"></span>
88
+ </label>
89
+ <input type="number" id="node-input-rowsPerMsg" placeholder="1" value="1" min="1" />
90
+ </div>
91
+ </script>