node-red-contrib-questdb 0.6.4 → 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/nodes/questdb.js CHANGED
@@ -167,7 +167,7 @@ module.exports = function (RED) {
167
167
  connectionState.sender = await Sender.fromConfig(connStr);
168
168
 
169
169
  // Wait for TCP transport to be fully ready
170
- await new Promise(resolve => setTimeout(resolve, 100));
170
+ await new Promise(resolve => setTimeout(resolve, 1000));
171
171
 
172
172
  connectionState.updateState(true);
173
173
 
@@ -346,7 +346,16 @@ module.exports = function (RED) {
346
346
  }
347
347
 
348
348
  try {
349
- connection.sender.table(tableName);
349
+ try {
350
+ connection.sender.table(tableName);
351
+ } catch (tableErr) {
352
+ if (tableErr.message && tableErr.message.includes('TCP transport is not connected')) {
353
+ node.error('TCP transport disconnected at table(), reconnecting...', msg);
354
+ connection.updateState(false, tableErr.message);
355
+ return;
356
+ }
357
+ throw tableErr;
358
+ }
350
359
 
351
360
  // Check if simple format (msg.topic + numeric payload)
352
361
  if (
@@ -356,8 +365,17 @@ module.exports = function (RED) {
356
365
  // Simple format: use msg.topic as tag, payload as value
357
366
  var tag = msg.topic || 'default';
358
367
  var value = typeof payload === 'number' ? payload : parseFloat(payload);
359
- connection.sender.symbol('tag_name', tag);
360
- connection.sender.floatColumn('value', value);
368
+ try {
369
+ connection.sender.symbol('tag_name', tag);
370
+ connection.sender.floatColumn('value', value);
371
+ } catch (symbolErr) {
372
+ if (symbolErr.message && symbolErr.message.includes('TCP transport is not connected')) {
373
+ node.error('TCP transport disconnected at symbol/column, reconnecting...', msg);
374
+ connection.updateState(false, symbolErr.message);
375
+ return;
376
+ }
377
+ throw symbolErr;
378
+ }
361
379
 
362
380
  // Handle timestamp
363
381
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-questdb",
3
- "version": "0.6.4",
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"