node-red-contrib-questdb 0.6.9 → 0.6.23
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/nodes/icons/questdb-logo.png +0 -0
- package/nodes/questdb-flatten.html +45 -0
- package/nodes/questdb-flatten.js +15 -0
- package/nodes/questdb-mapper.html +281 -277
- package/nodes/questdb-type-router.html +2 -1
- package/nodes/questdb-value.html +145 -0
- package/nodes/questdb-value.js +280 -0
- package/nodes/questdb.html +290 -289
- package/nodes/questdb.js +93 -40
- package/package.json +4 -2
|
Binary file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.questdb-white-text { fill: #ffffff !important; }
|
|
3
|
+
.red-ui-palette-node[data-palette-type="questdb-flatten"] .red-ui-palette-label { color: #ffffff !important; }
|
|
4
|
+
.red-ui-palette-node[data-palette-type="questdb-flatten"] .red-ui-palette-icon-container { background-color: #f0f0f0 !important; }
|
|
5
|
+
</style>
|
|
6
|
+
|
|
7
|
+
<script type="text/javascript">
|
|
8
|
+
RED.nodes.registerType('questdb-flatten', {
|
|
9
|
+
category: 'questdb',
|
|
10
|
+
color: '#a23154',
|
|
11
|
+
defaults: {
|
|
12
|
+
name: { value: "" }
|
|
13
|
+
},
|
|
14
|
+
inputs: 1,
|
|
15
|
+
outputs: 1,
|
|
16
|
+
icon: "questdb-logo.png",
|
|
17
|
+
paletteLabel: "Flatten",
|
|
18
|
+
label: function () { return this.name || "Flatten"; },
|
|
19
|
+
labelStyle: function () { return (this.name ? "node_label_italic" : "") + " questdb-white-text"; }
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script type="text/html" data-template-name="questdb-flatten">
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
26
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
27
|
+
</div>
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/html" data-help-name="questdb-flatten">
|
|
31
|
+
<p>Flattens <code>msg.payload</code> into the top-level message object.</p>
|
|
32
|
+
|
|
33
|
+
<h3>Behaviour</h3>
|
|
34
|
+
<p>Spreads all properties of <code>msg.payload</code> directly onto <code>msg</code>, then removes <code>msg.payload</code>.</p>
|
|
35
|
+
|
|
36
|
+
<h3>Example</h3>
|
|
37
|
+
<pre>// Input
|
|
38
|
+
msg.payload = { topic: "temp", symbols: { ... }, columns: { ... } }
|
|
39
|
+
|
|
40
|
+
// Output
|
|
41
|
+
msg.topic = "temp"
|
|
42
|
+
msg.symbols = { ... }
|
|
43
|
+
msg.columns = { ... }
|
|
44
|
+
// msg.payload is removed</pre>
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
function FlattenNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
var node = this;
|
|
5
|
+
|
|
6
|
+
node.on('input', function (msg) {
|
|
7
|
+
var payload = msg.payload;
|
|
8
|
+
delete msg.payload;
|
|
9
|
+
Object.assign(msg, payload);
|
|
10
|
+
node.send(msg);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
RED.nodes.registerType('questdb-flatten', FlattenNode);
|
|
15
|
+
};
|
|
@@ -1,277 +1,281 @@
|
|
|
1
|
-
<script type="text/javascript">
|
|
2
|
-
RED.nodes.registerType('questdb-mapper', {
|
|
3
|
-
category: 'questdb',
|
|
4
|
-
color: '#a23154',
|
|
5
|
-
defaults: {
|
|
6
|
-
name: {value: ""},
|
|
7
|
-
tableName: {value: ""},
|
|
8
|
-
timestampField: {value: "timestamp"},
|
|
9
|
-
timestampFieldType: {value: "msg"},
|
|
10
|
-
symbolMappings: {value: []},
|
|
11
|
-
columnMappings: {value: []}
|
|
12
|
-
},
|
|
13
|
-
inputs: 1,
|
|
14
|
-
outputs: 1,
|
|
15
|
-
icon: "
|
|
16
|
-
label: function() {
|
|
17
|
-
return this.name || "Mapper";
|
|
18
|
-
},
|
|
19
|
-
paletteLabel: "Mapper",
|
|
20
|
-
labelStyle: function() {
|
|
21
|
-
return this.name ? "node_label_italic questdb-white-text" : "questdb-white-text";
|
|
22
|
-
},
|
|
23
|
-
oneditprepare: function() {
|
|
24
|
-
const node = this;
|
|
25
|
-
|
|
26
|
-
// Initialize TypedInput for timestamp field
|
|
27
|
-
$("#node-input-timestampField").typedInput({
|
|
28
|
-
type: node.timestampFieldType || "msg",
|
|
29
|
-
types: ["msg", "flow", "global", "str", "jsonata"],
|
|
30
|
-
typeField: "#node-input-timestampFieldType"
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Symbol mappings list
|
|
34
|
-
const symbolList = $("#node-input-symbol-list").css({
|
|
35
|
-
'min-height': '100px',
|
|
36
|
-
'min-width': '400px'
|
|
37
|
-
}).editableList({
|
|
38
|
-
addItem: function(container, i, data) {
|
|
39
|
-
const row = $('<div/>').css({display: 'flex', alignItems: 'center'}).appendTo(container);
|
|
40
|
-
|
|
41
|
-
// Source field with TypedInput
|
|
42
|
-
const sourceInput = $('<input/>', {type: "text", class: "symbol-source"})
|
|
43
|
-
.css({width: "45%", marginRight: "5px"})
|
|
44
|
-
.appendTo(row);
|
|
45
|
-
|
|
46
|
-
$(sourceInput).typedInput({
|
|
47
|
-
default: 'msg',
|
|
48
|
-
types: ['msg', 'flow', 'global', 'jsonata']
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (data.source) {
|
|
52
|
-
$(sourceInput).typedInput('type', data.sourceType || 'msg');
|
|
53
|
-
$(sourceInput).typedInput('value', data.source);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
$('<span/>').text(" → ").css({margin: "0 5px"}).appendTo(row);
|
|
57
|
-
|
|
58
|
-
// Target field (plain text)
|
|
59
|
-
$('<input/>', {type: "text", placeholder: "symbol name", class: "symbol-target"})
|
|
60
|
-
.css({width: "40%"})
|
|
61
|
-
.val(data.target || "")
|
|
62
|
-
.appendTo(row);
|
|
63
|
-
},
|
|
64
|
-
removable: true,
|
|
65
|
-
sortable: true
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Column mappings list
|
|
69
|
-
const columnList = $("#node-input-column-list").css({
|
|
70
|
-
'min-height': '100px',
|
|
71
|
-
'min-width': '400px'
|
|
72
|
-
}).editableList({
|
|
73
|
-
addItem: function(container, i, data) {
|
|
74
|
-
const row = $('<div/>').css({display: 'flex', alignItems: 'center'}).appendTo(container);
|
|
75
|
-
|
|
76
|
-
// Source field with TypedInput
|
|
77
|
-
const sourceInput = $('<input/>', {type: "text", class: "column-source"})
|
|
78
|
-
.css({width: "35%", marginRight: "5px"})
|
|
79
|
-
.appendTo(row);
|
|
80
|
-
|
|
81
|
-
$(sourceInput).typedInput({
|
|
82
|
-
default: 'msg',
|
|
83
|
-
types: ['msg', 'flow', 'global', 'jsonata']
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (data.source) {
|
|
87
|
-
$(sourceInput).typedInput('type', data.sourceType || 'msg');
|
|
88
|
-
$(sourceInput).typedInput('value', data.source);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
$('<span/>').text(" → ").css({margin: "0 5px"}).appendTo(row);
|
|
92
|
-
|
|
93
|
-
// Target field (plain text)
|
|
94
|
-
$('<input/>', {type: "text", placeholder: "column name", class: "column-target"})
|
|
95
|
-
.css({width: "25%", marginRight: "5px"})
|
|
96
|
-
.val(data.target || "")
|
|
97
|
-
.appendTo(row);
|
|
98
|
-
|
|
99
|
-
// Type selector
|
|
100
|
-
const typeSelect = $('<select/>', {class: "column-type"})
|
|
101
|
-
.css({width: "20%"})
|
|
102
|
-
.appendTo(row);
|
|
103
|
-
['auto', 'float', 'double', 'integer', 'long', 'decimal', 'varchar', 'string', 'boolean', 'timestamp', 'array'].forEach(function(t) {
|
|
104
|
-
$('<option/>').val(t).text(t).appendTo(typeSelect);
|
|
105
|
-
});
|
|
106
|
-
typeSelect.val(data.type || "auto");
|
|
107
|
-
},
|
|
108
|
-
removable: true,
|
|
109
|
-
sortable: true
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Load existing mappings
|
|
113
|
-
if (node.symbolMappings) {
|
|
114
|
-
node.symbolMappings.forEach(function(m) {
|
|
115
|
-
symbolList.editableList('addItem', m);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
if (node.columnMappings) {
|
|
119
|
-
node.columnMappings.forEach(function(m) {
|
|
120
|
-
columnList.editableList('addItem', m);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
oneditresize: function(size) {
|
|
125
|
-
// Resize editable lists if needed
|
|
126
|
-
},
|
|
127
|
-
oneditsave: function() {
|
|
128
|
-
const node = this;
|
|
129
|
-
|
|
130
|
-
// Save symbol mappings
|
|
131
|
-
node.symbolMappings = [];
|
|
132
|
-
$("#node-input-symbol-list").editableList('items').each(function() {
|
|
133
|
-
const sourceInput = $(this).find(".symbol-source");
|
|
134
|
-
const source = sourceInput.typedInput('value');
|
|
135
|
-
const sourceType = sourceInput.typedInput('type');
|
|
136
|
-
const target = $(this).find(".symbol-target").val().trim();
|
|
137
|
-
if (source && target) {
|
|
138
|
-
node.symbolMappings.push({
|
|
139
|
-
source: source,
|
|
140
|
-
sourceType: sourceType,
|
|
141
|
-
target: target
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Save column mappings
|
|
147
|
-
node.columnMappings = [];
|
|
148
|
-
$("#node-input-column-list").editableList('items').each(function() {
|
|
149
|
-
const sourceInput = $(this).find(".column-source");
|
|
150
|
-
const source = sourceInput.typedInput('value');
|
|
151
|
-
const sourceType = sourceInput.typedInput('type');
|
|
152
|
-
const target = $(this).find(".column-target").val().trim();
|
|
153
|
-
const type = $(this).find(".column-type").val();
|
|
154
|
-
if (source && target) {
|
|
155
|
-
node.columnMappings.push({
|
|
156
|
-
source: source,
|
|
157
|
-
sourceType: sourceType,
|
|
158
|
-
target: target,
|
|
159
|
-
type: type
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
</script>
|
|
166
|
-
|
|
167
|
-
<script type="text/html" data-template-name="questdb-mapper">
|
|
168
|
-
<div class="form-row">
|
|
169
|
-
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
170
|
-
<input type="text" id="node-input-name" placeholder="Name">
|
|
171
|
-
</div>
|
|
172
|
-
<div class="form-row">
|
|
173
|
-
<label for="node-input-tableName"><i class="fa fa-table"></i> Table Name</label>
|
|
174
|
-
<input type="text" id="node-input-tableName" placeholder="Leave empty to use msg.topic">
|
|
175
|
-
</div>
|
|
176
|
-
<div class="form-row">
|
|
177
|
-
<label for="node-input-timestampField"><i class="fa fa-clock-o"></i> Timestamp Field</label>
|
|
178
|
-
<input type="hidden" id="node-input-timestampFieldType">
|
|
179
|
-
<input type="text" id="node-input-timestampField" placeholder="timestamp">
|
|
180
|
-
</div>
|
|
181
|
-
<div class="form-row">
|
|
182
|
-
<label style="width:100%;"><i class="fa fa-bookmark"></i> Symbol Mappings (indexed tags)</label>
|
|
183
|
-
<ol id="node-input-symbol-list"></ol>
|
|
184
|
-
</div>
|
|
185
|
-
<div class="form-row">
|
|
186
|
-
<label style="width:100%;"><i class="fa fa-columns"></i> Column Mappings (values)</label>
|
|
187
|
-
<ol id="node-input-column-list"></ol>
|
|
188
|
-
</div>
|
|
189
|
-
</script>
|
|
190
|
-
|
|
191
|
-
<script type="text/html" data-help-name="questdb-mapper">
|
|
192
|
-
<p>Maps message fields to QuestDB ILP structure for use with the Write node.</p>
|
|
193
|
-
|
|
194
|
-
<h3>Properties</h3>
|
|
195
|
-
<dl class="message-properties">
|
|
196
|
-
<dt>Table Name</dt>
|
|
197
|
-
<dd>Target table name. If empty, uses <code>msg.topic</code></dd>
|
|
198
|
-
<dt>Timestamp Field</dt>
|
|
199
|
-
<dd>Path to timestamp field (e.g. <code>payload.ts</code>)</dd>
|
|
200
|
-
<dt>Symbol Mappings</dt>
|
|
201
|
-
<dd>Map msg fields to QuestDB symbols (indexed string columns)</dd>
|
|
202
|
-
<dt>Column Mappings</dt>
|
|
203
|
-
<dd>Map msg fields to QuestDB columns with type conversion</dd>
|
|
204
|
-
</dl>
|
|
205
|
-
|
|
206
|
-
<h3>Column Types</h3>
|
|
207
|
-
<ul>
|
|
208
|
-
<li><b>auto</b> - Auto-detect type from value</li>
|
|
209
|
-
<li><b>float</b> - 32-bit floating point</li>
|
|
210
|
-
<li><b>double</b> - 64-bit floating point (higher precision)</li>
|
|
211
|
-
<li><b>integer</b> - 32-bit signed integer</li>
|
|
212
|
-
<li><b>long</b> - 64-bit signed integer</li>
|
|
213
|
-
<li><b>decimal</b> - Arbitrary precision decimal</li>
|
|
214
|
-
<li><b>varchar</b> - Variable-length text (QuestDB native type, preferred over string)</li>
|
|
215
|
-
<li><b>string</b> - Text value (alias for varchar)</li>
|
|
216
|
-
<li><b>boolean</b> - true/false</li>
|
|
217
|
-
<li><b>timestamp</b> - Date/time value</li>
|
|
218
|
-
<li><b>array</b> - Array of doubles (double[])</li>
|
|
219
|
-
</ul>
|
|
220
|
-
|
|
221
|
-
<h3>Field Path Syntax</h3>
|
|
222
|
-
<p>Use dot notation: <code>payload.sensor.value</code></p>
|
|
223
|
-
<p>Array access: <code>payload.readings[0]</code></p>
|
|
224
|
-
|
|
225
|
-
<h3>Output</h3>
|
|
226
|
-
<p>Produces a message ready for the QuestDB Write node:</p>
|
|
227
|
-
<pre>{
|
|
228
|
-
topic: "table_name",
|
|
229
|
-
payload: {
|
|
230
|
-
symbols: { ... },
|
|
231
|
-
columns: { ... },
|
|
232
|
-
timestamp: ...
|
|
233
|
-
}
|
|
234
|
-
}</pre>
|
|
235
|
-
|
|
236
|
-
<h3>Example</h3>
|
|
237
|
-
<p>Input message:</p>
|
|
238
|
-
<pre>{
|
|
239
|
-
topic: "sensors",
|
|
240
|
-
payload: {
|
|
241
|
-
device: "sensor1",
|
|
242
|
-
temp: 23.5,
|
|
243
|
-
readings: [1.1, 2.2, 3.3],
|
|
244
|
-
ts: 1699999999000
|
|
245
|
-
}
|
|
246
|
-
}</pre>
|
|
247
|
-
<p>With mappings:</p>
|
|
248
|
-
<ul>
|
|
249
|
-
<li>Symbol: <code>payload.device</code> → <code>device_id</code></li>
|
|
250
|
-
<li>Column: <code>payload.temp</code> → <code>temperature</code> (double)</li>
|
|
251
|
-
<li>Column: <code>payload.readings</code> → <code>values</code> (array)</li>
|
|
252
|
-
<li>Timestamp: <code>payload.ts</code></li>
|
|
253
|
-
</ul>
|
|
254
|
-
<p>Output:</p>
|
|
255
|
-
<pre>{
|
|
256
|
-
topic: "sensors",
|
|
257
|
-
payload: {
|
|
258
|
-
symbols: { device_id: "sensor1" },
|
|
259
|
-
columns: {
|
|
260
|
-
temperature: { value: 23.5, type: "double" },
|
|
261
|
-
values: { value: [1.1, 2.2, 3.3], type: "array" }
|
|
262
|
-
},
|
|
263
|
-
timestamp: 1699999999000
|
|
264
|
-
}
|
|
265
|
-
}</pre>
|
|
266
|
-
</script>
|
|
267
|
-
|
|
268
|
-
<style>
|
|
269
|
-
/* White text on flow canvas */
|
|
270
|
-
.questdb-white-text {
|
|
271
|
-
fill: #ffffff !important;
|
|
272
|
-
}
|
|
273
|
-
/* White text in palette */
|
|
274
|
-
.red-ui-palette-node[data-palette-type="questdb-mapper"] .red-ui-palette-label {
|
|
275
|
-
color: #ffffff !important;
|
|
276
|
-
}
|
|
277
|
-
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('questdb-mapper', {
|
|
3
|
+
category: 'questdb',
|
|
4
|
+
color: '#a23154',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: {value: ""},
|
|
7
|
+
tableName: {value: ""},
|
|
8
|
+
timestampField: {value: "timestamp"},
|
|
9
|
+
timestampFieldType: {value: "msg"},
|
|
10
|
+
symbolMappings: {value: []},
|
|
11
|
+
columnMappings: {value: []}
|
|
12
|
+
},
|
|
13
|
+
inputs: 1,
|
|
14
|
+
outputs: 1,
|
|
15
|
+
icon: "questdb-logo.png",
|
|
16
|
+
label: function() {
|
|
17
|
+
return this.name || "Mapper";
|
|
18
|
+
},
|
|
19
|
+
paletteLabel: "Mapper",
|
|
20
|
+
labelStyle: function() {
|
|
21
|
+
return this.name ? "node_label_italic questdb-white-text" : "questdb-white-text";
|
|
22
|
+
},
|
|
23
|
+
oneditprepare: function() {
|
|
24
|
+
const node = this;
|
|
25
|
+
|
|
26
|
+
// Initialize TypedInput for timestamp field
|
|
27
|
+
$("#node-input-timestampField").typedInput({
|
|
28
|
+
type: node.timestampFieldType || "msg",
|
|
29
|
+
types: ["msg", "flow", "global", "str", "jsonata"],
|
|
30
|
+
typeField: "#node-input-timestampFieldType"
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Symbol mappings list
|
|
34
|
+
const symbolList = $("#node-input-symbol-list").css({
|
|
35
|
+
'min-height': '100px',
|
|
36
|
+
'min-width': '400px'
|
|
37
|
+
}).editableList({
|
|
38
|
+
addItem: function(container, i, data) {
|
|
39
|
+
const row = $('<div/>').css({display: 'flex', alignItems: 'center'}).appendTo(container);
|
|
40
|
+
|
|
41
|
+
// Source field with TypedInput
|
|
42
|
+
const sourceInput = $('<input/>', {type: "text", class: "symbol-source"})
|
|
43
|
+
.css({width: "45%", marginRight: "5px"})
|
|
44
|
+
.appendTo(row);
|
|
45
|
+
|
|
46
|
+
$(sourceInput).typedInput({
|
|
47
|
+
default: 'msg',
|
|
48
|
+
types: ['msg', 'flow', 'global', 'jsonata']
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (data.source) {
|
|
52
|
+
$(sourceInput).typedInput('type', data.sourceType || 'msg');
|
|
53
|
+
$(sourceInput).typedInput('value', data.source);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$('<span/>').text(" → ").css({margin: "0 5px"}).appendTo(row);
|
|
57
|
+
|
|
58
|
+
// Target field (plain text)
|
|
59
|
+
$('<input/>', {type: "text", placeholder: "symbol name", class: "symbol-target"})
|
|
60
|
+
.css({width: "40%"})
|
|
61
|
+
.val(data.target || "")
|
|
62
|
+
.appendTo(row);
|
|
63
|
+
},
|
|
64
|
+
removable: true,
|
|
65
|
+
sortable: true
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Column mappings list
|
|
69
|
+
const columnList = $("#node-input-column-list").css({
|
|
70
|
+
'min-height': '100px',
|
|
71
|
+
'min-width': '400px'
|
|
72
|
+
}).editableList({
|
|
73
|
+
addItem: function(container, i, data) {
|
|
74
|
+
const row = $('<div/>').css({display: 'flex', alignItems: 'center'}).appendTo(container);
|
|
75
|
+
|
|
76
|
+
// Source field with TypedInput
|
|
77
|
+
const sourceInput = $('<input/>', {type: "text", class: "column-source"})
|
|
78
|
+
.css({width: "35%", marginRight: "5px"})
|
|
79
|
+
.appendTo(row);
|
|
80
|
+
|
|
81
|
+
$(sourceInput).typedInput({
|
|
82
|
+
default: 'msg',
|
|
83
|
+
types: ['msg', 'flow', 'global', 'jsonata']
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (data.source) {
|
|
87
|
+
$(sourceInput).typedInput('type', data.sourceType || 'msg');
|
|
88
|
+
$(sourceInput).typedInput('value', data.source);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$('<span/>').text(" → ").css({margin: "0 5px"}).appendTo(row);
|
|
92
|
+
|
|
93
|
+
// Target field (plain text)
|
|
94
|
+
$('<input/>', {type: "text", placeholder: "column name", class: "column-target"})
|
|
95
|
+
.css({width: "25%", marginRight: "5px"})
|
|
96
|
+
.val(data.target || "")
|
|
97
|
+
.appendTo(row);
|
|
98
|
+
|
|
99
|
+
// Type selector
|
|
100
|
+
const typeSelect = $('<select/>', {class: "column-type"})
|
|
101
|
+
.css({width: "20%"})
|
|
102
|
+
.appendTo(row);
|
|
103
|
+
['auto', 'float', 'double', 'integer', 'long', 'decimal', 'varchar', 'string', 'boolean', 'timestamp', 'array'].forEach(function(t) {
|
|
104
|
+
$('<option/>').val(t).text(t).appendTo(typeSelect);
|
|
105
|
+
});
|
|
106
|
+
typeSelect.val(data.type || "auto");
|
|
107
|
+
},
|
|
108
|
+
removable: true,
|
|
109
|
+
sortable: true
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Load existing mappings
|
|
113
|
+
if (node.symbolMappings) {
|
|
114
|
+
node.symbolMappings.forEach(function(m) {
|
|
115
|
+
symbolList.editableList('addItem', m);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (node.columnMappings) {
|
|
119
|
+
node.columnMappings.forEach(function(m) {
|
|
120
|
+
columnList.editableList('addItem', m);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
oneditresize: function(size) {
|
|
125
|
+
// Resize editable lists if needed
|
|
126
|
+
},
|
|
127
|
+
oneditsave: function() {
|
|
128
|
+
const node = this;
|
|
129
|
+
|
|
130
|
+
// Save symbol mappings
|
|
131
|
+
node.symbolMappings = [];
|
|
132
|
+
$("#node-input-symbol-list").editableList('items').each(function() {
|
|
133
|
+
const sourceInput = $(this).find(".symbol-source");
|
|
134
|
+
const source = sourceInput.typedInput('value');
|
|
135
|
+
const sourceType = sourceInput.typedInput('type');
|
|
136
|
+
const target = $(this).find(".symbol-target").val().trim();
|
|
137
|
+
if (source && target) {
|
|
138
|
+
node.symbolMappings.push({
|
|
139
|
+
source: source,
|
|
140
|
+
sourceType: sourceType,
|
|
141
|
+
target: target
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Save column mappings
|
|
147
|
+
node.columnMappings = [];
|
|
148
|
+
$("#node-input-column-list").editableList('items').each(function() {
|
|
149
|
+
const sourceInput = $(this).find(".column-source");
|
|
150
|
+
const source = sourceInput.typedInput('value');
|
|
151
|
+
const sourceType = sourceInput.typedInput('type');
|
|
152
|
+
const target = $(this).find(".column-target").val().trim();
|
|
153
|
+
const type = $(this).find(".column-type").val();
|
|
154
|
+
if (source && target) {
|
|
155
|
+
node.columnMappings.push({
|
|
156
|
+
source: source,
|
|
157
|
+
sourceType: sourceType,
|
|
158
|
+
target: target,
|
|
159
|
+
type: type
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<script type="text/html" data-template-name="questdb-mapper">
|
|
168
|
+
<div class="form-row">
|
|
169
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
170
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
171
|
+
</div>
|
|
172
|
+
<div class="form-row">
|
|
173
|
+
<label for="node-input-tableName"><i class="fa fa-table"></i> Table Name</label>
|
|
174
|
+
<input type="text" id="node-input-tableName" placeholder="Leave empty to use msg.topic">
|
|
175
|
+
</div>
|
|
176
|
+
<div class="form-row">
|
|
177
|
+
<label for="node-input-timestampField"><i class="fa fa-clock-o"></i> Timestamp Field</label>
|
|
178
|
+
<input type="hidden" id="node-input-timestampFieldType">
|
|
179
|
+
<input type="text" id="node-input-timestampField" placeholder="timestamp">
|
|
180
|
+
</div>
|
|
181
|
+
<div class="form-row">
|
|
182
|
+
<label style="width:100%;"><i class="fa fa-bookmark"></i> Symbol Mappings (indexed tags)</label>
|
|
183
|
+
<ol id="node-input-symbol-list"></ol>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="form-row">
|
|
186
|
+
<label style="width:100%;"><i class="fa fa-columns"></i> Column Mappings (values)</label>
|
|
187
|
+
<ol id="node-input-column-list"></ol>
|
|
188
|
+
</div>
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<script type="text/html" data-help-name="questdb-mapper">
|
|
192
|
+
<p>Maps message fields to QuestDB ILP structure for use with the Write node.</p>
|
|
193
|
+
|
|
194
|
+
<h3>Properties</h3>
|
|
195
|
+
<dl class="message-properties">
|
|
196
|
+
<dt>Table Name</dt>
|
|
197
|
+
<dd>Target table name. If empty, uses <code>msg.topic</code></dd>
|
|
198
|
+
<dt>Timestamp Field</dt>
|
|
199
|
+
<dd>Path to timestamp field (e.g. <code>payload.ts</code>)</dd>
|
|
200
|
+
<dt>Symbol Mappings</dt>
|
|
201
|
+
<dd>Map msg fields to QuestDB symbols (indexed string columns)</dd>
|
|
202
|
+
<dt>Column Mappings</dt>
|
|
203
|
+
<dd>Map msg fields to QuestDB columns with type conversion</dd>
|
|
204
|
+
</dl>
|
|
205
|
+
|
|
206
|
+
<h3>Column Types</h3>
|
|
207
|
+
<ul>
|
|
208
|
+
<li><b>auto</b> - Auto-detect type from value</li>
|
|
209
|
+
<li><b>float</b> - 32-bit floating point</li>
|
|
210
|
+
<li><b>double</b> - 64-bit floating point (higher precision)</li>
|
|
211
|
+
<li><b>integer</b> - 32-bit signed integer</li>
|
|
212
|
+
<li><b>long</b> - 64-bit signed integer</li>
|
|
213
|
+
<li><b>decimal</b> - Arbitrary precision decimal</li>
|
|
214
|
+
<li><b>varchar</b> - Variable-length text (QuestDB native type, preferred over string)</li>
|
|
215
|
+
<li><b>string</b> - Text value (alias for varchar)</li>
|
|
216
|
+
<li><b>boolean</b> - true/false</li>
|
|
217
|
+
<li><b>timestamp</b> - Date/time value</li>
|
|
218
|
+
<li><b>array</b> - Array of doubles (double[])</li>
|
|
219
|
+
</ul>
|
|
220
|
+
|
|
221
|
+
<h3>Field Path Syntax</h3>
|
|
222
|
+
<p>Use dot notation: <code>payload.sensor.value</code></p>
|
|
223
|
+
<p>Array access: <code>payload.readings[0]</code></p>
|
|
224
|
+
|
|
225
|
+
<h3>Output</h3>
|
|
226
|
+
<p>Produces a message ready for the QuestDB Write node:</p>
|
|
227
|
+
<pre>{
|
|
228
|
+
topic: "table_name",
|
|
229
|
+
payload: {
|
|
230
|
+
symbols: { ... },
|
|
231
|
+
columns: { ... },
|
|
232
|
+
timestamp: ...
|
|
233
|
+
}
|
|
234
|
+
}</pre>
|
|
235
|
+
|
|
236
|
+
<h3>Example</h3>
|
|
237
|
+
<p>Input message:</p>
|
|
238
|
+
<pre>{
|
|
239
|
+
topic: "sensors",
|
|
240
|
+
payload: {
|
|
241
|
+
device: "sensor1",
|
|
242
|
+
temp: 23.5,
|
|
243
|
+
readings: [1.1, 2.2, 3.3],
|
|
244
|
+
ts: 1699999999000
|
|
245
|
+
}
|
|
246
|
+
}</pre>
|
|
247
|
+
<p>With mappings:</p>
|
|
248
|
+
<ul>
|
|
249
|
+
<li>Symbol: <code>payload.device</code> → <code>device_id</code></li>
|
|
250
|
+
<li>Column: <code>payload.temp</code> → <code>temperature</code> (double)</li>
|
|
251
|
+
<li>Column: <code>payload.readings</code> → <code>values</code> (array)</li>
|
|
252
|
+
<li>Timestamp: <code>payload.ts</code></li>
|
|
253
|
+
</ul>
|
|
254
|
+
<p>Output:</p>
|
|
255
|
+
<pre>{
|
|
256
|
+
topic: "sensors",
|
|
257
|
+
payload: {
|
|
258
|
+
symbols: { device_id: "sensor1" },
|
|
259
|
+
columns: {
|
|
260
|
+
temperature: { value: 23.5, type: "double" },
|
|
261
|
+
values: { value: [1.1, 2.2, 3.3], type: "array" }
|
|
262
|
+
},
|
|
263
|
+
timestamp: 1699999999000
|
|
264
|
+
}
|
|
265
|
+
}</pre>
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<style>
|
|
269
|
+
/* White text on flow canvas */
|
|
270
|
+
.questdb-white-text {
|
|
271
|
+
fill: #ffffff !important;
|
|
272
|
+
}
|
|
273
|
+
/* White text in palette */
|
|
274
|
+
.red-ui-palette-node[data-palette-type="questdb-mapper"] .red-ui-palette-label {
|
|
275
|
+
color: #ffffff !important;
|
|
276
|
+
}
|
|
277
|
+
/* Light grey icon background */
|
|
278
|
+
.red-ui-palette-node[data-palette-type="questdb-mapper"] .red-ui-palette-icon-container {
|
|
279
|
+
background-color: #f0f0f0 !important;
|
|
280
|
+
}
|
|
281
|
+
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<style>
|
|
2
2
|
.questdb-white-text { fill: #ffffff !important; }
|
|
3
3
|
.red-ui-palette-node[data-palette-type="questdb-type-router"] .red-ui-palette-label { color: #ffffff !important; }
|
|
4
|
+
.red-ui-palette-node[data-palette-type="questdb-type-router"] .red-ui-palette-icon-container { background-color: #f0f0f0 !important; }
|
|
4
5
|
</style>
|
|
5
6
|
|
|
6
7
|
<script type="text/javascript">
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
inputs:1,
|
|
14
15
|
outputs:5,
|
|
15
16
|
outputLabels: ["long", "double", "bool", "string", "unresolvable"],
|
|
16
|
-
icon: "
|
|
17
|
+
icon: "questdb-logo.png",
|
|
17
18
|
paletteLabel: "Type Router",
|
|
18
19
|
label: function() { return this.name || "Type Router"; },
|
|
19
20
|
labelStyle: function() { return (this.name ? "node_label_italic" : "") + " questdb-white-text"; }
|