@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.
- package/.editorconfig +24 -0
- package/.eslintrc +17 -0
- package/.gitattributes +3 -0
- package/.github/dependabot.yml +16 -0
- package/.github/workflows/project-automation.yml +10 -0
- package/.github/workflows/release-publish.yml +21 -0
- package/.markdownlint.json +20 -0
- package/.markdownlintignore +2 -0
- package/LICENSE.md +201 -0
- package/README.md +123 -0
- package/eslint.config.mjs +61 -0
- package/examples/flow.json +291 -0
- package/examples/flow.png +0 -0
- package/node-postgres-named.js +33 -0
- package/nodes/icons/circle-stack.svg +3 -0
- package/nodes/icons/flowfuse.svg +6 -0
- package/nodes/locales/en-US/query-config.html +0 -0
- package/nodes/locales/en-US/query-config.json +37 -0
- package/nodes/locales/en-US/query.html +99 -0
- package/nodes/locales/en-US/query.json +10 -0
- package/nodes/query.html +91 -0
- package/nodes/query.js +320 -0
- package/nodes/utils/ff-api.js +15 -0
- package/package.json +79 -0
- package/test/node-postgres-named.test.js +63 -0
|
@@ -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>
|
package/nodes/query.html
ADDED
|
@@ -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>
|