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>string</b> - Text value</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>
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>
@@ -72,6 +72,7 @@ module.exports = function(RED) {
72
72
  case 'boolean':
73
73
  value = Boolean(value);
74
74
  break;
75
+ case 'varchar':
75
76
  case 'string':
76
77
  value = String(value);
77
78
  break;
@@ -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:4,
15
- outputLabels: ["int", "float", "bool", "string"],
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>Int:</b> Integer values</li>
35
- <li><b>Float:</b> Floating point values</li>
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>Boolean: <code>true</code>, <code>false</code>, or strings "true"/"false"</li>
42
- <li>Integer: Whole numbers without decimal point</li>
43
- <li>Float: Numbers with decimal point</li>
44
- <li>String: Everything else (converted if needed)</li>
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
- function TypeRouterNode(config) {
3
- RED.nodes.createNode(this, config);
4
- var node = this;
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
- node.on('input', function(msg) {
7
- var value = msg.payload;
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
- if (typeof value === 'boolean') {
10
- msg.dataType = 'bool';
11
- node.status({ fill: "purple", shape: "dot", text: "bool: " + value });
12
- return node.send([null, null, msg, null]);
13
- }
14
- else if (typeof value === 'number') {
15
- if (Number.isInteger(value)) {
16
- msg.dataType = 'int';
17
- node.status({ fill: "blue", shape: "dot", text: "int: " + value });
18
- return node.send([msg, null, null, null]);
19
- } else {
20
- msg.dataType = 'float';
21
- node.status({ fill: "green", shape: "dot", text: "float: " + value.toFixed(2) });
22
- return node.send([null, msg, null, null]);
23
- }
24
- }
25
- else if (typeof value === 'string') {
26
- var trimmed = value.trim();
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
- if (trimmed.toLowerCase() === 'true') {
29
- msg.payload = true;
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
- node.on('close', function() { node.status({}); });
69
- }
70
- RED.nodes.registerType("questdb-type-router", TypeRouterNode);
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.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"