node-red-contrib-questdb 0.6.6 → 0.6.8
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.
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
const typeSelect = $('<select/>', {class: "column-type"})
|
|
101
101
|
.css({width: "20%"})
|
|
102
102
|
.appendTo(row);
|
|
103
|
-
['auto', 'float', 'double', 'integer', 'long', 'decimal', 'string', 'boolean', 'timestamp', 'array'].forEach(function(t) {
|
|
103
|
+
['auto', 'float', 'double', 'integer', 'long', 'decimal', 'varchar', 'string', 'boolean', 'timestamp', 'array'].forEach(function(t) {
|
|
104
104
|
$('<option/>').val(t).text(t).appendTo(typeSelect);
|
|
105
105
|
});
|
|
106
106
|
typeSelect.val(data.type || "auto");
|
|
@@ -211,7 +211,8 @@
|
|
|
211
211
|
<li><b>integer</b> - 32-bit signed integer</li>
|
|
212
212
|
<li><b>long</b> - 64-bit signed integer</li>
|
|
213
213
|
<li><b>decimal</b> - Arbitrary precision decimal</li>
|
|
214
|
-
<li><b>
|
|
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>
|
|
215
216
|
<li><b>boolean</b> - true/false</li>
|
|
216
217
|
<li><b>timestamp</b> - Date/time value</li>
|
|
217
218
|
<li><b>array</b> - Array of doubles (double[])</li>
|
package/nodes/questdb-mapper.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
category: 'questdb',
|
|
9
9
|
color: '#a54a7b',
|
|
10
10
|
defaults: {
|
|
11
|
-
name: {value:""}
|
|
11
|
+
name: {value:""},
|
|
12
|
+
contextName: {value: "questdbTypeMap"}
|
|
12
13
|
},
|
|
13
14
|
inputs:1,
|
|
14
|
-
outputs:
|
|
15
|
-
outputLabels: ["
|
|
15
|
+
outputs:5,
|
|
16
|
+
outputLabels: ["long", "double", "bool", "string", "unresolvable"],
|
|
16
17
|
icon: "font-awesome/fa-random",
|
|
17
18
|
paletteLabel: "Type Router",
|
|
18
19
|
label: function() { return this.name || "Type Router"; },
|
|
@@ -25,24 +26,38 @@
|
|
|
25
26
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
26
27
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
27
28
|
</div>
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-contextName"><i class="fa fa-database"></i> Type Map</label>
|
|
31
|
+
<input type="text" id="node-input-contextName" placeholder="questdbTypeMap">
|
|
32
|
+
</div>
|
|
28
33
|
</script>
|
|
29
34
|
|
|
30
35
|
<script type="text/html" data-help-name="questdb-type-router">
|
|
31
|
-
<p>Routes messages to different outputs based on payload data type.</p>
|
|
36
|
+
<p>Routes messages to different outputs based on payload data type, with per-topic type latching.</p>
|
|
37
|
+
|
|
32
38
|
<h3>Outputs</h3>
|
|
33
39
|
<ol class="node-ports">
|
|
34
|
-
<li><b>
|
|
35
|
-
<li><b>
|
|
40
|
+
<li><b>Long:</b> Integer values (int and long both map here)</li>
|
|
41
|
+
<li><b>Double:</b> Floating-point values (float and double both map here)</li>
|
|
36
42
|
<li><b>Bool:</b> Boolean values (including "true"/"false" strings)</li>
|
|
37
|
-
<li><b>String:</b> String values</li>
|
|
43
|
+
<li><b>String:</b> String values that are not numbers or booleans</li>
|
|
44
|
+
<li><b>Unresolvable:</b> Values whose type cannot be determined, or that cannot be cast to the latched type for their topic</li>
|
|
38
45
|
</ol>
|
|
46
|
+
|
|
39
47
|
<h3>Type Detection</h3>
|
|
40
48
|
<ul>
|
|
41
|
-
<li>
|
|
42
|
-
<li>
|
|
43
|
-
<li>
|
|
44
|
-
<li>String
|
|
49
|
+
<li><b>Bool:</b> <code>true</code>, <code>false</code>, or strings <code>"true"</code>/<code>"false"</code> (case-insensitive)</li>
|
|
50
|
+
<li><b>Long:</b> Whole numbers or numeric strings without a decimal point (e.g. <code>42</code>, <code>"42"</code>)</li>
|
|
51
|
+
<li><b>Double:</b> Numbers with a decimal point or numeric strings with <code>.</code> (e.g. <code>3.14</code>, <code>"3.14"</code>)</li>
|
|
52
|
+
<li><b>String:</b> Non-numeric, non-boolean strings</li>
|
|
53
|
+
<li><b>Unresolvable:</b> Objects, arrays, <code>null</code>, <code>NaN</code>, <code>Infinity</code></li>
|
|
45
54
|
</ul>
|
|
55
|
+
|
|
56
|
+
<h3>Type Latching</h3>
|
|
57
|
+
<p>When a message includes <code>msg.topic</code>, the detected type is stored in the global context under the configured <b>Type Map</b> name (default: <code>questdbTypeMap</code>). The context holds a dictionary of <code>topic → type</code>.</p>
|
|
58
|
+
<p>Subsequent messages with the same topic are cast to the latched type. If the value cannot be cast (e.g. a string arrives for a topic latched as <code>long</code>), the message is routed to the <b>Unresolvable</b> output.</p>
|
|
59
|
+
<p>Messages without a topic are still routed by type but do not update the type map.</p>
|
|
60
|
+
|
|
46
61
|
<h3>Conversion</h3>
|
|
47
|
-
<p>String representations of numbers and booleans are automatically converted to their native types.</p>
|
|
62
|
+
<p>String representations of numbers and booleans are automatically converted to their native types during both auto-detection and latched casting.</p>
|
|
48
63
|
</script>
|
|
@@ -1,71 +1,125 @@
|
|
|
1
|
-
module.exports = function(RED) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
function TypeRouterNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
var node = this;
|
|
5
|
+
node.contextName = config.contextName || 'questdbTypeMap';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
// Detect the canonical type from a value.
|
|
8
|
+
// Returns 'long' | 'double' | 'bool' | 'string' | null (unresolvable)
|
|
9
|
+
function detectType(value) {
|
|
10
|
+
if (typeof value === 'boolean') return 'bool';
|
|
11
|
+
if (typeof value === 'number') {
|
|
12
|
+
if (!isFinite(value)) return null;
|
|
13
|
+
return Number.isInteger(value) ? 'long' : 'double';
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
var trimmed = value.trim();
|
|
17
|
+
var lower = trimmed.toLowerCase();
|
|
18
|
+
if (lower === 'true' || lower === 'false') return 'bool';
|
|
19
|
+
if (trimmed !== '' && !isNaN(trimmed)) {
|
|
20
|
+
return trimmed.includes('.') ? 'double' : 'long';
|
|
21
|
+
}
|
|
22
|
+
return 'string';
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// Cast value to the required type.
|
|
28
|
+
// Returns the casted value, or null if the cast is not possible.
|
|
29
|
+
function castToType(value, type) {
|
|
30
|
+
var num, t;
|
|
31
|
+
if (type === 'long') {
|
|
32
|
+
if (typeof value === 'boolean') return null;
|
|
33
|
+
num = Number(value);
|
|
34
|
+
if (!isFinite(num) || !Number.isInteger(num)) return null;
|
|
35
|
+
return num;
|
|
36
|
+
}
|
|
37
|
+
if (type === 'double') {
|
|
38
|
+
if (typeof value === 'boolean') return null;
|
|
39
|
+
num = Number(value);
|
|
40
|
+
if (!isFinite(num)) return null;
|
|
41
|
+
return num;
|
|
42
|
+
}
|
|
43
|
+
if (type === 'bool') {
|
|
44
|
+
if (typeof value === 'boolean') return value;
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
t = value.trim().toLowerCase();
|
|
47
|
+
if (t === 'true') return true;
|
|
48
|
+
if (t === 'false') return false;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (type === 'string') {
|
|
53
|
+
if (typeof value === 'string') return value;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
27
58
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
msg.dataType = 'bool';
|
|
31
|
-
node.status({ fill: "purple", shape: "dot", text: "bool: true" });
|
|
32
|
-
return node.send([null, null, msg, null]);
|
|
33
|
-
}
|
|
34
|
-
else if (trimmed.toLowerCase() === 'false') {
|
|
35
|
-
msg.payload = false;
|
|
36
|
-
msg.dataType = 'bool';
|
|
37
|
-
node.status({ fill: "purple", shape: "dot", text: "bool: false" });
|
|
38
|
-
return node.send([null, null, msg, null]);
|
|
39
|
-
}
|
|
40
|
-
else if (trimmed !== '' && !isNaN(trimmed)) {
|
|
41
|
-
var num = Number(trimmed);
|
|
42
|
-
msg.payload = num;
|
|
43
|
-
if (Number.isInteger(num) && !trimmed.includes('.')) {
|
|
44
|
-
msg.dataType = 'int';
|
|
45
|
-
node.status({ fill: "blue", shape: "dot", text: "int: " + num });
|
|
46
|
-
return node.send([msg, null, null, null]);
|
|
47
|
-
} else {
|
|
48
|
-
msg.dataType = 'float';
|
|
49
|
-
node.status({ fill: "green", shape: "dot", text: "float: " + num.toFixed(2) });
|
|
50
|
-
return node.send([null, msg, null, null]);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
msg.payload = String(value);
|
|
55
|
-
msg.dataType = 'string';
|
|
56
|
-
node.status({ fill: "yellow", shape: "dot", text: "str: " + (value.length > 10 ? value.substring(0,10) + "..." : value) });
|
|
57
|
-
return node.send([null, null, null, msg]);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
msg.payload = String(value);
|
|
62
|
-
msg.dataType = 'string';
|
|
63
|
-
node.status({ fill: "yellow", shape: "dot", text: "str: (converted)" });
|
|
64
|
-
return node.send([null, null, null, msg]);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
59
|
+
var TYPE_OUTPUT = { long: 0, double: 1, bool: 2, string: 3 };
|
|
60
|
+
var TYPE_COLOR = { long: 'blue', double: 'green', bool: 'purple', string: 'yellow' };
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
node.on('input', function (msg) {
|
|
63
|
+
var topic = msg.topic;
|
|
64
|
+
var value = msg.payload;
|
|
65
|
+
var globalCtx = node.context().global;
|
|
66
|
+
var typeMap = globalCtx.get(node.contextName) || {};
|
|
67
|
+
|
|
68
|
+
var type, castedValue;
|
|
69
|
+
|
|
70
|
+
if (topic && Object.prototype.hasOwnProperty.call(typeMap, topic)) {
|
|
71
|
+
// Latched: use the previously determined type
|
|
72
|
+
type = typeMap[topic];
|
|
73
|
+
if (type === 'unresolvable') {
|
|
74
|
+
node.status({ fill: 'red', shape: 'ring', text: 'unresolvable [' + topic + ']' });
|
|
75
|
+
return node.send([null, null, null, null, msg]);
|
|
76
|
+
}
|
|
77
|
+
castedValue = castToType(value, type);
|
|
78
|
+
if (castedValue === null) {
|
|
79
|
+
node.status({ fill: 'red', shape: 'ring', text: "can't cast [" + topic + ']' });
|
|
80
|
+
return node.send([null, null, null, null, msg]);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// First time: auto-detect and latch
|
|
84
|
+
type = detectType(value);
|
|
85
|
+
if (type === null) {
|
|
86
|
+
if (topic) {
|
|
87
|
+
typeMap[topic] = 'unresolvable';
|
|
88
|
+
globalCtx.set(node.contextName, typeMap);
|
|
89
|
+
}
|
|
90
|
+
node.status({ fill: 'red', shape: 'ring', text: 'unresolvable' });
|
|
91
|
+
return node.send([null, null, null, null, msg]);
|
|
92
|
+
}
|
|
93
|
+
if (topic) {
|
|
94
|
+
typeMap[topic] = type;
|
|
95
|
+
globalCtx.set(node.contextName, typeMap);
|
|
96
|
+
}
|
|
97
|
+
castedValue = castToType(value, type);
|
|
98
|
+
if (castedValue === null) {
|
|
99
|
+
// Guard: detectType and castToType should always be consistent
|
|
100
|
+
node.status({ fill: 'red', shape: 'ring', text: 'cast error' });
|
|
101
|
+
return node.send([null, null, null, null, msg]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
msg.payload = castedValue;
|
|
106
|
+
msg.dataType = type;
|
|
107
|
+
|
|
108
|
+
var outputIndex = TYPE_OUTPUT[type];
|
|
109
|
+
var outputs = [null, null, null, null, null];
|
|
110
|
+
outputs[outputIndex] = msg;
|
|
111
|
+
|
|
112
|
+
var label = String(castedValue);
|
|
113
|
+
if (label.length > 12) label = label.substring(0, 12) + '...';
|
|
114
|
+
node.status({ fill: TYPE_COLOR[type], shape: 'dot', text: type + ': ' + label });
|
|
115
|
+
|
|
116
|
+
return node.send(outputs);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
node.on('close', function () {
|
|
120
|
+
node.status({});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
RED.nodes.registerType('questdb-type-router', TypeRouterNode);
|
|
71
125
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-questdb",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "Node-RED nodes for writing high-performance time-series data to QuestDB using Influx Line Protocol (ILP). Supports IoT, industrial monitoring, smart buildings, fleet telematics, healthcare, agriculture, and more.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Holger Amort"
|