Merge remote-tracking branch 'upstream/dev' into proxy-logiv-dev-v4
@@ -227,34 +227,42 @@
|
||||
name: {value:""},
|
||||
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) {
|
||||
if (!v || v.length === 0) { return true }
|
||||
const errors = []
|
||||
for (var i=0;i<v.length;i++) {
|
||||
if (/^\${[^}]+}$/.test(v[i].v)) {
|
||||
// Allow ${ENV_VAR} value
|
||||
continue
|
||||
}
|
||||
if (/msg|flow|global/.test(v[i].vt)) {
|
||||
if (!RED.utils.validatePropertyExpression(v[i].v)) {
|
||||
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
|
||||
}
|
||||
} else if (v[i].vt === "jsonata") {
|
||||
try{ jsonata(v[i].v); }
|
||||
catch(e){
|
||||
return RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message }))
|
||||
}
|
||||
} else if (v[i].vt === "json") {
|
||||
try{ JSON.parse(v[i].v); }
|
||||
catch(e){
|
||||
return RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message }))
|
||||
}
|
||||
} else if (v[i].vt === "num"){
|
||||
if (!/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v[i].v)) {
|
||||
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
|
||||
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return errors
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
repeat: {
|
||||
value:"", validate: function(v, opt) {
|
||||
if ((v === "") ||
|
||||
(RED.validators.number(v) &&
|
||||
(RED.validators.number()(v) &&
|
||||
(v >= 0) && (v <= 2147483))) {
|
||||
return true;
|
||||
}
|
||||
@@ -263,7 +271,7 @@
|
||||
},
|
||||
crontab: {value:""},
|
||||
once: {value:false},
|
||||
onceDelay: {value:0.1},
|
||||
onceDelay: {value:0.1, validate: RED.validators.number(true)},
|
||||
topic: {value:""},
|
||||
payload: {value:"", validate: RED.validators.typedInput("payloadType", false) },
|
||||
payloadType: {value:"date"},
|
||||
@@ -320,7 +328,7 @@
|
||||
}
|
||||
// but replace with repeat one if set to repeat
|
||||
if ((this.repeat && this.repeat != 0) || this.crontab) {
|
||||
suffix = " ↻";
|
||||
suffix = "\t↻";
|
||||
}
|
||||
if (this.name) {
|
||||
return this.name+suffix;
|
||||
|
@@ -95,45 +95,70 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
this.on("input", function(msg, send, done) {
|
||||
var errors = [];
|
||||
var props = this.props;
|
||||
const errors = [];
|
||||
let props = this.props;
|
||||
if (msg.__user_inject_props__ && Array.isArray(msg.__user_inject_props__)) {
|
||||
props = msg.__user_inject_props__;
|
||||
}
|
||||
delete msg.__user_inject_props__;
|
||||
props.forEach(p => {
|
||||
var property = p.p;
|
||||
var value = p.v ? p.v : '';
|
||||
var valueType = p.vt ? p.vt : 'str';
|
||||
|
||||
if (!property) return;
|
||||
|
||||
if (valueType === "jsonata") {
|
||||
if (p.v) {
|
||||
try {
|
||||
var exp = RED.util.prepareJSONataExpression(p.v, node);
|
||||
var val = RED.util.evaluateJSONataExpression(exp, msg);
|
||||
RED.util.setMessageProperty(msg, property, val, true);
|
||||
props = [...props]
|
||||
function evaluateProperty(doneEvaluating) {
|
||||
if (props.length === 0) {
|
||||
doneEvaluating()
|
||||
return
|
||||
}
|
||||
const p = props.shift()
|
||||
const property = p.p;
|
||||
const value = p.v !== undefined ? p.v : '';
|
||||
const valueType = p.vt !== undefined ? p.vt : 'str';
|
||||
if (property) {
|
||||
if (valueType === "jsonata") {
|
||||
if (p.v) {
|
||||
try {
|
||||
var exp = RED.util.prepareJSONataExpression(p.v, node);
|
||||
RED.util.evaluateJSONataExpression(exp, msg, (err, newValue) => {
|
||||
if (err) {
|
||||
errors.push(err.toString())
|
||||
} else {
|
||||
RED.util.setMessageProperty(msg,property,newValue,true);
|
||||
}
|
||||
evaluateProperty(doneEvaluating)
|
||||
});
|
||||
} catch (err) {
|
||||
errors.push(err.message);
|
||||
evaluateProperty(doneEvaluating)
|
||||
}
|
||||
} else {
|
||||
evaluateProperty(doneEvaluating)
|
||||
}
|
||||
catch (err) {
|
||||
errors.push(err.message);
|
||||
} else {
|
||||
try {
|
||||
RED.util.evaluateNodeProperty(value, valueType, node, msg, (err, newValue) => {
|
||||
if (err) {
|
||||
errors.push(err.toString())
|
||||
} else {
|
||||
RED.util.setMessageProperty(msg,property,newValue,true);
|
||||
}
|
||||
evaluateProperty(doneEvaluating)
|
||||
})
|
||||
} catch (err) {
|
||||
errors.push(err.toString());
|
||||
evaluateProperty(doneEvaluating)
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
evaluateProperty(doneEvaluating)
|
||||
}
|
||||
try {
|
||||
RED.util.setMessageProperty(msg,property,RED.util.evaluateNodeProperty(value, valueType, this, msg),true);
|
||||
} catch (err) {
|
||||
errors.push(err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
done(errors.join('; '));
|
||||
} else {
|
||||
send(msg);
|
||||
done();
|
||||
}
|
||||
|
||||
evaluateProperty(() => {
|
||||
if (errors.length) {
|
||||
done(errors.join('; '));
|
||||
} else {
|
||||
send(msg);
|
||||
done();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -86,7 +86,7 @@
|
||||
},
|
||||
label: function() {
|
||||
var suffix = "";
|
||||
if (this.console === true || this.console === "true") { suffix = " ⇲"; }
|
||||
if (this.console === true || this.console === "true") { suffix = "\t⇲"; }
|
||||
if (this.targetType === "jsonata") {
|
||||
return (this.name || "JSONata") + suffix;
|
||||
}
|
||||
@@ -195,6 +195,119 @@
|
||||
node.dirty = true;
|
||||
});
|
||||
RED.view.redraw();
|
||||
},
|
||||
requestDebugNodeList: function(filteredNodes) {
|
||||
var workspaceOrder = RED.nodes.getWorkspaceOrder();
|
||||
var workspaceOrderMap = {};
|
||||
workspaceOrder.forEach(function(ws,i) {
|
||||
workspaceOrderMap[ws] = i;
|
||||
});
|
||||
|
||||
var candidateNodes = [];
|
||||
var candidateSFs = [];
|
||||
var subflows = {};
|
||||
RED.nodes.eachNode(function (n) {
|
||||
var nt = n.type;
|
||||
if (nt === "debug") {
|
||||
if (n.z in workspaceOrderMap) {
|
||||
candidateNodes.push(n);
|
||||
}
|
||||
else {
|
||||
var sf = RED.nodes.subflow(n.z);
|
||||
if (sf) {
|
||||
subflows[sf.id] = {
|
||||
debug: true,
|
||||
subflows: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(nt.substring(0, 8) === "subflow:") {
|
||||
if (n.z in workspaceOrderMap) {
|
||||
candidateSFs.push(n);
|
||||
}
|
||||
else {
|
||||
var psf = RED.nodes.subflow(n.z);
|
||||
if (psf) {
|
||||
var sid = nt.substring(8);
|
||||
var item = subflows[psf.id];
|
||||
if (!item) {
|
||||
item = {
|
||||
debug: undefined,
|
||||
subflows: {}
|
||||
};
|
||||
subflows[psf.id] = item;
|
||||
}
|
||||
item.subflows[sid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
candidateSFs.forEach(function (sf) {
|
||||
var sid = sf.type.substring(8);
|
||||
if (containsDebug(sid, subflows)) {
|
||||
candidateNodes.push(sf);
|
||||
}
|
||||
});
|
||||
|
||||
candidateNodes.sort(function(A,B) {
|
||||
var wsA = workspaceOrderMap[A.z];
|
||||
var wsB = workspaceOrderMap[B.z];
|
||||
if (wsA !== wsB) {
|
||||
return wsA-wsB;
|
||||
}
|
||||
var labelA = RED.utils.getNodeLabel(A,A.id);
|
||||
var labelB = RED.utils.getNodeLabel(B,B.id);
|
||||
return labelA.localeCompare(labelB);
|
||||
});
|
||||
var currentWs = null;
|
||||
var data = [];
|
||||
var currentFlow;
|
||||
var currentSelectedCount = 0;
|
||||
candidateNodes.forEach(function(node) {
|
||||
if (currentWs !== node.z) {
|
||||
if (currentFlow && currentFlow.checkbox) {
|
||||
currentFlow.selected = currentSelectedCount === currentFlow.children.length
|
||||
}
|
||||
currentSelectedCount = 0;
|
||||
currentWs = node.z;
|
||||
var parent = RED.nodes.workspace(currentWs) || RED.nodes.subflow(currentWs);
|
||||
currentFlow = {
|
||||
label: RED.utils.getNodeLabel(parent, currentWs),
|
||||
}
|
||||
if (!parent.disabled) {
|
||||
currentFlow.children = [];
|
||||
currentFlow.checkbox = true;
|
||||
} else {
|
||||
currentFlow.class = "disabled"
|
||||
}
|
||||
data.push(currentFlow);
|
||||
}
|
||||
if (currentFlow.children) {
|
||||
if (!filteredNodes[node.id]) {
|
||||
currentSelectedCount++;
|
||||
}
|
||||
currentFlow.children.push({
|
||||
label: RED.utils.getNodeLabel(node,node.id),
|
||||
node: {
|
||||
id: node.id
|
||||
},
|
||||
checkbox: true,
|
||||
selected: !filteredNodes[node.id]
|
||||
});
|
||||
}
|
||||
});
|
||||
if (currentFlow && currentFlow.checkbox) {
|
||||
currentFlow.selected = currentSelectedCount === currentFlow.children.length
|
||||
}
|
||||
if (subWindow) {
|
||||
try {
|
||||
subWindow.postMessage({event:"refreshDebugNodeList", nodes:data},"*");
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
RED.debug.refreshDebugNodeList(data)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -396,6 +509,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
function containsDebug(sid, map) {
|
||||
var item = map[sid];
|
||||
if (item) {
|
||||
if (item.debug === undefined) {
|
||||
var sfs = Object.keys(item.subflows);
|
||||
var contain = false;
|
||||
for (var i = 0; i < sfs.length; i++) {
|
||||
var sf = sfs[i];
|
||||
if (containsDebug(sf, map)) {
|
||||
contain = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.debug = contain;
|
||||
}
|
||||
return item.debug;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$("#red-ui-sidebar-debug-open").on("click", function(e) {
|
||||
e.preventDefault();
|
||||
subWindow = window.open(document.location.toString().replace(/[?#].*$/,"")+"debug/view/view.html"+document.location.search,"nodeREDDebugView","menubar=no,location=no,toolbar=no,chrome,height=500,width=600");
|
||||
@@ -427,6 +560,8 @@
|
||||
options.messageSourceClick(msg.id,msg._alias,msg.path);
|
||||
} else if (msg.event === "clear") {
|
||||
options.clear();
|
||||
} else if (msg.event === "requestDebugNodeList") {
|
||||
options.requestDebugNodeList(msg.filteredNodes)
|
||||
}
|
||||
};
|
||||
window.addEventListener('message',this.handleWindowMessage);
|
||||
|
@@ -5,6 +5,7 @@ module.exports = function(RED) {
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
var debuglength = RED.settings.debugMaxLength || 1000;
|
||||
var statuslength = RED.settings.debugStatusLength || 32;
|
||||
var useColors = RED.settings.debugUseColors || false;
|
||||
util.inspect.styles.boolean = "red";
|
||||
const { hasOwnProperty } = Object.prototype;
|
||||
@@ -164,7 +165,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
|
||||
if (st.length > 32) { st = st.substr(0,32) + "..."; }
|
||||
if (st.length > statuslength) { st = st.substr(0,statuslength) + "..."; }
|
||||
|
||||
var newStatus = {fill:fill, shape:shape, text:st};
|
||||
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script type="text/html" data-template-name="complete">
|
||||
<div class="form-row node-input-target-row">
|
||||
<button id="node-input-complete-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
|
||||
<button type="button" id="node-input-complete-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
|
||||
</div>
|
||||
<div class="form-row node-input-target-row node-input-target-list-row" style="position: relative; min-height: 100px">
|
||||
<div style="position: absolute; top: -30px; right: 0;"><input type="text" id="node-input-complete-target-filter"></div>
|
||||
@@ -18,7 +18,16 @@
|
||||
color:"#c0edc0",
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
scope: {value:[], type:"*[]"},
|
||||
scope: {
|
||||
value: [],
|
||||
type: "*[]",
|
||||
validate: function (v, opt) {
|
||||
if (v.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return RED._("node-red:complete.errors.scopeUndefined");
|
||||
}
|
||||
},
|
||||
uncaught: {value:false}
|
||||
},
|
||||
inputs:0,
|
||||
|
@@ -4,7 +4,8 @@
|
||||
<label style="width: auto" for="node-input-scope" data-i18n="catch.label.source"></label>
|
||||
<select id="node-input-scope-select">
|
||||
<option value="all" data-i18n="catch.scope.all"></option>
|
||||
<option value="target" data-i18n="catch.scope.selected"></options>
|
||||
<option value="group" data-i18n="catch.scope.group"></option>
|
||||
<option value="target" data-i18n="catch.scope.selected"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row node-input-uncaught-row">
|
||||
@@ -12,7 +13,7 @@
|
||||
<label for="node-input-uncaught" style="width: auto" data-i18n="catch.label.uncaught"></label>
|
||||
</div>
|
||||
<div class="form-row node-input-target-row">
|
||||
<button id="node-input-catch-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
|
||||
<button type="button" id="node-input-catch-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
|
||||
</div>
|
||||
<div class="form-row node-input-target-row node-input-target-list-row" style="position: relative; min-height: 100px">
|
||||
<div style="position: absolute; top: -30px; right: 0;"><input type="text" id="node-input-catch-target-filter"></div>
|
||||
@@ -40,7 +41,9 @@
|
||||
if (this.name) {
|
||||
return this.name;
|
||||
}
|
||||
if (this.scope) {
|
||||
if (this.scope === "group") {
|
||||
return this._("catch.catchGroup");
|
||||
} else if (Array.isArray(this.scope)) {
|
||||
return this._("catch.catchNodes",{number:this.scope.length});
|
||||
}
|
||||
return this.uncaught?this._("catch.catchUncaught"):this._("catch.catch")
|
||||
@@ -170,6 +173,8 @@
|
||||
});
|
||||
if (this.scope === null) {
|
||||
$("#node-input-scope-select").val("all");
|
||||
} else if(this.scope === "group"){
|
||||
$("#node-input-scope-select").val("group");
|
||||
} else {
|
||||
$("#node-input-scope-select").val("target");
|
||||
}
|
||||
@@ -179,6 +184,8 @@
|
||||
var scope = $("#node-input-scope-select").val();
|
||||
if (scope === 'all') {
|
||||
this.scope = null;
|
||||
} else if(scope === 'group') {
|
||||
this.scope = "group";
|
||||
} else {
|
||||
$("#node-input-uncaught").prop("checked",false);
|
||||
this.scope = $("#node-input-catch-target-container-div").treeList('selected').map(function(i) { return i.node.id})
|
||||
|
@@ -4,11 +4,12 @@
|
||||
<label style="width: auto" for="node-input-scope" data-i18n="status.label.source"></label>
|
||||
<select id="node-input-scope-select">
|
||||
<option value="all" data-i18n="status.scope.all"></option>
|
||||
<option value="target" data-i18n="status.scope.selected"></options>
|
||||
<option value="group" data-i18n="status.scope.group"></option>
|
||||
<option value="target" data-i18n="status.scope.selected"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row node-input-target-row">
|
||||
<button id="node-input-status-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
|
||||
<button type="button" id="node-input-status-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
|
||||
</div>
|
||||
<div class="form-row node-input-target-row node-input-target-list-row" style="position: relative; min-height: 100px">
|
||||
<div style="position: absolute; top: -30px; right: 0;"><input type="text" id="node-input-status-target-filter"></div>
|
||||
@@ -32,7 +33,15 @@
|
||||
outputs:1,
|
||||
icon: "status.svg",
|
||||
label: function() {
|
||||
return this.name||(this.scope?this._("status.statusNodes",{number:this.scope.length}):this._("status.status"));
|
||||
if (this.name) {
|
||||
return this.name;
|
||||
}
|
||||
if (this.scope === "group") {
|
||||
return this._("status.statusGroup");
|
||||
} else if (Array.isArray(this.scope)) {
|
||||
return this._("status.statusNodes",{number:this.scope.length});
|
||||
}
|
||||
return this._("status.status")
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name?"node_label_italic":"";
|
||||
@@ -157,6 +166,8 @@
|
||||
});
|
||||
if (this.scope === null) {
|
||||
$("#node-input-scope-select").val("all");
|
||||
} else if(this.scope === "group"){
|
||||
$("#node-input-scope-select").val("group");
|
||||
} else {
|
||||
$("#node-input-scope-select").val("target");
|
||||
}
|
||||
@@ -166,6 +177,8 @@
|
||||
var scope = $("#node-input-scope-select").val();
|
||||
if (scope === 'all') {
|
||||
this.scope = null;
|
||||
} else if(scope === 'group') {
|
||||
this.scope = "group";
|
||||
} else {
|
||||
this.scope = $("#node-input-status-target-container-div").treeList('selected').map(function(i) { return i.node.id})
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
<script type="text/html" data-template-name="link in">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
@@ -29,7 +28,7 @@
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-timeout"><span data-i18n="exec.label.timeout"></span></label>
|
||||
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="exec.label.timeout"></span></label>
|
||||
<input type="text" id="node-input-timeout" placeholder="30" style="width: 70px; margin-right: 5px;"><span data-i18n="inject.seconds"></span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
@@ -272,7 +271,17 @@
|
||||
color:"#ddd",//"#87D8CF",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
links: { value: [], type:"link in[]" },
|
||||
links: {
|
||||
value: [],
|
||||
type: "link in[]",
|
||||
validate: function (v, opt) {
|
||||
if (((this.linkType || "static") === "static" && v.length > 0)
|
||||
|| this.linkType === "dynamic") {
|
||||
return true;
|
||||
}
|
||||
return RED._("node-red:link.errors.linkUndefined");
|
||||
}
|
||||
},
|
||||
linkType: { value:"static" },
|
||||
timeout: {
|
||||
value: "30",
|
||||
|
@@ -164,10 +164,10 @@ module.exports = function(RED) {
|
||||
if (returnNode && returnNode.returnLinkMessage) {
|
||||
returnNode.returnLinkMessage(messageEvent.id, msg);
|
||||
} else {
|
||||
node.warn(RED._("link.error.missingReturn"))
|
||||
node.warn(RED._("link.errors.missingReturn"));
|
||||
}
|
||||
} else {
|
||||
node.warn(RED._("link.error.missingReturn"))
|
||||
node.warn(RED._("link.errors.missingReturn"));
|
||||
}
|
||||
done();
|
||||
} else if (mode === "link") {
|
||||
@@ -248,6 +248,14 @@ module.exports = function(RED) {
|
||||
}
|
||||
});
|
||||
|
||||
this.on("close", function () {
|
||||
for (const event of Object.values(messageEvents)) {
|
||||
if (event.ts) {
|
||||
clearTimeout(event.ts)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.returnLinkMessage = function(eventId, msg) {
|
||||
if (Array.isArray(msg._linkSource) && msg._linkSource.length === 0) {
|
||||
delete msg._linkSource;
|
||||
|
27
packages/node_modules/@node-red/nodes/core/common/91-global-config.html
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<script type="text/html" data-template-name="global-config">
|
||||
<div class="form-row">
|
||||
<label style="width: 100%"><span data-i18n="global-config.label.open-conf"></span>:</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button class="red-ui-button" type="button" id="node-input-edit-env-var" data-i18n="editor:env-var.header" style="margin-left: 20px"></button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('global-config',{
|
||||
category: 'config',
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
env: { value: [] },
|
||||
},
|
||||
credentials: {
|
||||
map: { type: "map" }
|
||||
},
|
||||
oneditprepare: function() {
|
||||
$('#node-input-edit-env-var').on('click', function(evt) {
|
||||
RED.actions.invoke('core:show-user-settings', 'envvar')
|
||||
});
|
||||
},
|
||||
hasUsers: false
|
||||
});
|
||||
</script>
|
7
packages/node_modules/@node-red/nodes/core/common/91-global-config.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = function(RED) {
|
||||
"use strict";
|
||||
function GlobalConfigNode(n) {
|
||||
RED.nodes.createNode(this,n);
|
||||
}
|
||||
RED.nodes.registerType("global-config", GlobalConfigNode);
|
||||
}
|
@@ -167,19 +167,13 @@ RED.debug = (function() {
|
||||
var menu = RED.popover.menu({
|
||||
options: options,
|
||||
onselect: function(item) {
|
||||
if (item.value !== filterType) {
|
||||
filterType = item.value;
|
||||
$('#red-ui-sidebar-debug-filter span').text(RED._('node-red:debug.sidebar.'+filterType));
|
||||
refreshMessageList();
|
||||
RED.settings.set("debug.filter",filterType)
|
||||
}
|
||||
setFilterType(item.value)
|
||||
if (filterType === 'filterSelected') {
|
||||
refreshDebugNodeList();
|
||||
config.requestDebugNodeList(filteredNodes);
|
||||
filterDialog.slideDown(200);
|
||||
filterDialogShown = true;
|
||||
debugNodeTreeList.focus();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
menu.show({
|
||||
@@ -254,131 +248,7 @@ RED.debug = (function() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function containsDebug(sid, map) {
|
||||
var item = map[sid];
|
||||
if (item) {
|
||||
if (item.debug === undefined) {
|
||||
var sfs = Object.keys(item.subflows);
|
||||
var contain = false;
|
||||
for (var i = 0; i < sfs.length; i++) {
|
||||
var sf = sfs[i];
|
||||
if (containsDebug(sf, map)) {
|
||||
contain = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.debug = contain;
|
||||
}
|
||||
return item.debug;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function refreshDebugNodeList() {
|
||||
var workspaceOrder = RED.nodes.getWorkspaceOrder();
|
||||
var workspaceOrderMap = {};
|
||||
workspaceOrder.forEach(function(ws,i) {
|
||||
workspaceOrderMap[ws] = i;
|
||||
});
|
||||
|
||||
var candidateNodes = [];
|
||||
var candidateSFs = [];
|
||||
var subflows = {};
|
||||
RED.nodes.eachNode(function (n) {
|
||||
var nt = n.type;
|
||||
if (nt === "debug") {
|
||||
if (n.z in workspaceOrderMap) {
|
||||
candidateNodes.push(n);
|
||||
}
|
||||
else {
|
||||
var sf = RED.nodes.subflow(n.z);
|
||||
if (sf) {
|
||||
subflows[sf.id] = {
|
||||
debug: true,
|
||||
subflows: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(nt.substring(0, 8) === "subflow:") {
|
||||
if (n.z in workspaceOrderMap) {
|
||||
candidateSFs.push(n);
|
||||
}
|
||||
else {
|
||||
var psf = RED.nodes.subflow(n.z);
|
||||
if (psf) {
|
||||
var sid = nt.substring(8);
|
||||
var item = subflows[psf.id];
|
||||
if (!item) {
|
||||
item = {
|
||||
debug: undefined,
|
||||
subflows: {}
|
||||
};
|
||||
subflows[psf.id] = item;
|
||||
}
|
||||
item.subflows[sid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
candidateSFs.forEach(function (sf) {
|
||||
var sid = sf.type.substring(8);
|
||||
if (containsDebug(sid, subflows)) {
|
||||
candidateNodes.push(sf);
|
||||
}
|
||||
});
|
||||
|
||||
candidateNodes.sort(function(A,B) {
|
||||
var wsA = workspaceOrderMap[A.z];
|
||||
var wsB = workspaceOrderMap[B.z];
|
||||
if (wsA !== wsB) {
|
||||
return wsA-wsB;
|
||||
}
|
||||
var labelA = RED.utils.getNodeLabel(A,A.id);
|
||||
var labelB = RED.utils.getNodeLabel(B,B.id);
|
||||
return labelA.localeCompare(labelB);
|
||||
});
|
||||
var currentWs = null;
|
||||
var data = [];
|
||||
var currentFlow;
|
||||
var currentSelectedCount = 0;
|
||||
candidateNodes.forEach(function(node) {
|
||||
if (currentWs !== node.z) {
|
||||
if (currentFlow && currentFlow.checkbox) {
|
||||
currentFlow.selected = currentSelectedCount === currentFlow.children.length
|
||||
}
|
||||
currentSelectedCount = 0;
|
||||
currentWs = node.z;
|
||||
var parent = RED.nodes.workspace(currentWs) || RED.nodes.subflow(currentWs);
|
||||
currentFlow = {
|
||||
label: RED.utils.getNodeLabel(parent, currentWs),
|
||||
}
|
||||
if (!parent.disabled) {
|
||||
currentFlow.children = [];
|
||||
currentFlow.checkbox = true;
|
||||
} else {
|
||||
currentFlow.class = "disabled"
|
||||
}
|
||||
data.push(currentFlow);
|
||||
}
|
||||
if (currentFlow.children) {
|
||||
if (!filteredNodes[node.id]) {
|
||||
currentSelectedCount++;
|
||||
}
|
||||
currentFlow.children.push({
|
||||
label: RED.utils.getNodeLabel(node,node.id),
|
||||
node: node,
|
||||
checkbox: true,
|
||||
selected: !filteredNodes[node.id]
|
||||
});
|
||||
}
|
||||
});
|
||||
if (currentFlow && currentFlow.checkbox) {
|
||||
currentFlow.selected = currentSelectedCount === currentFlow.children.length
|
||||
}
|
||||
|
||||
function refreshDebugNodeList(data) {
|
||||
debugNodeTreeList.treeList("data", data);
|
||||
}
|
||||
|
||||
@@ -401,7 +271,7 @@ RED.debug = (function() {
|
||||
},200);
|
||||
}
|
||||
function _refreshMessageList(_activeWorkspace) {
|
||||
if (_activeWorkspace) {
|
||||
if (typeof _activeWorkspace === 'string') {
|
||||
activeWorkspace = _activeWorkspace.replace(/\./g,"_");
|
||||
}
|
||||
if (filterType === "filterAll") {
|
||||
@@ -479,12 +349,12 @@ RED.debug = (function() {
|
||||
filteredNodes[n.id] = true;
|
||||
});
|
||||
delete filteredNodes[sourceId];
|
||||
$("#red-ui-sidebar-debug-filterSelected").trigger("click");
|
||||
RED.settings.set('debug.filteredNodes',Object.keys(filteredNodes))
|
||||
setFilterType('filterSelected')
|
||||
refreshMessageList();
|
||||
}},
|
||||
{id:"red-ui-debug-msg-menu-item-clear-filter",label:RED._("node-red:debug.messageMenu.clearFilter"),onselect:function(){
|
||||
$("#red-ui-sidebar-debug-filterAll").trigger("click");
|
||||
clearFilterSettings()
|
||||
refreshMessageList();
|
||||
}}
|
||||
);
|
||||
@@ -642,7 +512,8 @@ RED.debug = (function() {
|
||||
hideKey: false,
|
||||
path: path,
|
||||
sourceId: sourceNode&&sourceNode.id,
|
||||
rootPath: path
|
||||
rootPath: path,
|
||||
nodeSelector: config.messageSourceClick,
|
||||
});
|
||||
// Do this in a separate step so the element functions aren't stripped
|
||||
debugMessage.appendTo(el);
|
||||
@@ -713,9 +584,17 @@ RED.debug = (function() {
|
||||
if (!!clearFilter) {
|
||||
clearFilterSettings();
|
||||
}
|
||||
refreshDebugNodeList();
|
||||
config.requestDebugNodeList(filteredNodes);
|
||||
}
|
||||
|
||||
function setFilterType(type) {
|
||||
if (type !== filterType) {
|
||||
filterType = type;
|
||||
$('#red-ui-sidebar-debug-filter span').text(RED._('node-red:debug.sidebar.'+filterType));
|
||||
refreshMessageList();
|
||||
RED.settings.set("debug.filter",filterType)
|
||||
}
|
||||
}
|
||||
function clearFilterSettings() {
|
||||
filteredNodes = {};
|
||||
filterType = 'filterAll';
|
||||
@@ -728,6 +607,7 @@ RED.debug = (function() {
|
||||
init: init,
|
||||
refreshMessageList:refreshMessageList,
|
||||
handleDebugMessage: handleDebugMessage,
|
||||
clearMessageList: clearMessageList
|
||||
clearMessageList: clearMessageList,
|
||||
refreshDebugNodeList: refreshDebugNodeList
|
||||
}
|
||||
})();
|
||||
|
@@ -12,6 +12,9 @@ $(function() {
|
||||
},
|
||||
clear: function() {
|
||||
window.opener.postMessage({event:"clear"},'*');
|
||||
},
|
||||
requestDebugNodeList: function(filteredNodes) {
|
||||
window.opener.postMessage({event: 'requestDebugNodeList', filteredNodes},'*')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +29,8 @@ $(function() {
|
||||
RED.debug.refreshMessageList(evt.data.activeWorkspace);
|
||||
} else if (evt.data.event === "projectChange") {
|
||||
RED.debug.clearMessageList(true);
|
||||
} else if (evt.data.event === "refreshDebugNodeList") {
|
||||
RED.debug.refreshDebugNodeList(evt.data.nodes)
|
||||
}
|
||||
},false);
|
||||
} catch(err) {
|
||||
|
@@ -17,6 +17,8 @@
|
||||
display: flex;
|
||||
background: var(--red-ui-tertiary-background);
|
||||
padding-right: 75px;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
#node-input-libs-container-row .red-ui-editableList-header > div {
|
||||
flex-grow: 1;
|
||||
@@ -75,9 +77,15 @@
|
||||
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
|
||||
|
||||
<div id="func-tab-config" style="display:none">
|
||||
<div class="form-row">
|
||||
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
||||
<input id="node-input-outputs" style="width: 60px;" value="1">
|
||||
<div>
|
||||
<div class="form-row" style="display: inline-block; margin-right: 50px;">
|
||||
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
||||
<input id="node-input-outputs" style="width: 60px;" value="1">
|
||||
</div>
|
||||
<div class="form-row" style="display: inline-block;">
|
||||
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="function.label.timeout"></span></label>
|
||||
<input id="node-input-timeout" style="width: 60px;" data-i18n="[placeholder]join.seconds">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
|
||||
@@ -91,21 +99,21 @@
|
||||
<div id="func-tab-init" style="display:none">
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
|
||||
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button type="button" id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="func-tab-body" style="display:none">
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
|
||||
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button type="button" id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="func-tab-finalize" style="display:none">
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
|
||||
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button type="button" id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -294,7 +302,7 @@
|
||||
if (val === "_custom_") {
|
||||
val = $(this).val();
|
||||
}
|
||||
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/].?/g, function(v) { return v[1]?v[1].toUpperCase():"" });
|
||||
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/\.].?/g, function(v) { return v[1]?v[1].toUpperCase():"" });
|
||||
fvar.val(varName);
|
||||
fvar.trigger("change");
|
||||
|
||||
@@ -358,6 +366,7 @@
|
||||
name: {value:"_DEFAULT_"},
|
||||
func: {value:"\nreturn msg;"},
|
||||
outputs: {value:1},
|
||||
timeout:{value:RED.settings.functionTimeout || 0},
|
||||
noerr: {value:0,required:true,
|
||||
validate: function(v, opt) {
|
||||
if (!v) {
|
||||
@@ -451,11 +460,33 @@
|
||||
tabs.activateTab("func-tab-body");
|
||||
|
||||
$( "#node-input-outputs" ).spinner({
|
||||
min:0,
|
||||
min: 0,
|
||||
max: 500,
|
||||
change: function(event, ui) {
|
||||
var value = parseInt(this.value);
|
||||
value = isNaN(value) ? 1 : value;
|
||||
value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
|
||||
value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
|
||||
if (value !== this.value) { $(this).spinner("value", value); }
|
||||
}
|
||||
});
|
||||
|
||||
// 4294967 is max in node.js timeout.
|
||||
$( "#node-input-timeout" ).spinner({
|
||||
min: 0,
|
||||
max: 4294967,
|
||||
change: function(event, ui) {
|
||||
var value = this.value;
|
||||
if (!value.match(/^\d+$/)) { value = 1; }
|
||||
else if (value < this.min) { value = this.min; }
|
||||
if(value == ""){
|
||||
value = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = parseInt(value);
|
||||
}
|
||||
value = isNaN(value) ? 1 : value;
|
||||
value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
|
||||
value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
|
||||
if (value !== this.value) { $(this).spinner("value", value); }
|
||||
}
|
||||
});
|
||||
@@ -499,7 +530,7 @@
|
||||
editor:this.editor, // the field name the main text body goes to
|
||||
mode:"ace/mode/nrjavascript",
|
||||
fields:[
|
||||
'name', 'outputs',
|
||||
'name', 'outputs', 'timeout',
|
||||
{
|
||||
name: 'initialize',
|
||||
get: function() {
|
||||
|
@@ -96,6 +96,13 @@ module.exports = function(RED) {
|
||||
node.name = n.name;
|
||||
node.func = n.func;
|
||||
node.outputs = n.outputs;
|
||||
node.timeout = n.timeout*1000;
|
||||
if(node.timeout>0){
|
||||
node.timeoutOptions = {
|
||||
timeout:node.timeout,
|
||||
breakOnSigint:true
|
||||
}
|
||||
}
|
||||
node.ini = n.initialize ? n.initialize.trim() : "";
|
||||
node.fin = n.finalize ? n.finalize.trim() : "";
|
||||
node.libs = n.libs || [];
|
||||
@@ -308,7 +315,7 @@ module.exports = function(RED) {
|
||||
var spec = module.module;
|
||||
if (spec && (spec !== "")) {
|
||||
moduleLoadPromises.push(RED.import(module.module).then(lib => {
|
||||
sandbox[vname] = lib.default;
|
||||
sandbox[vname] = lib.default || lib;
|
||||
}).catch(err => {
|
||||
node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:err.toString()}))
|
||||
throw err;
|
||||
@@ -362,6 +369,10 @@ module.exports = function(RED) {
|
||||
})(__initSend__);`;
|
||||
iniOpt = createVMOpt(node, " setup");
|
||||
iniScript = new vm.Script(iniText, iniOpt);
|
||||
if(node.timeout>0){
|
||||
iniOpt.timeout = node.timeout;
|
||||
iniOpt.breakOnSigint = true;
|
||||
}
|
||||
}
|
||||
node.script = vm.createScript(functionText, createVMOpt(node, ""));
|
||||
if (node.fin && (node.fin !== "")) {
|
||||
@@ -385,6 +396,10 @@ module.exports = function(RED) {
|
||||
})();`;
|
||||
finOpt = createVMOpt(node, " cleanup");
|
||||
finScript = new vm.Script(finText, finOpt);
|
||||
if(node.timeout>0){
|
||||
finOpt.timeout = node.timeout;
|
||||
finOpt.breakOnSigint = true;
|
||||
}
|
||||
}
|
||||
var promise = Promise.resolve();
|
||||
if (iniScript) {
|
||||
@@ -396,9 +411,12 @@ module.exports = function(RED) {
|
||||
var start = process.hrtime();
|
||||
context.msg = msg;
|
||||
context.__send__ = send;
|
||||
context.__done__ = done;
|
||||
|
||||
node.script.runInContext(context);
|
||||
context.__done__ = done;
|
||||
var opts = {};
|
||||
if (node.timeout>0){
|
||||
opts = node.timeoutOptions;
|
||||
}
|
||||
node.script.runInContext(context,opts);
|
||||
context.results.then(function(results) {
|
||||
sendResults(node,send,msg._msgid,results,false);
|
||||
if (handleNodeDoneCall) {
|
||||
@@ -503,7 +521,8 @@ module.exports = function(RED) {
|
||||
RED.nodes.registerType("function",FunctionNode, {
|
||||
dynamicModuleList: "libs",
|
||||
settings: {
|
||||
functionExternalModules: { value: true, exportable: true }
|
||||
functionExternalModules: { value: true, exportable: true },
|
||||
functionTimeout: { value:0, exportable: true }
|
||||
}
|
||||
});
|
||||
RED.library.register("functions");
|
||||
|
@@ -167,7 +167,35 @@
|
||||
label:RED._("node-red:common.label.payload"),
|
||||
validate: RED.validators.typedInput("propertyType", false)},
|
||||
propertyType: { value:"msg" },
|
||||
rules: {value:[{t:"eq", v:"", vt:"str"}]},
|
||||
rules: {
|
||||
value:[{t:"eq", v:"", vt:"str"}],
|
||||
validate: function (rules, opt) {
|
||||
let msg;
|
||||
const errors = []
|
||||
if (!rules || rules.length === 0) { return true }
|
||||
for (var i=0;i<rules.length;i++) {
|
||||
const opt = { label: RED._('node-red:switch.label.rule')+' '+(i+1) }
|
||||
const r = rules[i];
|
||||
if (r.t !== 'istype') {
|
||||
if (r.hasOwnProperty('v')) {
|
||||
if ((msg = RED.utils.validateTypedProperty(r.v,r.vt,opt)) !== true) {
|
||||
errors.push(msg)
|
||||
}
|
||||
}
|
||||
if (r.hasOwnProperty('v2')) {
|
||||
if ((msg = RED.utils.validateTypedProperty(r.v2,r.v2t,opt)) !== true) {
|
||||
errors.push(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length) {
|
||||
console.log(errors)
|
||||
return errors
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
checkall: {value:"true", required:true},
|
||||
repair: {value:false},
|
||||
outputs: {value:1}
|
||||
@@ -217,7 +245,11 @@
|
||||
if (i > 0) {
|
||||
var lastRule = $("#node-input-rule-container").editableList('getItemAt',i-1);
|
||||
var exportedRule = exportRule(lastRule.element);
|
||||
opt.r.vt = exportedRule.vt;
|
||||
if (exportedRule.t === "istype") {
|
||||
opt.r.vt = (exportedRule.vt === "number") ? "num" : "str";
|
||||
} else {
|
||||
opt.r.vt = exportedRule.vt;
|
||||
}
|
||||
opt.r.v = "";
|
||||
// We could copy the value over as well and preselect it (see the 'activeElement' code below)
|
||||
// But not sure that feels right. Is copying over the last value 'expected' behaviour?
|
||||
|
@@ -19,71 +19,42 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
function isInvalidProperty(v,vt) {
|
||||
if (/msg|flow|global/.test(vt)) {
|
||||
if (!RED.utils.validatePropertyExpression(v)) {
|
||||
return RED._("node-red:change.errors.invalid-prop", {
|
||||
property: v
|
||||
});
|
||||
}
|
||||
} else if (vt === "jsonata") {
|
||||
try{ jsonata(v); } catch(e) {
|
||||
return RED._("node-red:change.errors.invalid-expr", {
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
} else if (vt === "json") {
|
||||
try{ JSON.parse(v); } catch(e) {
|
||||
return RED._("node-red:change.errors.invalid-json-data", {
|
||||
error: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RED.nodes.registerType('change', {
|
||||
color: "#E2D96E",
|
||||
category: 'function',
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
rules:{value:[{t:"set",p:"payload",pt:"msg",to:"",tot:"str"}],validate: function(rules, opt) {
|
||||
var msg;
|
||||
if (!rules || rules.length === 0) { return true }
|
||||
for (var i=0;i<rules.length;i++) {
|
||||
var r = rules[i];
|
||||
if (r.t === 'set') {
|
||||
if (msg = isInvalidProperty(r.p,r.pt)) {
|
||||
return msg;
|
||||
rules:{
|
||||
value:[{t:"set",p:"payload",pt:"msg",to:"",tot:"str"}],
|
||||
validate: function(rules, opt) {
|
||||
let msg;
|
||||
const errors = []
|
||||
if (!rules || rules.length === 0) { return true }
|
||||
for (var i=0;i<rules.length;i++) {
|
||||
const opt = { label: RED._('node-red:change.label.rule')+' '+(i+1) }
|
||||
const r = rules[i];
|
||||
if (r.t === 'set' || r.t === 'change' || r.t === 'delete' || r.t === 'move') {
|
||||
if ((msg = RED.utils.validateTypedProperty(r.p,r.pt,opt)) !== true) {
|
||||
errors.push(msg)
|
||||
}
|
||||
}
|
||||
if (msg = isInvalidProperty(r.to,r.tot)) {
|
||||
return msg;
|
||||
if (r.t === 'set' || r.t === 'change' || r.t === 'move') {
|
||||
if ((msg = RED.utils.validateTypedProperty(r.to,r.tot,opt)) !== true) {
|
||||
errors.push(msg)
|
||||
}
|
||||
}
|
||||
} else if (r.t === 'change') {
|
||||
if (msg = isInvalidProperty(r.p,r.pt)) {
|
||||
return msg;
|
||||
}
|
||||
if(msg = isInvalidProperty(r.from,r.fromt)) {
|
||||
return msg;
|
||||
}
|
||||
if(msg = isInvalidProperty(r.to,r.tot)) {
|
||||
return msg;
|
||||
}
|
||||
} else if (r.t === 'delete') {
|
||||
if (msg = isInvalidProperty(r.p,r.pt)) {
|
||||
return msg;
|
||||
}
|
||||
} else if (r.t === 'move') {
|
||||
if (msg = isInvalidProperty(r.p,r.pt)) {
|
||||
return msg;
|
||||
}
|
||||
if (msg = isInvalidProperty(r.to,r.tot)) {
|
||||
return msg;
|
||||
if (r.t === 'change') {
|
||||
if ((msg = RED.utils.validateTypedProperty(r.from,r.fromt,opt)) !== true) {
|
||||
errors.push(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length) {
|
||||
return errors
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}},
|
||||
},
|
||||
// legacy
|
||||
action: {value:""},
|
||||
property: {value:""},
|
||||
|
@@ -117,7 +117,7 @@ module.exports = function(RED) {
|
||||
});
|
||||
return
|
||||
} else if (rule.tot === 'date') {
|
||||
value = Date.now();
|
||||
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
|
||||
} else if (rule.tot === 'jsonata') {
|
||||
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
|
||||
if (err) {
|
||||
@@ -233,7 +233,9 @@ module.exports = function(RED) {
|
||||
// only replace if they match exactly
|
||||
RED.util.setMessageProperty(msg,property,value);
|
||||
} else {
|
||||
current = current.replace(fromRE,value);
|
||||
// if target is boolean then just replace it
|
||||
if (rule.tot === "bool") { current = value; }
|
||||
else { current = current.replace(fromRE,value); }
|
||||
RED.util.setMessageProperty(msg,property,current);
|
||||
}
|
||||
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
|
||||
@@ -318,7 +320,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
var r = node.rules[currentRule];
|
||||
if (r.t === "move") {
|
||||
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) {
|
||||
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1) && (r.p !== r.to)) {
|
||||
applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt},(err,msg) => {
|
||||
applyRule(msg,{t:"delete", p:r.p, pt:r.pt}, (err,msg) => {
|
||||
completeApplyingRules(msg,currentRule,done);
|
||||
|
@@ -10,6 +10,7 @@
|
||||
<option value="scale" data-i18n="range.scale.payload"></option>
|
||||
<option value="clamp" data-i18n="range.scale.limit"></option>
|
||||
<option value="roll" data-i18n="range.scale.wrap"></option>
|
||||
<option value="drop" data-i18n="range.scale.drop"></option>
|
||||
</select>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -56,7 +57,9 @@
|
||||
action: {value:"scale"},
|
||||
round: {value:false},
|
||||
property: {value:"payload",required:true,
|
||||
label:RED._("node-red:common.label.property")},
|
||||
label:RED._("node-red:common.label.property"),
|
||||
validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })
|
||||
},
|
||||
name: {value:""}
|
||||
},
|
||||
inputs: 1,
|
||||
|
@@ -32,11 +32,15 @@ module.exports = function(RED) {
|
||||
if (value !== undefined) {
|
||||
var n = Number(value);
|
||||
if (!isNaN(n)) {
|
||||
if (node.action == "clamp") {
|
||||
if (node.action === "drop") {
|
||||
if (n < node.minin) { done(); return; }
|
||||
if (n > node.maxin) { done(); return; }
|
||||
}
|
||||
if (node.action === "clamp") {
|
||||
if (n < node.minin) { n = node.minin; }
|
||||
if (n > node.maxin) { n = node.maxin; }
|
||||
}
|
||||
if (node.action == "roll") {
|
||||
if (node.action === "roll") {
|
||||
var divisor = node.maxin - node.minin;
|
||||
n = ((n - node.minin) % divisor + divisor) % divisor + node.minin;
|
||||
}
|
||||
|
@@ -21,12 +21,13 @@
|
||||
<option value="javascript">JavaScript</option>
|
||||
<option value="css">CSS</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="php">PHP</option>
|
||||
<option value="python">Python</option>
|
||||
<option value="sql">SQL</option>
|
||||
<option value="yaml">YAML</option>
|
||||
<option value="text" data-i18n="template.label.none"></option>
|
||||
</select>
|
||||
<button id="node-template-expand-editor" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button>
|
||||
<button type="button" id="node-template-expand-editor" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row node-text-editor-row">
|
||||
@@ -152,7 +153,7 @@
|
||||
}
|
||||
var editorRow = $("#dialog-form>div.node-text-editor-row");
|
||||
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
|
||||
$(".node-text-editor").css("height",height+"px");
|
||||
$("#dialog-form .node-text-editor").css("height",height+"px");
|
||||
this.editor.resize();
|
||||
}
|
||||
});
|
||||
|
@@ -229,6 +229,7 @@ module.exports = function(RED) {
|
||||
node.on("input", function(msg, send, done) {
|
||||
if (!node.drop) {
|
||||
var m = RED.util.cloneMessage(msg);
|
||||
delete m.flush;
|
||||
if (Object.keys(m).length > 1) {
|
||||
if (node.intervalID !== -1) {
|
||||
if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate)) && node.rate !== m.rate) {
|
||||
@@ -283,7 +284,7 @@ module.exports = function(RED) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (!msg.hasOwnProperty("reset")) {
|
||||
if (maxKeptMsgsCount(node) > 0) {
|
||||
if (node.intervalID === -1) {
|
||||
node.send(msg);
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<select id="node-then-type" style="width:70%;">
|
||||
<option value="block" data-i18n="trigger.wait-reset"></option>
|
||||
<option value="wait" data-i18n="trigger.wait-for"></option>
|
||||
<option value="loop" data-i18n="trigger.wait-loop"></option>
|
||||
<option id="node-trigger-wait-loop" value="loop" data-i18n="trigger.wait-loop"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row node-type-duration">
|
||||
@@ -181,6 +181,13 @@
|
||||
$("#node-input-op2type").val('str');
|
||||
}
|
||||
|
||||
$("#node-input-op1").on("change", function() {
|
||||
if ($("#node-input-op1type").val() === "nul") {
|
||||
$("#node-trigger-wait-loop").hide();
|
||||
}
|
||||
else { $("#node-trigger-wait-loop").show(); }
|
||||
});
|
||||
|
||||
var optionNothing = {value:"nul",label:this._("trigger.output.nothing"),hasValue:false};
|
||||
var optionPayload = {value:"pay",label:this._("trigger.output.existing"),hasValue:false};
|
||||
var optionOriginalPayload = {value:"pay",label:this._("trigger.output.original"),hasValue:false};
|
||||
|
@@ -56,7 +56,7 @@
|
||||
color:"darksalmon",
|
||||
defaults: {
|
||||
command: {value:""},
|
||||
addpay: {value:""},
|
||||
addpay: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
|
||||
append: {value:""},
|
||||
useSpawn: {value:"false"},
|
||||
timer: {value:""},
|
||||
|
@@ -56,9 +56,11 @@
|
||||
inout: {value:"out"},
|
||||
septopics: {value:true},
|
||||
property: {value:"payload", required:true,
|
||||
label:RED._("node-red:rbe.label.property")},
|
||||
label:RED._("node-red:rbe.label.property"),
|
||||
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })},
|
||||
topi: {value:"topic", required:true,
|
||||
label:RED._("node-red:rbe.label.topic")}
|
||||
label:RED._("node-red:rbe.label.topic"),
|
||||
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
|
@@ -35,7 +35,11 @@ module.exports = function(RED) {
|
||||
}
|
||||
else { node.previous = {}; }
|
||||
}
|
||||
var value = RED.util.getMessageProperty(msg,node.property);
|
||||
var value;
|
||||
try {
|
||||
value = RED.util.getMessageProperty(msg,node.property);
|
||||
}
|
||||
catch(e) { }
|
||||
if (value !== undefined) {
|
||||
var t = "_no_topic";
|
||||
if (node.septopics) { t = topic || t; }
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<label class="red-ui-button" for="node-config-input-certfile"><i class="fa fa-upload"></i> <span data-i18n="tls.label.upload"></span></label>
|
||||
<input class="hide" type="file" id="node-config-input-certfile">
|
||||
<span id="tls-config-certname" style="width: calc(100% - 280px); overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
|
||||
<button class="red-ui-button red-ui-button-small" id="tls-config-button-cert-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
|
||||
<button type="button" class="red-ui-button red-ui-button-small" id="tls-config-button-cert-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
|
||||
</span>
|
||||
<input type="hidden" id="node-config-input-certname">
|
||||
<input type="hidden" id="node-config-input-certdata">
|
||||
@@ -37,7 +37,7 @@
|
||||
<label class="red-ui-button" for="node-config-input-keyfile"><i class="fa fa-upload"></i> <span data-i18n="tls.label.upload"></span></label>
|
||||
<input class="hide" type="file" id="node-config-input-keyfile">
|
||||
<span id="tls-config-keyname" style="width: calc(100% - 280px); overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
|
||||
<button class="red-ui-button red-ui-button-small" id="tls-config-button-key-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
|
||||
<button type="button" class="red-ui-button red-ui-button-small" id="tls-config-button-key-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
|
||||
</span>
|
||||
<input type="hidden" id="node-config-input-keyname">
|
||||
<input type="hidden" id="node-config-input-keydata">
|
||||
@@ -53,7 +53,7 @@
|
||||
<label class="red-ui-button" for="node-config-input-cafile"><i class="fa fa-upload"></i> <span data-i18n="tls.label.upload"></span></label>
|
||||
<input class="hide" type="file" title=" " id="node-config-input-cafile">
|
||||
<span id="tls-config-caname" style="width: calc(100% - 280px); overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
|
||||
<button class="red-ui-button red-ui-button-small" id="tls-config-button-ca-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
|
||||
<button type="button" class="red-ui-button red-ui-button-small" id="tls-config-button-ca-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
|
||||
</span>
|
||||
<input type="hidden" id="node-config-input-caname">
|
||||
<input type="hidden" id="node-config-input-cadata">
|
||||
|
@@ -101,6 +101,7 @@
|
||||
hostField.val(data.host);
|
||||
}
|
||||
},
|
||||
sortable: true,
|
||||
removable: true
|
||||
});
|
||||
if (this.noproxy) {
|
||||
|
@@ -249,6 +249,12 @@
|
||||
<span id="node-config-input-cleansession-label" data-i18n="mqtt.label.cleansession"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row mqtt-persistence">
|
||||
<label for="node-config-input-autoUnsubscribe" style="width: auto;">
|
||||
<input type="checkbox" id="node-config-input-autoUnsubscribe" style="position: relative;vertical-align: bottom; top: -2px; width: 15px;height: 15px;">
|
||||
<span id="node-config-input-autoUnsubscribe-label" data-i18n="mqtt.label.autoUnsubscribe"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row mqtt5">
|
||||
<label style="width:auto" for="node-config-input-sessionExpiry"><span data-i18n="mqtt.label.sessionExpiry"></span></label>
|
||||
<input type="number" min="0" id="node-config-input-sessionExpiry" style="width: 100px" >
|
||||
@@ -421,7 +427,11 @@
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
|
||||
var typedInputNoneOpt = { value: 'none', label: '', hasValue: false };
|
||||
var typedInputNoneOpt = {
|
||||
value: 'none',
|
||||
label: RED._("node-red:mqtt.label.none"),
|
||||
hasValue: false
|
||||
};
|
||||
var makeTypedInputOpt = function(value){
|
||||
return {
|
||||
value: value,
|
||||
@@ -436,7 +446,11 @@
|
||||
makeTypedInputOpt("text/csv"),
|
||||
makeTypedInputOpt("text/html"),
|
||||
makeTypedInputOpt("text/plain"),
|
||||
{value:"other", label:""}
|
||||
{
|
||||
value: "other",
|
||||
label: RED._("node-red:mqtt.label.other"),
|
||||
icon: "red/images/typedInput/az.svg"
|
||||
}
|
||||
];
|
||||
|
||||
function getDefaultContentType(value) {
|
||||
@@ -475,17 +489,23 @@
|
||||
tls: {type:"tls-config",required: false,
|
||||
label:RED._("node-red:mqtt.label.use-tls") },
|
||||
clientid: {value:"", validate: function(v, opt) {
|
||||
var ok = false;
|
||||
let ok = true;
|
||||
if ($("#node-config-input-clientid").length) {
|
||||
// Currently editing the node
|
||||
ok = $("#node-config-input-cleansession").is(":checked") || (v||"").length > 0;
|
||||
let needClientId = !$("#node-config-input-cleansession").is(":checked")
|
||||
if (needClientId) {
|
||||
ok = (v||"").length > 0;
|
||||
}
|
||||
} else {
|
||||
ok = (this.cleansession===undefined || this.cleansession) || (v||"").length > 0;
|
||||
let needClientId = !(this.cleansession===undefined || this.cleansession)
|
||||
if (needClientId) {
|
||||
ok = (v||"").length > 0;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
return ok;
|
||||
if (!ok) {
|
||||
return RED._("node-red:mqtt.errors.invalid-client-id");
|
||||
}
|
||||
return RED._("node-red:mqtt.errors.invalid-client-id");
|
||||
return true;
|
||||
}},
|
||||
autoConnect: {value: true},
|
||||
usetls: {value: false},
|
||||
@@ -497,19 +517,20 @@
|
||||
label: RED._("node-red:mqtt.label.keepalive"),
|
||||
validate:RED.validators.number(false)},
|
||||
cleansession: {value: true},
|
||||
autoUnsubscribe: {value: true},
|
||||
birthTopic: {value:"", validate:validateMQTTPublishTopic},
|
||||
birthQos: {value:"0"},
|
||||
birthRetain: {value:false},
|
||||
birthRetain: {value:"false"},
|
||||
birthPayload: {value:""},
|
||||
birthMsg: { value: {}},
|
||||
closeTopic: {value:"", validate:validateMQTTPublishTopic},
|
||||
closeQos: {value:"0"},
|
||||
closeRetain: {value:false},
|
||||
closeRetain: {value:"false"},
|
||||
closePayload: {value:""},
|
||||
closeMsg: { value: {}},
|
||||
willTopic: {value:"", validate:validateMQTTPublishTopic},
|
||||
willQos: {value:"0"},
|
||||
willRetain: {value:false},
|
||||
willRetain: {value:"false"},
|
||||
willPayload: {value:""},
|
||||
willMsg: { value: {}},
|
||||
userProps: { value: ""},
|
||||
@@ -612,6 +633,10 @@
|
||||
this.cleansession = true;
|
||||
$("#node-config-input-cleansession").prop("checked",true);
|
||||
}
|
||||
if (typeof this.autoUnsubscribe === 'undefined') {
|
||||
this.autoUnsubscribe = true;
|
||||
$("#node-config-input-autoUnsubscribe").prop("checked",true);
|
||||
}
|
||||
if (typeof this.usetls === 'undefined') {
|
||||
this.usetls = false;
|
||||
$("#node-config-input-usetls").prop("checked",false);
|
||||
@@ -627,6 +652,14 @@
|
||||
if (typeof this.protocolVersion === 'undefined') {
|
||||
this.protocolVersion = 4;
|
||||
}
|
||||
$("#node-config-input-cleansession").on("change", function() {
|
||||
const useCleanSession = $("#node-config-input-cleansession").is(':checked');
|
||||
if(useCleanSession) {
|
||||
$("div.form-row.mqtt-persistence").hide();
|
||||
} else {
|
||||
$("div.form-row.mqtt-persistence").show();
|
||||
}
|
||||
});
|
||||
$("#node-config-input-protocolVersion").on("change", function() {
|
||||
var v5 = $("#node-config-input-protocolVersion").val() == "5";
|
||||
if(v5) {
|
||||
|
@@ -25,7 +25,6 @@ module.exports = function(RED) {
|
||||
"text/css":"string",
|
||||
"text/html":"string",
|
||||
"text/plain":"string",
|
||||
"text/html":"string",
|
||||
"application/json":"json",
|
||||
"application/octet-stream":"buffer",
|
||||
"application/pdf":"buffer",
|
||||
@@ -106,6 +105,7 @@ module.exports = function(RED) {
|
||||
* @returns `true` if it is a valid topic
|
||||
*/
|
||||
function isValidPublishTopic(topic) {
|
||||
if (topic.length === 0) return false;
|
||||
return !/[\+#\b\f\n\r\t\v\0]/.test(topic);
|
||||
}
|
||||
|
||||
@@ -220,8 +220,10 @@ module.exports = function(RED) {
|
||||
* Handle the payload / packet recieved in MQTT In and MQTT Sub nodes
|
||||
*/
|
||||
function subscriptionHandler(node, datatype ,topic, payload, packet) {
|
||||
const v5 = node.brokerConn.options && node.brokerConn.options.protocolVersion == 5;
|
||||
var msg = {topic:topic, payload:null, qos:packet.qos, retain:packet.retain};
|
||||
const msg = {topic:topic, payload:null, qos:packet.qos, retain:packet.retain};
|
||||
const v5 = (node && node.brokerConn)
|
||||
? node.brokerConn.v5()
|
||||
: Object.prototype.hasOwnProperty.call(packet, "properties");
|
||||
if(v5 && packet.properties) {
|
||||
setStrProp(packet.properties, msg, "responseTopic");
|
||||
setBufferProp(packet.properties, msg, "correlationData");
|
||||
@@ -296,12 +298,12 @@ module.exports = function(RED) {
|
||||
/* mute error - it simply isnt JSON, just leave payload as a string */
|
||||
}
|
||||
}
|
||||
} //else {
|
||||
} //else {
|
||||
//leave as buffer
|
||||
//}
|
||||
}
|
||||
msg.payload = payload;
|
||||
if ((node.brokerConn.broker === "localhost")||(node.brokerConn.broker === "127.0.0.1")) {
|
||||
if (node.brokerConn && (node.brokerConn.broker === "localhost" || node.brokerConn.broker === "127.0.0.1")) {
|
||||
msg._topic = topic;
|
||||
}
|
||||
node.send(msg);
|
||||
@@ -358,7 +360,7 @@ module.exports = function(RED) {
|
||||
return;
|
||||
}
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
@@ -367,6 +369,16 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(node, allNodes) {
|
||||
let setStatus = setStatusDisconnected
|
||||
if(node.connecting) {
|
||||
setStatus = setStatusConnecting
|
||||
} else if(node.connected) {
|
||||
setStatus = setStatusConnected
|
||||
}
|
||||
setStatus(node, allNodes)
|
||||
}
|
||||
|
||||
function setStatusDisconnected(node, allNodes) {
|
||||
if(allNodes) {
|
||||
for (var id in node.users) {
|
||||
@@ -403,6 +415,12 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the connect action
|
||||
* @param {MQTTInNode|MQTTOutNode} node
|
||||
* @param {Object} msg
|
||||
* @param {Function} done
|
||||
*/
|
||||
function handleConnectAction(node, msg, done) {
|
||||
let actionData = typeof msg.broker === 'object' ? msg.broker : null;
|
||||
if (node.brokerConn.canConnect()) {
|
||||
@@ -433,12 +451,17 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the disconnect action
|
||||
* @param {MQTTInNode|MQTTOutNode} node
|
||||
* @param {Function} done
|
||||
*/
|
||||
function handleDisconnectAction(node, done) {
|
||||
node.brokerConn.disconnect(function () {
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
const unsubscribeCandidates = {}
|
||||
//#endregion "Supporting functions"
|
||||
|
||||
//#region "Broker node"
|
||||
@@ -460,7 +483,6 @@ module.exports = function(RED) {
|
||||
if(!opts || typeof opts !== "object") {
|
||||
return; //nothing to change, simply return
|
||||
}
|
||||
const originalBrokerURL = node.brokerurl;
|
||||
|
||||
//apply property changes (only if the property exists in the opts object)
|
||||
setIfHasProperty(opts, node, "url", init);
|
||||
@@ -469,13 +491,12 @@ module.exports = function(RED) {
|
||||
setIfHasProperty(opts, node, "clientid", init);
|
||||
setIfHasProperty(opts, node, "autoConnect", init);
|
||||
setIfHasProperty(opts, node, "usetls", init);
|
||||
setIfHasProperty(opts, node, "usews", init);
|
||||
setIfHasProperty(opts, node, "verifyservercert", init);
|
||||
setIfHasProperty(opts, node, "compatmode", init);
|
||||
setIfHasProperty(opts, node, "protocolVersion", init);
|
||||
setIfHasProperty(opts, node, "keepalive", init);
|
||||
setIfHasProperty(opts, node, "cleansession", init);
|
||||
setIfHasProperty(opts, node, "sessionExpiry", init);
|
||||
setIfHasProperty(opts, node, "autoUnsubscribe", init);
|
||||
setIfHasProperty(opts, node, "topicAliasMaximum", init);
|
||||
setIfHasProperty(opts, node, "maximumPacketSize", init);
|
||||
setIfHasProperty(opts, node, "receiveMaximum", init);
|
||||
@@ -485,6 +506,11 @@ module.exports = function(RED) {
|
||||
} else if (hasProperty(opts, "userProps")) {
|
||||
node.userProperties = opts.userProps;
|
||||
}
|
||||
if (hasProperty(opts, "sessionExpiry")) {
|
||||
node.sessionExpiryInterval = opts.sessionExpiry;
|
||||
} else if (hasProperty(opts, "sessionExpiryInterval")) {
|
||||
node.sessionExpiryInterval = opts.sessionExpiryInterval
|
||||
}
|
||||
|
||||
function createLWT(topic, payload, qos, retain, v5opts, v5SubPropName) {
|
||||
let message = undefined;
|
||||
@@ -568,9 +594,6 @@ module.exports = function(RED) {
|
||||
if (typeof node.usetls === 'undefined') {
|
||||
node.usetls = false;
|
||||
}
|
||||
if (typeof node.usews === 'undefined') {
|
||||
node.usews = false;
|
||||
}
|
||||
if (typeof node.verifyservercert === 'undefined') {
|
||||
node.verifyservercert = false;
|
||||
}
|
||||
@@ -582,13 +605,15 @@ module.exports = function(RED) {
|
||||
if (typeof node.cleansession === 'undefined') {
|
||||
node.cleansession = true;
|
||||
}
|
||||
|
||||
if (typeof node.autoUnsubscribe !== 'boolean') {
|
||||
node.autoUnsubscribe = true;
|
||||
}
|
||||
//use url or build a url from usetls://broker:port
|
||||
if (node.url && node.brokerurl !== node.url) {
|
||||
node.brokerurl = node.url;
|
||||
} else {
|
||||
// if the broker is ws:// or wss:// or tcp://
|
||||
if (node.broker.indexOf("://") > -1) {
|
||||
if ((typeof node.broker === 'string') && node.broker.indexOf("://") > -1) {
|
||||
node.brokerurl = node.broker;
|
||||
// Only for ws or wss, check if proxy env var for additional configuration
|
||||
if (node.brokerurl.indexOf("wss://") > -1 || node.brokerurl.indexOf("ws://") > -1) {
|
||||
@@ -680,7 +705,8 @@ module.exports = function(RED) {
|
||||
node.options.rejectUnauthorized = (node.verifyservercert == "true" || node.verifyservercert === true);
|
||||
}
|
||||
}
|
||||
|
||||
node.v5 = () => node.options && node.options.protocolVersion == 5
|
||||
node.subscriptionIdentifiersAvailable = () => node.v5() && node.serverProperties && node.serverProperties.subscriptionIdentifiersAvailable
|
||||
n.autoConnect = n.autoConnect === "false" || n.autoConnect === false ? false : true;
|
||||
node.setOptions(n, true);
|
||||
|
||||
@@ -690,16 +716,21 @@ module.exports = function(RED) {
|
||||
if (Object.keys(node.users).length === 1) {
|
||||
if(node.autoConnect) {
|
||||
node.connect();
|
||||
//update nodes status
|
||||
setTimeout(function() {
|
||||
updateStatus(node, true)
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.deregister = function(mqttNode,done) {
|
||||
node.deregister = function(mqttNode, done, autoDisconnect) {
|
||||
delete node.users[mqttNode.id];
|
||||
if (!node.closing && node.connected && Object.keys(node.users).length === 0) {
|
||||
node.disconnect();
|
||||
if (autoDisconnect && !node.closing && node.connected && Object.keys(node.users).length === 0) {
|
||||
node.disconnect(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
done();
|
||||
};
|
||||
node.canConnect = function() {
|
||||
return !node.connected && !node.connecting;
|
||||
@@ -757,24 +788,19 @@ module.exports = function(RED) {
|
||||
// Re-subscribe to stored topics
|
||||
for (var s in node.subscriptions) {
|
||||
if (node.subscriptions.hasOwnProperty(s)) {
|
||||
let topic = s;
|
||||
let qos = 0;
|
||||
let _options = {};
|
||||
for (var r in node.subscriptions[s]) {
|
||||
if (node.subscriptions[s].hasOwnProperty(r)) {
|
||||
qos = Math.max(qos,node.subscriptions[s][r].qos);
|
||||
_options = node.subscriptions[s][r].options;
|
||||
node._clientOn('message',node.subscriptions[s][r].handler);
|
||||
node.subscribe(node.subscriptions[s][r])
|
||||
}
|
||||
}
|
||||
_options.qos = _options.qos || qos;
|
||||
node.client.subscribe(topic, _options);
|
||||
}
|
||||
}
|
||||
|
||||
// Send any birth message
|
||||
if (node.birthMessage) {
|
||||
node.publish(node.birthMessage);
|
||||
setTimeout(() => {
|
||||
node.publish(node.birthMessage);
|
||||
}, 1);
|
||||
}
|
||||
});
|
||||
node._clientOn("reconnect", function() {
|
||||
@@ -828,22 +854,28 @@ module.exports = function(RED) {
|
||||
if(!node.client) { return _callback(); }
|
||||
if(node.closing) { return _callback(); }
|
||||
|
||||
/**
|
||||
* Call end and wait for the client to end (or timeout)
|
||||
* @param {mqtt.MqttClient} client The broker client
|
||||
* @param {number} ms The time to wait for the client to end
|
||||
* @returns
|
||||
*/
|
||||
let waitEnd = (client, ms) => {
|
||||
return new Promise( (resolve, reject) => {
|
||||
node.closing = true;
|
||||
if(!client) {
|
||||
if (!client) {
|
||||
resolve();
|
||||
} else {
|
||||
} else {
|
||||
const t = setTimeout(() => {
|
||||
//clean end() has exceeded WAIT_END, lets force end!
|
||||
client && client.end(true);
|
||||
reject();
|
||||
resolve();
|
||||
}, ms);
|
||||
client.end(() => {
|
||||
clearTimeout(t);
|
||||
resolve()
|
||||
});
|
||||
}
|
||||
clearTimeout(t);
|
||||
resolve()
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
if(node.connected && node.closeMessage) {
|
||||
@@ -864,64 +896,222 @@ module.exports = function(RED) {
|
||||
}
|
||||
node.subscriptionIds = {};
|
||||
node.subid = 1;
|
||||
node.subscribe = function (topic,options,callback,ref) {
|
||||
ref = ref||0;
|
||||
var qos;
|
||||
if(typeof options == "object") {
|
||||
qos = options.qos;
|
||||
} else {
|
||||
qos = options;
|
||||
options = {};
|
||||
|
||||
//typedef for subscription object:
|
||||
/**
|
||||
* @typedef {Object} Subscription
|
||||
* @property {String} topic - topic to subscribe to
|
||||
* @property {Object} [options] - options object
|
||||
* @property {Number} [options.qos] - quality of service
|
||||
* @property {Number} [options.nl] - no local
|
||||
* @property {Number} [options.rap] - retain as published
|
||||
* @property {Number} [options.rh] - retain handling
|
||||
* @property {Number} [options.properties] - MQTT 5.0 properties
|
||||
* @property {Number} [options.properties.subscriptionIdentifier] - MQTT 5.0 subscription identifier
|
||||
* @property {Number} [options.properties.userProperties] - MQTT 5.0 user properties
|
||||
* @property {Function} callback
|
||||
* @property {String} ref - reference to the node that created the subscription
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a subscription object
|
||||
* @param {String} _topic - topic to subscribe to
|
||||
* @param {Object} _options - options object
|
||||
* @param {String} _ref - reference to the node that created the subscription
|
||||
* @returns {Subscription}
|
||||
*/
|
||||
function createSubscriptionObject(_topic, _options, _ref, _brokerId) {
|
||||
/** @type {Subscription} */
|
||||
const subscription = {};
|
||||
const ref = _ref || 0;
|
||||
let options
|
||||
let qos = 1 // default to QoS 1 (AWS and several other brokers don't support QoS 2)
|
||||
|
||||
// if options is an object, then clone it
|
||||
if (typeof _options == "object") {
|
||||
options = RED.util.cloneMessage(_options || {})
|
||||
qos = _options.qos;
|
||||
} else if (typeof _options == "number") {
|
||||
qos = _options;
|
||||
}
|
||||
options.qos = qos;
|
||||
options = options || {};
|
||||
|
||||
// sanitise qos
|
||||
if (typeof qos === "number" && qos >= 0 && qos <= 2) {
|
||||
options.qos = qos;
|
||||
}
|
||||
|
||||
subscription.topic = _topic;
|
||||
subscription.qos = qos;
|
||||
subscription.options = RED.util.cloneMessage(options);
|
||||
subscription.ref = ref;
|
||||
subscription.brokerId = _brokerId;
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* If topic is a subscription object, then use that, otherwise look up the topic in
|
||||
* the subscriptions object. If the topic is not found, then create a new subscription
|
||||
* object and add it to the subscriptions object.
|
||||
* @param {Subscription|String} topic
|
||||
* @param {*} options
|
||||
* @param {*} callback
|
||||
* @param {*} ref
|
||||
*/
|
||||
node.subscribe = function (topic, options, callback, ref) {
|
||||
/** @type {Subscription} */
|
||||
let subscription
|
||||
let doCompare = false
|
||||
let changesFound = false
|
||||
|
||||
// function signature 1: subscribe(subscription: Subscription)
|
||||
if (typeof topic === "object" && topic !== null) {
|
||||
subscription = topic
|
||||
topic = subscription.topic
|
||||
options = subscription.options
|
||||
ref = subscription.ref
|
||||
callback = subscription.callback
|
||||
}
|
||||
|
||||
// function signature 2: subscribe(topic: String, options: Object, callback: Function, ref: String)
|
||||
else if (typeof topic === "string") {
|
||||
// since this is a call where all params are provided, it might be
|
||||
// a node change (modification) so we need to check for changes
|
||||
doCompare = true
|
||||
subscription = node.subscriptions[topic] && node.subscriptions[topic][ref]
|
||||
}
|
||||
|
||||
// bad function call
|
||||
else {
|
||||
console.warn('Invalid call to node.subscribe')
|
||||
return
|
||||
}
|
||||
const thisBrokerId = node.type === 'mqtt-broker' ? node.id : node.broker
|
||||
|
||||
// unsubscribe topics where the broker has changed
|
||||
const oldBrokerSubs = (unsubscribeCandidates[ref] || []).filter(sub => sub.brokerId !== thisBrokerId)
|
||||
oldBrokerSubs.forEach(sub => {
|
||||
/** @type {MQTTBrokerNode} */
|
||||
const _brokerConn = RED.nodes.getNode(sub.brokerId)
|
||||
if (_brokerConn) {
|
||||
_brokerConn.unsubscribe(sub.topic, sub.ref, true)
|
||||
}
|
||||
})
|
||||
|
||||
// if subscription is found (or sent in as a parameter), then check for changes.
|
||||
// if there are any changes requested, tidy up the old subscription
|
||||
if (subscription) {
|
||||
if (doCompare) {
|
||||
// compare the current sub to the passed in parameters. Use RED.util.compareObjects against
|
||||
// only the minimal set of properties to identify if the subscription has changed
|
||||
const currentSubscription = createSubscriptionObject(subscription.topic, subscription.options, subscription.ref)
|
||||
const newSubscription = createSubscriptionObject(topic, options, ref)
|
||||
changesFound = RED.util.compareObjects(currentSubscription, newSubscription) === false
|
||||
}
|
||||
}
|
||||
|
||||
if (changesFound) {
|
||||
if (subscription.handler) {
|
||||
node._clientRemoveListeners('message', subscription.handler)
|
||||
subscription.handler = null
|
||||
}
|
||||
const _brokerConn = RED.nodes.getNode(subscription.brokerId)
|
||||
if (_brokerConn) {
|
||||
_brokerConn.unsubscribe(subscription.topic, subscription.ref, true)
|
||||
}
|
||||
}
|
||||
|
||||
// clean up the unsubscribe candidate list
|
||||
delete unsubscribeCandidates[ref]
|
||||
|
||||
// determine if this is an existing subscription
|
||||
const existingSubscription = typeof subscription === "object" && subscription !== null
|
||||
|
||||
// if existing subscription is not found or has changed, create a new subscription object
|
||||
if (existingSubscription === false || changesFound) {
|
||||
subscription = createSubscriptionObject(topic, options, ref, node.id)
|
||||
}
|
||||
|
||||
// setup remainder of subscription properties and event handling
|
||||
node.subscriptions[topic] = node.subscriptions[topic] || {};
|
||||
node.subscriptions[topic][ref] = subscription
|
||||
if (!node.subscriptionIds[topic]) {
|
||||
node.subscriptionIds[topic] = node.subid++;
|
||||
}
|
||||
options.properties = options.properties || {};
|
||||
options.properties.subscriptionIdentifier = node.subscriptionIds[topic];
|
||||
subscription.options = subscription.options || {};
|
||||
subscription.options.properties = options.properties || {};
|
||||
subscription.options.properties.subscriptionIdentifier = node.subscriptionIds[topic];
|
||||
subscription.callback = callback;
|
||||
|
||||
node.subscriptions[topic] = node.subscriptions[topic]||{};
|
||||
var sub = {
|
||||
topic:topic,
|
||||
qos:qos,
|
||||
options:options,
|
||||
handler:function(mtopic,mpayload, mpacket) {
|
||||
if(mpacket.properties && options.properties && mpacket.properties.subscriptionIdentifier && options.properties.subscriptionIdentifier && (mpacket.properties.subscriptionIdentifier !== options.properties.subscriptionIdentifier) ) {
|
||||
//do nothing as subscriptionIdentifier does not match
|
||||
} else if (matchTopic(topic,mtopic)) {
|
||||
callback(mtopic,mpayload, mpacket);
|
||||
}
|
||||
},
|
||||
ref: ref
|
||||
};
|
||||
node.subscriptions[topic][ref] = sub;
|
||||
// if the client is connected, then setup the handler and subscribe
|
||||
if (node.connected) {
|
||||
node._clientOn('message',sub.handler);
|
||||
node.client.subscribe(topic, options);
|
||||
}
|
||||
};
|
||||
const subIdsAvailable = node.subscriptionIdentifiersAvailable()
|
||||
|
||||
node.unsubscribe = function (topic, ref, removed) {
|
||||
ref = ref||0;
|
||||
var sub = node.subscriptions[topic];
|
||||
if (sub) {
|
||||
if (sub[ref]) {
|
||||
if(node.client) {
|
||||
node._clientRemoveListeners('message',sub[ref].handler);
|
||||
}
|
||||
delete sub[ref];
|
||||
}
|
||||
//TODO: Review. The `if(removed)` was commented out to always delete and remove subscriptions.
|
||||
// if we dont then property changes dont get applied and old subs still trigger
|
||||
//if (removed) {
|
||||
if (Object.keys(sub).length === 0) {
|
||||
delete node.subscriptions[topic];
|
||||
delete node.subscriptionIds[topic];
|
||||
if (node.connected) {
|
||||
node.client.unsubscribe(topic);
|
||||
if (!subscription.handler) {
|
||||
subscription.handler = function (mtopic, mpayload, mpacket) {
|
||||
const sops = subscription.options ? subscription.options.properties : {}
|
||||
const pops = mpacket.properties || {}
|
||||
if (subIdsAvailable && pops.subscriptionIdentifier && sops.subscriptionIdentifier && (pops.subscriptionIdentifier !== sops.subscriptionIdentifier)) {
|
||||
//do nothing as subscriptionIdentifier does not match
|
||||
} else if (matchTopic(topic, mtopic)) {
|
||||
subscription.callback && subscription.callback(mtopic, mpayload, mpacket)
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
node._clientOn('message', subscription.handler)
|
||||
// if the broker doesn't support subscription identifiers, then don't send them (AWS support)
|
||||
if (subscription.options.properties && subscription.options.properties.subscriptionIdentifier && subIdsAvailable !== true) {
|
||||
delete subscription.options.properties.subscriptionIdentifier
|
||||
}
|
||||
node.client.subscribe(topic, subscription.options)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
node.unsubscribe = function (topic, ref, removeClientSubscription) {
|
||||
ref = ref||0;
|
||||
const unsub = removeClientSubscription || node.autoUnsubscribe !== false
|
||||
const sub = node.subscriptions[topic];
|
||||
let brokerId = node.id
|
||||
if (sub) {
|
||||
if (sub[ref]) {
|
||||
brokerId = sub[ref].brokerId || brokerId
|
||||
if(node.client && sub[ref].handler) {
|
||||
node._clientRemoveListeners('message', sub[ref].handler);
|
||||
sub[ref].handler = null
|
||||
}
|
||||
if (unsub) {
|
||||
delete sub[ref]
|
||||
}
|
||||
}
|
||||
// if instructed to remove the actual MQTT client subscription
|
||||
if (unsub) {
|
||||
// if there are no more subscriptions for the topic, then remove the topic
|
||||
if (Object.keys(sub).length === 0) {
|
||||
try {
|
||||
node.client.unsubscribe(topic)
|
||||
} catch (_err) {
|
||||
// do nothing
|
||||
} finally {
|
||||
// remove unsubscribe candidate as it is now REALLY unsubscribed
|
||||
delete node.subscriptions[topic];
|
||||
delete node.subscriptionIds[topic];
|
||||
if (unsubscribeCandidates[ref]) {
|
||||
unsubscribeCandidates[ref] = unsubscribeCandidates[ref].filter(sub => sub.topic !== topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if instructed to not remove the client subscription, then add it to the candidate list
|
||||
// of subscriptions to be removed when the the same ref is used in a subsequent subscribe
|
||||
// and the topic has changed
|
||||
unsubscribeCandidates[ref] = unsubscribeCandidates[ref] || [];
|
||||
unsubscribeCandidates[ref].push({
|
||||
topic: topic,
|
||||
ref: ref,
|
||||
brokerId: brokerId
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
node.topicAliases = {};
|
||||
@@ -959,7 +1149,7 @@ module.exports = function(RED) {
|
||||
setStrProp(msg, options.properties, "contentType");
|
||||
setIntProp(msg, options.properties, "messageExpiryInterval", 0);
|
||||
setUserProperties(msg.userProperties, options.properties);
|
||||
setIntProp(msg, options.properties, "topicAlias", 1, node.serverProperties.topicAliasMaximum || 0);
|
||||
setIntProp(msg, options.properties, "topicAlias", 1, bsp.topicAliasMaximum || 0);
|
||||
setBoolProp(msg, options.properties, "payloadFormatIndicator");
|
||||
//FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
|
||||
|
||||
@@ -983,14 +1173,21 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
if (topicOK) {
|
||||
node.client.publish(msg.topic, msg.payload, options, function(err) {
|
||||
done && done(err);
|
||||
return
|
||||
});
|
||||
node.client.publish(msg.topic, msg.payload, options, function (err) {
|
||||
if (done) {
|
||||
done(err)
|
||||
} else if(err) {
|
||||
node.error(err, msg)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const error = new Error(RED._("mqtt.errors.invalid-topic"));
|
||||
error.warn = true;
|
||||
done(error);
|
||||
const error = new Error(RED._("mqtt.errors.invalid-topic"))
|
||||
error.warn = true
|
||||
if (done) {
|
||||
done(error)
|
||||
} else {
|
||||
node.warn(error, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1003,7 +1200,7 @@ module.exports = function(RED) {
|
||||
|
||||
/**
|
||||
* Add event handlers to the MQTT.js client and track them so that
|
||||
* we do not remove any handlers that the MQTT client uses internally.
|
||||
* we do not remove any handlers that the MQTT client uses internally.
|
||||
* Use {@link node._clientRemoveListeners `node._clientRemoveListeners`} to remove handlers
|
||||
* @param {string} event The name of the event
|
||||
* @param {function} handler The handler for this event
|
||||
@@ -1011,11 +1208,11 @@ module.exports = function(RED) {
|
||||
node._clientOn = function(event, handler) {
|
||||
node.clientListeners.push({event, handler})
|
||||
node.client.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event handlers from the MQTT.js client & only the events
|
||||
* that we attached in {@link node._clientOn `node._clientOn`}.
|
||||
* Remove event handlers from the MQTT.js client & only the events
|
||||
* that we attached in {@link node._clientOn `node._clientOn`}.
|
||||
* * If `event` is omitted, then all events matching `handler` are removed
|
||||
* * If `handler` is omitted, then all events named `event` are removed
|
||||
* * If both parameters are omitted, then all events are removed
|
||||
@@ -1088,7 +1285,7 @@ module.exports = function(RED) {
|
||||
if(node.rap === "true" || node.rap === true) options.rap = true;
|
||||
else if(node.rap === "false" || node.rap === false) options.rap = false;
|
||||
}
|
||||
|
||||
node._topic = node.topic; // store the original topic incase node is later changed
|
||||
node.brokerConn.subscribe(node.topic,options,function(topic, payload, packet) {
|
||||
subscriptionHandler(node, node.datatype, topic, payload, packet);
|
||||
},node.id);
|
||||
@@ -1141,7 +1338,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
if (action === Actions.UNSUBSCRIBE) {
|
||||
subscriptions.forEach(function (sub) {
|
||||
node.brokerConn.unsubscribe(sub.topic, node.id);
|
||||
node.brokerConn.unsubscribe(sub.topic, node.id, true);
|
||||
delete node.dynamicSubs[sub.topic];
|
||||
})
|
||||
//user can access current subscriptions through the complete node is so desired
|
||||
@@ -1151,7 +1348,7 @@ module.exports = function(RED) {
|
||||
subscriptions.forEach(function (sub) {
|
||||
//always unsubscribe before subscribe to prevent multiple subs to same topic
|
||||
if (node.dynamicSubs[sub.topic]) {
|
||||
node.brokerConn.unsubscribe(sub.topic, node.id);
|
||||
node.brokerConn.unsubscribe(sub.topic, node.id, true);
|
||||
delete node.dynamicSubs[sub.topic];
|
||||
}
|
||||
|
||||
@@ -1202,9 +1399,9 @@ module.exports = function(RED) {
|
||||
});
|
||||
node.dynamicSubs = {};
|
||||
} else {
|
||||
node.brokerConn.unsubscribe(node.topic,node.id, removed);
|
||||
node.brokerConn.unsubscribe(node.topic, node.id, removed);
|
||||
}
|
||||
node.brokerConn.deregister(node, done);
|
||||
node.brokerConn.deregister(node, done, removed);
|
||||
node.brokerConn = null;
|
||||
} else {
|
||||
done();
|
||||
@@ -1267,9 +1464,9 @@ module.exports = function(RED) {
|
||||
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
|
||||
}
|
||||
node.brokerConn.register(node);
|
||||
node.on('close', function(done) {
|
||||
node.on('close', function(removed, done) {
|
||||
if (node.brokerConn) {
|
||||
node.brokerConn.deregister(node,done);
|
||||
node.brokerConn.deregister(node, done, removed)
|
||||
node.brokerConn = null;
|
||||
} else {
|
||||
done();
|
||||
|
@@ -227,6 +227,7 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
sortable: true,
|
||||
removable: true
|
||||
});
|
||||
|
||||
|
@@ -46,7 +46,7 @@ module.exports = function(RED) {
|
||||
isText = true;
|
||||
} else if (parsedType.type !== "application") {
|
||||
isText = false;
|
||||
} else if ((parsedType.subtype !== "octet-stream")
|
||||
} else if ((parsedType.subtype !== "octet-stream")
|
||||
&& (parsedType.subtype !== "cbor")
|
||||
&& (parsedType.subtype !== "x-protobuf")) {
|
||||
checkUTF = true;
|
||||
@@ -200,6 +200,15 @@ module.exports = function(RED) {
|
||||
this.callback = function(req,res) {
|
||||
var msgid = RED.util.generateId();
|
||||
res._msgid = msgid;
|
||||
// Since Node 15, req.headers are lazily computed and the property
|
||||
// marked as non-enumerable.
|
||||
// That means it doesn't show up in the Debug sidebar.
|
||||
// This redefines the property causing it to be evaluated *and*
|
||||
// marked as enumerable again.
|
||||
Object.defineProperty(req, 'headers', {
|
||||
value: req.headers,
|
||||
enumerable: true
|
||||
})
|
||||
if (node.method.match(/^(post|delete|put|options|patch)$/)) {
|
||||
node.send({_msgid:msgid,req:req,res:createResponseWrapper(node,res),payload:req.body});
|
||||
} else if (node.method == "get") {
|
||||
@@ -282,7 +291,7 @@ module.exports = function(RED) {
|
||||
RED.nodes.createNode(this,n);
|
||||
var node = this;
|
||||
this.headers = n.headers||{};
|
||||
this.statusCode = n.statusCode;
|
||||
this.statusCode = parseInt(n.statusCode);
|
||||
this.on("input",function(msg,_send,done) {
|
||||
if (msg.res) {
|
||||
var headers = RED.util.cloneMessage(node.headers);
|
||||
@@ -323,7 +332,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
}
|
||||
var statusCode = node.statusCode || msg.statusCode || 200;
|
||||
var statusCode = node.statusCode || parseInt(msg.statusCode) || 200;
|
||||
if (typeof msg.payload == "object" && !Buffer.isBuffer(msg.payload)) {
|
||||
msg.res._res.status(statusCode).jsonp(msg.payload);
|
||||
} else {
|
||||
|
@@ -91,6 +91,11 @@
|
||||
<label for="node-input-senderr" style="width: auto" data-i18n="httpin.senderr"></label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<input type="checkbox" id="node-input-insecureHTTPParser" style="display: inline-block; width: auto; vertical-align: top;">
|
||||
<label for="node-input-insecureHTTPParser" style="width: auto;" data-i18n="httpin.insecureHTTPParser"></label>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-ret"><i class="fa fa-arrow-left"></i> <span data-i18n="httpin.label.return"></span></label>
|
||||
@@ -227,6 +232,7 @@
|
||||
persist: {value:false},
|
||||
proxy: {type:"http proxy",required: false,
|
||||
label:RED._("node-red:httpin.proxy-config") },
|
||||
insecureHTTPParser: {value: false},
|
||||
authType: {value: ""},
|
||||
senderr: {value: false},
|
||||
headers: { value: [] }
|
||||
@@ -338,6 +344,12 @@
|
||||
} else {
|
||||
$("#node-input-useProxy").prop("checked", false);
|
||||
}
|
||||
|
||||
if (node.insecureHTTPParser) {
|
||||
$("node-intput-insecureHTTPParser").prop("checked", true)
|
||||
} else {
|
||||
$("node-intput-insecureHTTPParser").prop("checked", false)
|
||||
}
|
||||
updateProxyOptions();
|
||||
$("#node-input-useProxy").on("click", function() {
|
||||
updateProxyOptions();
|
||||
@@ -405,6 +417,7 @@
|
||||
});
|
||||
|
||||
},
|
||||
sortable: true,
|
||||
removable: true
|
||||
});
|
||||
if (node.headers) {
|
||||
|
@@ -14,16 +14,22 @@
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
module.exports = function(RED) {
|
||||
module.exports = async function(RED) {
|
||||
"use strict";
|
||||
<<<<<<< HEAD
|
||||
const { getProxyForUrl, parseUrl } = require('./lib/proxyHelper');
|
||||
const got = require("got");
|
||||
=======
|
||||
const { got } = await import('got')
|
||||
>>>>>>> upstream/dev
|
||||
const {CookieJar} = require("tough-cookie");
|
||||
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
|
||||
const FormData = require('form-data');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const crypto = require('crypto');
|
||||
const URL = require("url").URL
|
||||
const http = require("http")
|
||||
const https = require("https")
|
||||
var mustache = require("mustache");
|
||||
var querystring = require("querystring");
|
||||
var cookie = require("cookie");
|
||||
@@ -66,16 +72,27 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
function HTTPRequest(n) {
|
||||
RED.nodes.createNode(this,n);
|
||||
checkNodeAgentPatch();
|
||||
var node = this;
|
||||
var nodeUrl = n.url;
|
||||
var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
|
||||
var nodeMethod = n.method || "GET";
|
||||
var paytoqs = false;
|
||||
var paytobody = false;
|
||||
var redirectList = [];
|
||||
var sendErrorsToCatch = n.senderr;
|
||||
const node = this;
|
||||
const nodeUrl = n.url;
|
||||
const isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
|
||||
const nodeMethod = n.method || "GET";
|
||||
let paytoqs = false;
|
||||
let paytobody = false;
|
||||
let redirectList = [];
|
||||
const sendErrorsToCatch = n.senderr;
|
||||
node.headers = n.headers || [];
|
||||
var nodeHTTPPersistent = n["persist"];
|
||||
const useKeepAlive = n["persist"];
|
||||
let agents = null
|
||||
if (useKeepAlive) {
|
||||
agents = {
|
||||
http: new http.Agent({ keepAlive: true }),
|
||||
https: new https.Agent({ keepAlive: true })
|
||||
}
|
||||
node.on('close', function () {
|
||||
agents.http.destroy()
|
||||
agents.https.destroy()
|
||||
})
|
||||
}
|
||||
if (n.tls) {
|
||||
var tlsNode = RED.nodes.getNode(n.tls);
|
||||
}
|
||||
@@ -87,6 +104,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
if (n.paytoqs === true || n.paytoqs === "query") { paytoqs = true; }
|
||||
else if (n.paytoqs === "body") { paytobody = true; }
|
||||
|
||||
<<<<<<< HEAD
|
||||
let proxyConfig = n.proxy ? RED.nodes.getNode(n.proxy) || {} : null
|
||||
const getProxy = (url) => {
|
||||
const proxyOptions = Object.assign({}, RED.settings.proxyOptions);
|
||||
@@ -97,6 +115,21 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
}
|
||||
}
|
||||
return getProxyForUrl(url, proxyOptions)
|
||||
=======
|
||||
node.insecureHTTPParser = n.insecureHTTPParser
|
||||
|
||||
var prox, noprox;
|
||||
if (process.env.http_proxy) { prox = process.env.http_proxy; }
|
||||
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
|
||||
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
|
||||
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
|
||||
|
||||
var proxyConfig = null;
|
||||
if (n.proxy) {
|
||||
proxyConfig = RED.nodes.getNode(n.proxy);
|
||||
prox = proxyConfig.url;
|
||||
noprox = proxyConfig.noproxy;
|
||||
>>>>>>> upstream/dev
|
||||
}
|
||||
let prox = getProxy(nodeUrl || '')
|
||||
|
||||
@@ -127,15 +160,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Object} headersObject
|
||||
* @param {string} name
|
||||
* @return {any} value
|
||||
*/
|
||||
const getHeaderValue = (headersObject, name) => {
|
||||
const asLowercase = name.toLowercase();
|
||||
return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
|
||||
}
|
||||
|
||||
this.on("input",function(msg,nodeSend,nodeDone) {
|
||||
checkNodeAgentPatch();
|
||||
//reset redirectList on each request
|
||||
@@ -213,24 +238,24 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
// set defaultport, else when using HttpsProxyAgent, it's defaultPort of 443 will be used :(.
|
||||
// Had to remove this to get http->https redirect to work
|
||||
// opts.defaultPort = isHttps?443:80;
|
||||
opts.timeout = node.reqTimeout;
|
||||
opts.timeout = { request: node.reqTimeout || 5000 };
|
||||
opts.throwHttpErrors = false;
|
||||
// TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
|
||||
opts.decompress = false;
|
||||
opts.method = method;
|
||||
opts.retry = 0;
|
||||
opts.retry = { limit: 0 };
|
||||
opts.responseType = 'buffer';
|
||||
opts.maxRedirects = 21;
|
||||
opts.cookieJar = new CookieJar();
|
||||
opts.ignoreInvalidCookies = true;
|
||||
opts.forever = nodeHTTPPersistent;
|
||||
// opts.forever = nodeHTTPPersistent;
|
||||
if (msg.requestTimeout !== undefined) {
|
||||
if (isNaN(msg.requestTimeout)) {
|
||||
node.warn(RED._("httpin.errors.timeout-isnan"));
|
||||
} else if (msg.requestTimeout < 1) {
|
||||
node.warn(RED._("httpin.errors.timeout-isnegative"));
|
||||
} else {
|
||||
opts.timeout = msg.requestTimeout;
|
||||
opts.timeout = { request: msg.requestTimeout };
|
||||
}
|
||||
}
|
||||
const originalHeaderMap = {};
|
||||
@@ -248,6 +273,13 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
delete options.headers[h];
|
||||
}
|
||||
})
|
||||
if (node.insecureHTTPParser) {
|
||||
// Setting the property under _unixOptions as pretty
|
||||
// much the only hack available to get got to apply
|
||||
// a core http option it doesn't think we should be
|
||||
// allowed to set
|
||||
options._unixOptions = { ...options.unixOptions, insecureHTTPParser: true }
|
||||
}
|
||||
}
|
||||
],
|
||||
beforeRedirect: [
|
||||
@@ -283,7 +315,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
}
|
||||
|
||||
opts.headers = {};
|
||||
//add msg.headers
|
||||
//add msg.headers
|
||||
//NOTE: ui headers will take precidence over msg.headers
|
||||
if (msg.headers) {
|
||||
if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
|
||||
@@ -402,15 +434,16 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
return response
|
||||
}
|
||||
const requestUrl = new URL(response.request.requestUrl);
|
||||
const options = response.request.options;
|
||||
const options = { headers: {} }
|
||||
const normalisedHeaders = {};
|
||||
Object.keys(response.headers).forEach(k => {
|
||||
normalisedHeaders[k.toLowerCase()] = response.headers[k]
|
||||
})
|
||||
if (normalisedHeaders['www-authenticate']) {
|
||||
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, options.method, requestUrl.pathname, normalisedHeaders['www-authenticate'])
|
||||
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname, normalisedHeaders['www-authenticate'])
|
||||
options.headers.Authorization = authHeader;
|
||||
}
|
||||
// response.request.options.merge(options)
|
||||
sentCreds = true;
|
||||
return retry(options);
|
||||
}
|
||||
@@ -543,13 +576,21 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
//need both incase of http -> https redirect
|
||||
opts.agent = {
|
||||
http: new HttpProxyAgent(proxyOptions),
|
||||
<<<<<<< HEAD
|
||||
https: new HttpProxyAgent(proxyOptions)
|
||||
};
|
||||
|
||||
=======
|
||||
https: new HttpsProxyAgent(proxyOptions)
|
||||
}
|
||||
>>>>>>> upstream/dev
|
||||
} else {
|
||||
node.warn("Bad proxy url: " + proxyUrl);
|
||||
}
|
||||
}
|
||||
if (useKeepAlive && !opts.agent) {
|
||||
opts.agent = agents
|
||||
}
|
||||
if (tlsNode) {
|
||||
opts.https = {};
|
||||
tlsNode.addTLSOptions(opts.https);
|
||||
@@ -606,6 +647,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
msg.payload = msg.payload.toString('utf8'); // txt
|
||||
|
||||
if (node.ret === "obj") {
|
||||
if (msg.statusCode == 204){msg.payload= "{}"};
|
||||
try { msg.payload = JSON.parse(msg.payload); } // obj
|
||||
catch(e) { node.warn(RED._("httpin.errors.json-error")); }
|
||||
}
|
||||
@@ -687,25 +729,43 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
});
|
||||
|
||||
const md5 = (value) => { return crypto.createHash('md5').update(value).digest('hex') }
|
||||
const sha256 = (value) => { return crypto.createHash('sha256').update(value).digest('hex') }
|
||||
const sha512 = (value) => { return crypto.createHash('sha512').update(value).digest('hex') }
|
||||
|
||||
function digestCompute(algorithm, value) {
|
||||
var lowercaseAlgorithm = ""
|
||||
if (algorithm) {
|
||||
lowercaseAlgorithm = algorithm.toLowerCase().replace(/-sess$/, '')
|
||||
}
|
||||
|
||||
if (lowercaseAlgorithm === "sha-256") {
|
||||
return sha256(value)
|
||||
} else if (lowercaseAlgorithm === "sha-512-256") {
|
||||
var hash = sha512(value)
|
||||
return hash.slice(0, 64) // Only use the first 256 bits
|
||||
} else {
|
||||
return md5(value)
|
||||
}
|
||||
}
|
||||
|
||||
function ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
|
||||
/**
|
||||
* RFC 2617: handle both MD5 and MD5-sess algorithms.
|
||||
* RFC 2617: handle both standard and -sess algorithms.
|
||||
*
|
||||
* If the algorithm directive's value is "MD5" or unspecified, then HA1 is
|
||||
* HA1=MD5(username:realm:password)
|
||||
* If the algorithm directive's value is "MD5-sess", then HA1 is
|
||||
* HA1=MD5(MD5(username:realm:password):nonce:cnonce)
|
||||
* If the algorithm directive's value ends with "-sess", then HA1 is
|
||||
* HA1=digestCompute(digestCompute(username:realm:password):nonce:cnonce)
|
||||
*
|
||||
* If the algorithm directive's value does not end with "-sess", then HA1 is
|
||||
* HA1=digestCompute(username:realm:password)
|
||||
*/
|
||||
var ha1 = md5(user + ':' + realm + ':' + pass)
|
||||
if (algorithm && algorithm.toLowerCase() === 'md5-sess') {
|
||||
return md5(ha1 + ':' + nonce + ':' + cnonce)
|
||||
var ha1 = digestCompute(algorithm, user + ':' + realm + ':' + pass)
|
||||
if (algorithm && /-sess$/i.test(algorithm)) {
|
||||
return digestCompute(algorithm, ha1 + ':' + nonce + ':' + cnonce)
|
||||
} else {
|
||||
return ha1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function buildDigestHeader(user, pass, method, path, authHeader) {
|
||||
var challenge = {}
|
||||
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
|
||||
@@ -720,10 +780,10 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
var nc = qop && '00000001'
|
||||
var cnonce = qop && uuid().replace(/-/g, '')
|
||||
var ha1 = ha1Compute(challenge.algorithm, user, challenge.realm, pass, challenge.nonce, cnonce)
|
||||
var ha2 = md5(method + ':' + path)
|
||||
var ha2 = digestCompute(challenge.algorithm, method + ':' + path)
|
||||
var digestResponse = qop
|
||||
? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2)
|
||||
: md5(ha1 + ':' + challenge.nonce + ':' + ha2)
|
||||
? digestCompute(challenge.algorithm, ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2)
|
||||
: digestCompute(challenge.algorithm, ha1 + ':' + challenge.nonce + ':' + ha2)
|
||||
var authValues = {
|
||||
username: user,
|
||||
realm: challenge.realm,
|
||||
|
@@ -40,6 +40,99 @@
|
||||
|
||||
(function() {
|
||||
|
||||
const headerTypes = [
|
||||
/*
|
||||
{ value: "Accept", label: "Accept", hasValue: false },
|
||||
{ value: "Accept-Encoding", label: "Accept-Encoding", hasValue: false },
|
||||
{ value: "Accept-Language", label: "Accept-Language", hasValue: false },
|
||||
*/
|
||||
{ value: "Authorization", label: "Authorization", hasValue: false },
|
||||
/*
|
||||
{ value: "Content-Type", label: "Content-Type", hasValue: false },
|
||||
{ value: "Cache-Control", label: "Cache-Control", hasValue: false },
|
||||
*/
|
||||
{ value: "User-Agent", label: "User-Agent", hasValue: false },
|
||||
/*
|
||||
{ value: "Location", label: "Location", hasValue: false },
|
||||
*/
|
||||
{ value: "other", label: RED._("node-red:httpin.label.other"),
|
||||
hasValue: true, icon: "red/images/typedInput/az.svg" },
|
||||
]
|
||||
|
||||
const headerOptions = {};
|
||||
const defaultOptions = [
|
||||
{ value: "other", label: RED._("node-red:httpin.label.other"),
|
||||
hasValue: true, icon: "red/images/typedInput/az.svg" },
|
||||
"env",
|
||||
];
|
||||
/*
|
||||
headerOptions["accept"] = [
|
||||
{ value: "text/plain", label: "text/plain", hasValue: false },
|
||||
{ value: "text/html", label: "text/html", hasValue: false },
|
||||
{ value: "application/json", label: "application/json", hasValue: false },
|
||||
{ value: "application/xml", label: "application/xml", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
|
||||
headerOptions["accept-encoding"] = [
|
||||
{ value: "gzip", label: "gzip", hasValue: false },
|
||||
{ value: "deflate", label: "deflate", hasValue: false },
|
||||
{ value: "compress", label: "compress", hasValue: false },
|
||||
{ value: "br", label: "br", hasValue: false },
|
||||
{ value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
|
||||
{ value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
headerOptions["accept-language"] = [
|
||||
{ value: "*", label: "*", hasValue: false },
|
||||
{ value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
|
||||
{ value: "de-AT, de-DE;q=0.9, en;q=0.5", label: "de-AT, de-DE;q=0.9, en;q=0.5", hasValue: false },
|
||||
{ value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
|
||||
{ value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
|
||||
{ value: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", label: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", hasValue: false },
|
||||
{ value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
headerOptions["content-type"] = [
|
||||
{ value: "text/css", label: "text/css", hasValue: false },
|
||||
{ value: "text/plain", label: "text/plain", hasValue: false },
|
||||
{ value: "text/html", label: "text/html", hasValue: false },
|
||||
{ value: "application/json", label: "application/json", hasValue: false },
|
||||
{ value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
|
||||
{ value: "application/pdf", label: "application/pdf", hasValue: false },
|
||||
{ value: "application/xml", label: "application/xml", hasValue: false },
|
||||
{ value: "application/zip", label: "application/zip", hasValue: false },
|
||||
{ value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
|
||||
{ value: "audio/aac", label: "audio/aac", hasValue: false },
|
||||
{ value: "audio/ac3", label: "audio/ac3", hasValue: false },
|
||||
{ value: "audio/basic", label: "audio/basic", hasValue: false },
|
||||
{ value: "audio/mp4", label: "audio/mp4", hasValue: false },
|
||||
{ value: "audio/ogg", label: "audio/ogg", hasValue: false },
|
||||
{ value: "image/bmp", label: "image/bmp", hasValue: false },
|
||||
{ value: "image/gif", label: "image/gif", hasValue: false },
|
||||
{ value: "image/jpeg", label: "image/jpeg", hasValue: false },
|
||||
{ value: "image/png", label: "image/png", hasValue: false },
|
||||
{ value: "image/tiff", label: "image/tiff", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
headerOptions["cache-control"] = [
|
||||
{ value: "max-age=0", label: "max-age=0", hasValue: false },
|
||||
{ value: "max-age=86400", label: "max-age=86400", hasValue: false },
|
||||
{ value: "no-cache", label: "no-cache", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
*/
|
||||
headerOptions["user-agent"] = [
|
||||
{ value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
|
||||
...defaultOptions,
|
||||
];
|
||||
|
||||
function getHeaderOptions(headerName) {
|
||||
const lc = (headerName || "").toLowerCase();
|
||||
let opts = headerOptions[lc];
|
||||
return opts || defaultOptions;
|
||||
}
|
||||
|
||||
function ws_oneditprepare() {
|
||||
$("#websocket-client-row").hide();
|
||||
$("#node-input-mode").on("change", function() {
|
||||
@@ -192,7 +285,8 @@
|
||||
value: "",
|
||||
label:RED._("node-red:websocket.sendheartbeat"),
|
||||
validate: RED.validators.number(/*blank allowed*/true) },
|
||||
subprotocol: {value:"",required: false}
|
||||
subprotocol: {value:"",required: false},
|
||||
headers: { value: [] }
|
||||
},
|
||||
inputs:0,
|
||||
outputs:0,
|
||||
@@ -200,6 +294,9 @@
|
||||
return this.path;
|
||||
},
|
||||
oneditprepare: function() {
|
||||
|
||||
const node = this;
|
||||
|
||||
$("#node-config-input-path").on("change keyup paste",function() {
|
||||
$(".node-config-row-tls").toggle(/^wss:/i.test($(this).val()))
|
||||
});
|
||||
@@ -214,14 +311,114 @@
|
||||
if (!heartbeatActive) {
|
||||
$("#node-config-input-hb").val("");
|
||||
}
|
||||
|
||||
const hasMatch = function (arr, value) {
|
||||
return arr.some(function (ht) {
|
||||
return ht.value === value
|
||||
});
|
||||
}
|
||||
|
||||
const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
|
||||
addItem: function (container, i, header) {
|
||||
const row = $('<div/>').css({
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex'
|
||||
}).appendTo(container);
|
||||
const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
|
||||
const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
|
||||
.appendTo(propertNameCell)
|
||||
.typedInput({ types: headerTypes });
|
||||
|
||||
const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
|
||||
const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
|
||||
.appendTo(propertyValueCell)
|
||||
.typedInput({
|
||||
types: getHeaderOptions(header.keyType)
|
||||
});
|
||||
|
||||
const setup = function(_header) {
|
||||
const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
|
||||
const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
|
||||
|
||||
const {keyType, keyValue, valueType, valueValue} = header;
|
||||
|
||||
if(keyType == "other") {
|
||||
propertyName.typedInput('type', keyType);
|
||||
propertyName.typedInput('value', keyValue);
|
||||
} else if (headerTypeIsAPreset(keyType)) {
|
||||
propertyName.typedInput('type', keyType);
|
||||
} else {
|
||||
propertyName.typedInput('type', "other");
|
||||
propertyName.typedInput('value', keyValue);
|
||||
}
|
||||
|
||||
if(valueType == "other" || valueType == "env" ) {
|
||||
propertyValue.typedInput('type', valueType);
|
||||
propertyValue.typedInput('value', valueValue);
|
||||
} else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
|
||||
propertyValue.typedInput('type', valueType);
|
||||
} else {
|
||||
propertyValue.typedInput('type', "other");
|
||||
propertyValue.typedInput('value', valueValue);
|
||||
}
|
||||
}
|
||||
setup(header);
|
||||
|
||||
propertyName.on('change', function (event) {
|
||||
propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
|
||||
});
|
||||
|
||||
},
|
||||
sortable: true,
|
||||
removable: true
|
||||
});
|
||||
if (node.headers) {
|
||||
for (let index = 0; index < node.headers.length; index++) {
|
||||
const element = node.headers[index];
|
||||
headerList.editableList('addItem', node.headers[index]);
|
||||
}
|
||||
}
|
||||
},
|
||||
oneditsave: function() {
|
||||
|
||||
const node = this;
|
||||
|
||||
if (!/^wss:/i.test($("#node-config-input-path").val())) {
|
||||
$("#node-config-input-tls").val("_ADD_");
|
||||
}
|
||||
if (!$("#node-config-input-hb-cb").prop("checked")) {
|
||||
$("#node-config-input-hb").val("0");
|
||||
}
|
||||
|
||||
const headers = $("#node-input-headers-container").editableList('items');
|
||||
|
||||
node.headers = [];
|
||||
headers.each(function(i) {
|
||||
const header = $(this);
|
||||
const keyType = header.find(".node-input-header-name").typedInput('type');
|
||||
const keyValue = header.find(".node-input-header-name").typedInput('value');
|
||||
const valueType = header.find(".node-input-header-value").typedInput('type');
|
||||
const valueValue = header.find(".node-input-header-value").typedInput('value');
|
||||
node.headers.push({
|
||||
keyType, keyValue, valueType, valueValue
|
||||
})
|
||||
|
||||
});
|
||||
},
|
||||
oneditresize: function(size) {
|
||||
const dlg = $("#dialog-form");
|
||||
const expandRow = dlg.find('.node-input-headers-container-row');
|
||||
let height = dlg.height() - 5;
|
||||
if(expandRow && expandRow.length){
|
||||
const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
|
||||
for (let i = 0; i < siblingRows.size(); i++) {
|
||||
const cr = $(siblingRows[i]);
|
||||
if(cr.is(":visible"))
|
||||
height -= cr.outerHeight(true);
|
||||
}
|
||||
$("#node-input-headers-container").editableList('height',height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -299,8 +496,15 @@
|
||||
<span data-i18n="inject.seconds"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<label><i class="fa fa-list"></i> <span data-i18n="httpin.label.headers"></span></label>
|
||||
</div>
|
||||
<div class="form-row node-input-headers-container-row">
|
||||
<ol id="node-input-headers-container"></ol>
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
<p><span data-i18n="[html]websocket.tip.url1"></span></p>
|
||||
<span data-i18n="[html]websocket.tip.url2"></span>
|
||||
<p><span data-i18n="[html]websocket.tip.url2"></span></p>
|
||||
<span data-i18n="[html]websocket.tip.headers"></span>
|
||||
</div>
|
||||
</script>
|
||||
|
@@ -58,6 +58,7 @@ module.exports = function(RED) {
|
||||
node.isServer = !/^ws{1,2}:\/\//i.test(node.path);
|
||||
node.closing = false;
|
||||
node.tls = n.tls;
|
||||
node.upgradeHeaders = n.headers
|
||||
|
||||
if (n.hb) {
|
||||
var heartbeat = parseInt(n.hb);
|
||||
@@ -84,6 +85,42 @@ module.exports = function(RED) {
|
||||
tlsNode.addTLSOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to check if undefined, to guard against previous installs, that will not have had this property set (applies to 3.1.x setups)
|
||||
// Else this will be breaking potentially
|
||||
if(node.upgradeHeaders !== undefined && node.upgradeHeaders.length > 0){
|
||||
options.headers = {};
|
||||
for(let i = 0;i<node.upgradeHeaders.length;i++){
|
||||
const header = node.upgradeHeaders[i];
|
||||
const keyType = header.keyType;
|
||||
const keyValue = header.keyValue;
|
||||
const valueType = header.valueType;
|
||||
const valueValue = header.valueValue;
|
||||
|
||||
const headerName = keyType === 'other' ? keyValue : keyType;
|
||||
let headerValue;
|
||||
|
||||
switch(valueType){
|
||||
case 'other':
|
||||
headerValue = valueValue;
|
||||
break;
|
||||
|
||||
case 'env':
|
||||
headerValue = RED.util.evaluateNodeProperty(valueValue,valueType,node);
|
||||
break;
|
||||
|
||||
default:
|
||||
headerValue = valueType;
|
||||
break;
|
||||
}
|
||||
|
||||
if(headerName && headerValue){
|
||||
options.headers[headerName] = headerValue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var socket = new ws(node.path,node.subprotocol,options);
|
||||
socket.setMaxListeners(0);
|
||||
node.server = socket; // keep for closing
|
||||
|
@@ -86,7 +86,7 @@ module.exports = function(RED) {
|
||||
this.topic = n.topic;
|
||||
this.stream = (!n.datamode||n.datamode=='stream'); /* stream,single*/
|
||||
this.datatype = n.datatype||'buffer'; /* buffer,utf8,base64 */
|
||||
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
|
||||
this.newline = (n.newline||"").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t");
|
||||
this.base64 = n.base64;
|
||||
this.trim = n.trim || false;
|
||||
this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server");
|
||||
@@ -411,23 +411,33 @@ module.exports = function(RED) {
|
||||
if (msg._session && msg._session.type == "tcp") {
|
||||
var client = connectionPool[msg._session.id];
|
||||
if (client) {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
client.write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
client.write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
client.write(Buffer.from(""+msg.payload));
|
||||
if (msg?.reset === true) {
|
||||
client.destroy();
|
||||
}
|
||||
else {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
client.write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
client.write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
client.write(Buffer.from(""+msg.payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (var i in connectionPool) {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
connectionPool[i].write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
connectionPool[i].write(Buffer.from(""+msg.payload));
|
||||
if (msg?.reset === true) {
|
||||
connectionPool[i].destroy();
|
||||
}
|
||||
else {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
connectionPool[i].write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
connectionPool[i].write(Buffer.from(""+msg.payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,13 +557,34 @@ module.exports = function(RED) {
|
||||
|
||||
this.on("input", function(msg, nodeSend, nodeDone) {
|
||||
var i = 0;
|
||||
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
|
||||
if (msg.payload !== undefined && (!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
|
||||
msg.payload = msg.payload.toString();
|
||||
}
|
||||
|
||||
var host = node.server || msg.host;
|
||||
var port = node.port || msg.port;
|
||||
|
||||
if (node.out === "sit" && msg?.reset) {
|
||||
if (msg.reset === true) { // kill all connections
|
||||
for (var cl in clients) {
|
||||
if (clients[cl].hasOwnProperty("client")) {
|
||||
clients[cl].client.destroy();
|
||||
delete clients[cl];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof(msg.reset) === "string" && msg.reset.includes(":")) { // just kill connection host:port
|
||||
if (clients.hasOwnProperty(msg.reset) && clients[msg.reset].hasOwnProperty("client")) {
|
||||
clients[msg.reset].client.destroy();
|
||||
delete clients[msg.reset];
|
||||
}
|
||||
}
|
||||
const cc = Object.keys(clients).length;
|
||||
node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
|
||||
if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
|
||||
if (!msg.hasOwnProperty("payload")) { return; }
|
||||
}
|
||||
|
||||
// Store client information independently
|
||||
// the clients object will have:
|
||||
// clients[id].client, clients[id].msg, clients[id].timeout
|
||||
@@ -621,13 +652,16 @@ module.exports = function(RED) {
|
||||
clients[connection_id].connecting = true;
|
||||
clients[connection_id].client.connect(connOpts, function() {
|
||||
//node.log(RED._("tcpin.errors.client-connected"));
|
||||
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
|
||||
// node.status({fill:"green",shape:"dot",text:"common.status.connected"});
|
||||
node.status({fill:"green",shape:"dot",text:RED._("tcpin.status.connections",{count:Object.keys(clients).length})});
|
||||
if (clients[connection_id] && clients[connection_id].client) {
|
||||
clients[connection_id].connected = true;
|
||||
clients[connection_id].connecting = false;
|
||||
let event;
|
||||
while (event = dequeue(clients[connection_id].msgQueue)) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
if (event.msg.payload !== undefined) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
}
|
||||
event.nodeDone();
|
||||
}
|
||||
if (node.out === "time" && node.splitc < 0) {
|
||||
@@ -823,7 +857,9 @@ module.exports = function(RED) {
|
||||
else if (!clients[connection_id].connecting && clients[connection_id].connected) {
|
||||
if (clients[connection_id] && clients[connection_id].client) {
|
||||
let event = dequeue(clients[connection_id].msgQueue)
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
if (event.msg.payload !== undefined ) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
}
|
||||
event.nodeDone();
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,20 @@
|
||||
</select>
|
||||
<input style="width:40px;" type="text" id="node-input-sep" pattern=".">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-code"></i> <span data-i18n="csv.label.spec"></span></label>
|
||||
<div style="display: inline-grid;width: 70%;">
|
||||
<select style="width:100%" id="csv-option-spec">
|
||||
<option value="rfc" data-i18n="csv.spec.rfc"></option>
|
||||
<option value="" data-i18n="csv.spec.legacy"></option>
|
||||
</select>
|
||||
<div>
|
||||
<div class="form-tips csv-lecacy-warning" data-i18n="node-red:csv.spec.legacy_warning"
|
||||
style="width: calc(100% - 18px); margin-top: 4px; max-width: unset;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
|
||||
@@ -60,10 +73,10 @@
|
||||
<div class="form-row" style="padding-left:20px;">
|
||||
<label></label>
|
||||
<label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
|
||||
<select style="width:150px;" id="node-input-ret">
|
||||
<select style="width:calc(70% - 108px);" id="node-input-ret">
|
||||
<option value='\r\n' data-i18n="csv.newline.windows"></option>
|
||||
<option value='\n' data-i18n="csv.newline.linux"></option>
|
||||
<option value='\r' data-i18n="csv.newline.mac"></option>
|
||||
<option value='\r\n' data-i18n="csv.newline.windows"></option>
|
||||
</select>
|
||||
</div>
|
||||
</script>
|
||||
@@ -75,6 +88,7 @@
|
||||
color:"#DEBD5C",
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
spec: {value:"rfc"},
|
||||
sep: {
|
||||
value:',', required:true,
|
||||
label:RED._("node-red:csv.label.separator"),
|
||||
@@ -83,7 +97,7 @@
|
||||
hdrin: {value:""},
|
||||
hdrout: {value:"none"},
|
||||
multi: {value:"one",required:true},
|
||||
ret: {value:'\\n'},
|
||||
ret: {value:'\\r\\n'}, // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
|
||||
temp: {value:""},
|
||||
skip: {value:"0"},
|
||||
strings: {value:true},
|
||||
@@ -123,6 +137,27 @@
|
||||
$("#node-input-sep").hide();
|
||||
}
|
||||
});
|
||||
|
||||
$("#csv-option-spec").on("change", function() {
|
||||
if ($("#csv-option-spec").val() == "rfc") {
|
||||
$(".form-tips.csv-lecacy-warning").hide();
|
||||
} else {
|
||||
$(".form-tips.csv-lecacy-warning").show();
|
||||
}
|
||||
});
|
||||
// new nodes will have `spec` set to "rfc" (default), but existing nodes will either not have
|
||||
// a spec value or it will be empty - we need to maintain the legacy behaviour for existing
|
||||
// flows but default to rfc for new nodes
|
||||
let spec = !this.spec ? "" : "rfc"
|
||||
$("#csv-option-spec").val(spec).trigger("change")
|
||||
},
|
||||
oneditsave: function() {
|
||||
const specFormVal = $("#csv-option-spec").val() || '' // empty === legacy
|
||||
const spectNodeVal = this.spec || '' // empty === legacy, null/undefined means in-place node upgrade (keep as is)
|
||||
if (specFormVal !== spectNodeVal) {
|
||||
// only update the flow value if changed (avoid marking the node dirty unnecessarily)
|
||||
this.spec = specFormVal
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@@ -15,308 +15,674 @@
|
||||
**/
|
||||
|
||||
module.exports = function(RED) {
|
||||
const csv = require('./lib/csv')
|
||||
|
||||
"use strict";
|
||||
function CSVNode(n) {
|
||||
RED.nodes.createNode(this,n);
|
||||
this.template = (n.temp || "");
|
||||
this.sep = (n.sep || ',').replace("\\t","\t").replace("\\n","\n").replace("\\r","\r");
|
||||
this.quo = '"';
|
||||
this.ret = (n.ret || "\n").replace("\\n","\n").replace("\\r","\r");
|
||||
this.winflag = (this.ret === "\r\n");
|
||||
this.lineend = "\n";
|
||||
this.multi = n.multi || "one";
|
||||
this.hdrin = n.hdrin || false;
|
||||
this.hdrout = n.hdrout || "none";
|
||||
this.goodtmpl = true;
|
||||
this.skip = parseInt(n.skip || 0);
|
||||
this.store = [];
|
||||
this.parsestrings = n.strings;
|
||||
this.include_empty_strings = n.include_empty_strings || false;
|
||||
this.include_null_values = n.include_null_values || false;
|
||||
if (this.parsestrings === undefined) { this.parsestrings = true; }
|
||||
if (this.hdrout === false) { this.hdrout = "none"; }
|
||||
if (this.hdrout === true) { this.hdrout = "all"; }
|
||||
var tmpwarn = true;
|
||||
var node = this;
|
||||
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
|
||||
RED.nodes.createNode(this,n)
|
||||
const node = this
|
||||
const RFC4180Mode = n.spec === 'rfc'
|
||||
const legacyMode = !RFC4180Mode
|
||||
|
||||
// pass in an array of column names to be trimmed, de-quoted and retrimmed
|
||||
var clean = function(col,sep) {
|
||||
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
|
||||
col = col.trim().split(re) || [""];
|
||||
col = col.map(x => x.replace(/"/g,'').trim());
|
||||
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
|
||||
else { node.goodtmpl = true; }
|
||||
return col;
|
||||
}
|
||||
var template = clean(node.template,',');
|
||||
var notemplate = template.length === 1 && template[0] === '';
|
||||
node.hdrSent = false;
|
||||
node.status({}) // clear status
|
||||
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
node.hdrSent = false;
|
||||
if (legacyMode) {
|
||||
this.template = (n.temp || "");
|
||||
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
|
||||
this.quo = '"';
|
||||
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
|
||||
this.winflag = (this.ret === "\r\n");
|
||||
this.lineend = "\n";
|
||||
this.multi = n.multi || "one";
|
||||
this.hdrin = n.hdrin || false;
|
||||
this.hdrout = n.hdrout || "none";
|
||||
this.goodtmpl = true;
|
||||
this.skip = parseInt(n.skip || 0);
|
||||
this.store = [];
|
||||
this.parsestrings = n.strings;
|
||||
this.include_empty_strings = n.include_empty_strings || false;
|
||||
this.include_null_values = n.include_null_values || false;
|
||||
if (this.parsestrings === undefined) { this.parsestrings = true; }
|
||||
if (this.hdrout === false) { this.hdrout = "none"; }
|
||||
if (this.hdrout === true) { this.hdrout = "all"; }
|
||||
var tmpwarn = true;
|
||||
// var node = this;
|
||||
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
|
||||
|
||||
// pass in an array of column names to be trimmed, de-quoted and retrimmed
|
||||
var clean = function(col,sep) {
|
||||
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
|
||||
col = col.trim().split(re) || [""];
|
||||
col = col.map(x => x.replace(/"/g,'').trim());
|
||||
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
|
||||
else { node.goodtmpl = true; }
|
||||
return col;
|
||||
}
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
if (typeof msg.payload == "object") { // convert object to CSV string
|
||||
try {
|
||||
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
||||
template = clean(node.template);
|
||||
}
|
||||
var ou = "";
|
||||
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
|
||||
if (node.hdrout !== "none" && node.hdrSent === false) {
|
||||
if ((template.length === 1) && (template[0] === '')) {
|
||||
if (msg.hasOwnProperty("columns")) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
else {
|
||||
template = Object.keys(msg.payload[0]);
|
||||
}
|
||||
var template = clean(node.template,',');
|
||||
var notemplate = template.length === 1 && template[0] === '';
|
||||
node.hdrSent = false;
|
||||
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
node.hdrSent = false;
|
||||
}
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
if (typeof msg.payload == "object") { // convert object to CSV string
|
||||
try {
|
||||
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
||||
template = clean(node.template);
|
||||
}
|
||||
ou += template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep) + node.ret;
|
||||
if (node.hdrout === "once") { node.hdrSent = true; }
|
||||
}
|
||||
for (var s = 0; s < msg.payload.length; s++) {
|
||||
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
|
||||
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
|
||||
for (var t = 0; t < msg.payload[s].length; t++) {
|
||||
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
|
||||
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
}
|
||||
ou += msg.payload[s].join(node.sep) + node.ret;
|
||||
}
|
||||
else {
|
||||
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
const ou = [];
|
||||
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
|
||||
if (node.hdrout !== "none" && node.hdrSent === false) {
|
||||
if ((template.length === 1) && (template[0] === '')) {
|
||||
/* istanbul ignore else */
|
||||
if (tmpwarn === true) { // just warn about missing template once
|
||||
node.warn(RED._("csv.errors.obj_csv"));
|
||||
tmpwarn = false;
|
||||
if (msg.hasOwnProperty("columns")) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
for (var p in msg.payload[0]) {
|
||||
else {
|
||||
template = Object.keys(msg.payload[0]);
|
||||
}
|
||||
}
|
||||
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
|
||||
if (node.hdrout === "once") { node.hdrSent = true; }
|
||||
}
|
||||
for (var s = 0; s < msg.payload.length; s++) {
|
||||
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
|
||||
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
|
||||
for (var t = 0; t < msg.payload[s].length; t++) {
|
||||
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
|
||||
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
}
|
||||
ou.push(msg.payload[s].join(node.sep));
|
||||
}
|
||||
else {
|
||||
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
if ((template.length === 1) && (template[0] === '')) {
|
||||
/* istanbul ignore else */
|
||||
if (msg.payload[s].hasOwnProperty(p)) {
|
||||
if (tmpwarn === true) { // just warn about missing template once
|
||||
node.warn(RED._("csv.errors.obj_csv"));
|
||||
tmpwarn = false;
|
||||
}
|
||||
const row = [];
|
||||
for (var p in msg.payload[0]) {
|
||||
/* istanbul ignore else */
|
||||
if (typeof msg.payload[s][p] !== "object") {
|
||||
var q = "" + msg.payload[s][p];
|
||||
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
q = q.replace(/"/g, '""');
|
||||
ou += node.quo + q + node.quo + node.sep;
|
||||
if (msg.payload[s].hasOwnProperty(p)) {
|
||||
/* istanbul ignore else */
|
||||
if (typeof msg.payload[s][p] !== "object") {
|
||||
// Fix to honour include null values flag
|
||||
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
|
||||
var q = "";
|
||||
if (msg.payload[s][p] !== undefined) {
|
||||
q += msg.payload[s][p];
|
||||
}
|
||||
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
q = q.replace(/"/g, '""');
|
||||
row.push(node.quo + q + node.quo);
|
||||
}
|
||||
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
row.push(node.quo + q + node.quo);
|
||||
}
|
||||
else { row.push(q); } // otherwise just add
|
||||
}
|
||||
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
ou += node.quo + q + node.quo + node.sep;
|
||||
}
|
||||
else { ou += q + node.sep; } // otherwise just add
|
||||
}
|
||||
}
|
||||
ou.push(row.join(node.sep)); // add separator
|
||||
}
|
||||
ou = ou.slice(0,-1) + node.ret;
|
||||
else {
|
||||
const row = [];
|
||||
for (var t=0; t < template.length; t++) {
|
||||
if (template[t] === '') {
|
||||
row.push('');
|
||||
}
|
||||
else {
|
||||
var tt = template[t];
|
||||
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
|
||||
else { tt = '"'+tt+'"'; }
|
||||
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
|
||||
/* istanbul ignore else */
|
||||
if (p === undefined) { p = ""; }
|
||||
// fix to honour include null values flag
|
||||
//if (p === null && node.include_null_values !== true) { p = "";}
|
||||
p = RED.util.ensureString(p);
|
||||
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
p = p.replace(/"/g, '""');
|
||||
row.push(node.quo + p + node.quo);
|
||||
}
|
||||
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
row.push(node.quo + p + node.quo);
|
||||
}
|
||||
else { row.push(p); } // otherwise just add
|
||||
}
|
||||
}
|
||||
ou.push(row.join(node.sep)); // add separator
|
||||
}
|
||||
}
|
||||
}
|
||||
// join lines, don't forget to add the last new line
|
||||
msg.payload = ou.join(node.ret) + node.ret;
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
|
||||
if (msg.payload !== '') {
|
||||
send(msg);
|
||||
}
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
}
|
||||
else if (typeof msg.payload == "string") { // convert CSV string to object
|
||||
try {
|
||||
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
|
||||
var j = 0; // pointer into array of template items
|
||||
var k = [""]; // array of data for each of the template items
|
||||
var o = {}; // output object to build up
|
||||
var a = []; // output array is needed for multiline option
|
||||
var first = true; // is this the first line
|
||||
var last = false;
|
||||
var line = msg.payload;
|
||||
var linecount = 0;
|
||||
var tmp = "";
|
||||
var has_parts = msg.hasOwnProperty("parts");
|
||||
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
|
||||
if (msg.hasOwnProperty("parts")) {
|
||||
linecount = msg.parts.index;
|
||||
if (msg.parts.index > node.skip) { first = false; }
|
||||
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
|
||||
}
|
||||
|
||||
// For now we are just going to assume that any \r or \n means an end of line...
|
||||
// got to be a weird csv that has singleton \r \n in it for another reason...
|
||||
|
||||
// Now process the whole file/line
|
||||
var nocr = (line.match(/[\r\n]/g)||[]).length;
|
||||
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
|
||||
for (var i = 0; i < line.length; i++) {
|
||||
if (first && (linecount < node.skip)) {
|
||||
if (line[i] === "\n") { linecount += 1; }
|
||||
continue;
|
||||
}
|
||||
if ((node.hdrin === true) && first) { // if the template is in the first line
|
||||
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
|
||||
if (line.length - i === 1) { tmp += line[i]; }
|
||||
template = clean(tmp,node.sep);
|
||||
first = false;
|
||||
}
|
||||
else { tmp += line[i]; }
|
||||
}
|
||||
else {
|
||||
for (var t=0; t < template.length; t++) {
|
||||
if (template[t] === '') {
|
||||
ou += node.sep;
|
||||
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
|
||||
f = !f;
|
||||
if (line[i-1] === node.quo) {
|
||||
if (f === false) { k[j] += '\"'; }
|
||||
} // if it's a quotequote then it's actually a quote
|
||||
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
|
||||
}
|
||||
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
|
||||
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
else {
|
||||
var p = RED.util.ensureString(RED.util.getMessageProperty(msg,"payload["+s+"]['"+template[t]+"']"));
|
||||
/* istanbul ignore else */
|
||||
if (p === "undefined") { p = ""; }
|
||||
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
p = p.replace(/"/g, '""');
|
||||
ou += node.quo + p + node.quo + node.sep;
|
||||
}
|
||||
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
ou += node.quo + p + node.quo + node.sep;
|
||||
}
|
||||
else { ou += p + node.sep; } // otherwise just add
|
||||
j += 1;
|
||||
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
|
||||
k[j] = line.length - 1 === i ? null : "";
|
||||
}
|
||||
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
|
||||
//console.log(j,k,o,k[j]);
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
|
||||
if (line[i-1] === node.sep) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
j = 0;
|
||||
k = [""];
|
||||
o = {};
|
||||
f = true; // reset in/out flag ready for next line.
|
||||
}
|
||||
else { // just add to the part of the message
|
||||
k[j] += line[i];
|
||||
}
|
||||
ou = ou.slice(0,-1) + node.ret; // remove final "comma" and add "newline"
|
||||
}
|
||||
}
|
||||
}
|
||||
msg.payload = ou;
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
|
||||
if (msg.payload !== '') { send(msg); }
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
}
|
||||
else if (typeof msg.payload == "string") { // convert CSV string to object
|
||||
try {
|
||||
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
|
||||
var j = 0; // pointer into array of template items
|
||||
var k = [""]; // array of data for each of the template items
|
||||
var o = {}; // output object to build up
|
||||
var a = []; // output array is needed for multiline option
|
||||
var first = true; // is this the first line
|
||||
var last = false;
|
||||
var line = msg.payload;
|
||||
var linecount = 0;
|
||||
var tmp = "";
|
||||
var has_parts = msg.hasOwnProperty("parts");
|
||||
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
|
||||
if (msg.hasOwnProperty("parts")) {
|
||||
linecount = msg.parts.index;
|
||||
if (msg.parts.index > node.skip) { first = false; }
|
||||
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
|
||||
}
|
||||
// Finished so finalize and send anything left
|
||||
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
|
||||
// For now we are just going to assume that any \r or \n means an end of line...
|
||||
// got to be a weird csv that has singleton \r \n in it for another reason...
|
||||
|
||||
// Now process the whole file/line
|
||||
var nocr = (line.match(/[\r\n]/g)||[]).length;
|
||||
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
|
||||
for (var i = 0; i < line.length; i++) {
|
||||
if (first && (linecount < node.skip)) {
|
||||
if (line[i] === "\n") { linecount += 1; }
|
||||
continue;
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
if ((node.hdrin === true) && first) { // if the template is in the first line
|
||||
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
|
||||
if (line.length - i === 1) { tmp += line[i]; }
|
||||
template = clean(tmp,node.sep);
|
||||
first = false;
|
||||
}
|
||||
else { tmp += line[i]; }
|
||||
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
else {
|
||||
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
|
||||
f = !f;
|
||||
if (line[i-1] === node.quo) {
|
||||
if (f === false) { k[j] += '\"'; }
|
||||
} // if it's a quotequote then it's actually a quote
|
||||
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
|
||||
}
|
||||
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
|
||||
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
j += 1;
|
||||
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
|
||||
k[j] = line.length - 1 === i ? null : "";
|
||||
}
|
||||
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
|
||||
//console.log(j,k,o,k[j]);
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
|
||||
if (line[i-1] === node.sep) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
j = 0;
|
||||
k = [""];
|
||||
o = {};
|
||||
f = true; // reset in/out flag ready for next line.
|
||||
}
|
||||
else { // just add to the part of the message
|
||||
k[j] += line[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Finished so finalize and send anything left
|
||||
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
|
||||
if (node.multi !== "one") {
|
||||
msg.payload = a;
|
||||
if (has_parts && nocr <= 1) {
|
||||
if (JSON.stringify(o) !== "{}") {
|
||||
node.store.push(o);
|
||||
if (node.multi !== "one") {
|
||||
msg.payload = a;
|
||||
if (has_parts && nocr <= 1) {
|
||||
if (JSON.stringify(o) !== "{}") {
|
||||
node.store.push(o);
|
||||
}
|
||||
if (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store;
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
delete msg.parts;
|
||||
send(msg);
|
||||
node.store = [];
|
||||
}
|
||||
}
|
||||
if (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store;
|
||||
else {
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
delete msg.parts;
|
||||
send(msg);
|
||||
node.store = [];
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
else {
|
||||
var len = a.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var newMessage = RED.util.cloneMessage(msg);
|
||||
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
newMessage.payload = a[i];
|
||||
if (!has_parts) {
|
||||
newMessage.parts = {
|
||||
id: msg._msgid,
|
||||
index: i,
|
||||
count: len
|
||||
};
|
||||
var len = a.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var newMessage = RED.util.cloneMessage(msg);
|
||||
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
newMessage.payload = a[i];
|
||||
if (!has_parts) {
|
||||
newMessage.parts = {
|
||||
id: msg._msgid,
|
||||
index: i,
|
||||
count: len
|
||||
};
|
||||
}
|
||||
else {
|
||||
newMessage.parts.index -= node.skip;
|
||||
newMessage.parts.count -= node.skip;
|
||||
if (node.hdrin) { // if we removed the header line then shift the counts by 1
|
||||
newMessage.parts.index -= 1;
|
||||
newMessage.parts.count -= 1;
|
||||
}
|
||||
}
|
||||
if (last) { newMessage.complete = true; }
|
||||
send(newMessage);
|
||||
}
|
||||
else {
|
||||
newMessage.parts.index -= node.skip;
|
||||
newMessage.parts.count -= node.skip;
|
||||
if (node.hdrin) { // if we removed the header line then shift the counts by 1
|
||||
newMessage.parts.index -= 1;
|
||||
newMessage.parts.count -= 1;
|
||||
if (has_parts && last && len === 0) {
|
||||
send({complete:true});
|
||||
}
|
||||
}
|
||||
node.linecount = 0;
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
}
|
||||
else { node.warn(RED._("csv.errors.csv_js")); done(); }
|
||||
}
|
||||
else {
|
||||
if (!msg.hasOwnProperty("reset")) {
|
||||
node.send(msg); // If no payload and not reset - just pass it on.
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(RFC4180Mode) {
|
||||
node.template = (n.temp || "")
|
||||
node.sep = (n.sep || ',').replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
|
||||
node.quo = '"'
|
||||
// default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
|
||||
node.ret = (n.ret || "\r\n").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
|
||||
node.multi = n.multi || "one"
|
||||
node.hdrin = n.hdrin || false
|
||||
node.hdrout = n.hdrout || "none"
|
||||
node.goodtmpl = true
|
||||
node.skip = parseInt(n.skip || 0)
|
||||
node.store = []
|
||||
node.parsestrings = n.strings
|
||||
node.include_empty_strings = n.include_empty_strings || false
|
||||
node.include_null_values = n.include_null_values || false
|
||||
if (node.parsestrings === undefined) { node.parsestrings = true }
|
||||
if (node.hdrout === false) { node.hdrout = "none" }
|
||||
if (node.hdrout === true) { node.hdrout = "all" }
|
||||
const dontSendHeaders = node.hdrout === "none"
|
||||
const sendHeadersOnce = node.hdrout === "once"
|
||||
const sendHeadersAlways = node.hdrout === "all"
|
||||
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
|
||||
const quoteables = [node.sep, node.quo, "\n", "\r"]
|
||||
const templateQuoteables = [',', '"', "\n", "\r"]
|
||||
let badTemplateWarnOnce = true
|
||||
|
||||
const columnStringToTemplateArray = function (col, sep) {
|
||||
// NOTE: enforce strict column template parsing in RFC4180 mode
|
||||
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
|
||||
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
|
||||
return parsed.headers.length ? parsed.headers : null
|
||||
}
|
||||
const templateArrayToColumnString = function (template, keepEmptyColumns) {
|
||||
// NOTE: enforce strict column template parsing in RFC4180 mode
|
||||
const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true })
|
||||
return keepEmptyColumns
|
||||
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
|
||||
: parsed.header // exclues empty columns
|
||||
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV
|
||||
// CSV->JSON: empty columns are excluded
|
||||
// JSON->CSV: empty columns are kept in some cases
|
||||
}
|
||||
function addQuotes(cell, options) {
|
||||
options = options || {}
|
||||
return csv.quoteCell(cell, {
|
||||
quote: options.quote || node.quo || '"',
|
||||
separator: options.separator || node.sep || ',',
|
||||
quoteables: options.quoteables || quoteables
|
||||
})
|
||||
}
|
||||
const hasTemplate = (t) => t?.length > 0 && !(t.length === 1 && t[0] === '')
|
||||
let template
|
||||
try {
|
||||
template = columnStringToTemplateArray(node.template, ',') || ['']
|
||||
} catch (e) {
|
||||
node.warn(RED._("csv.errors.bad_template")) // is warning really necessary now we have status?
|
||||
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
|
||||
return // dont hook up the node
|
||||
}
|
||||
const noTemplate = hasTemplate(template) === false
|
||||
node.hdrSent = false
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
node.status({}) // clear status
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
node.hdrSent = false
|
||||
}
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
let inputData = msg.payload
|
||||
if (typeof inputData == "object") { // convert object to CSV string
|
||||
try {
|
||||
// first determine the payload kind. Array or objects? Array of primitives? Array of arrays? Just an object?
|
||||
// then, if necessary, convert to an array of objects/arrays
|
||||
let isObject = !Array.isArray(inputData) && typeof inputData === 'object'
|
||||
let isArrayOfObjects = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] === 'object'
|
||||
let isArrayOfArrays = Array.isArray(inputData) && inputData.length > 0 && Array.isArray(inputData[0])
|
||||
let isArrayOfPrimitives = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] !== 'object'
|
||||
|
||||
if (isObject) {
|
||||
inputData = [inputData]
|
||||
isArrayOfObjects = true
|
||||
isObject = false
|
||||
} else if (isArrayOfPrimitives) {
|
||||
inputData = [inputData]
|
||||
isArrayOfArrays = true
|
||||
isArrayOfPrimitives = false
|
||||
}
|
||||
|
||||
const stringBuilder = []
|
||||
if (!(noTemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
||||
template = columnStringToTemplateArray(node.template) || ['']
|
||||
}
|
||||
|
||||
// build header line
|
||||
if (sendHeaders && node.hdrSent === false) {
|
||||
if (hasTemplate(template) === false) {
|
||||
if (msg.hasOwnProperty("columns")) {
|
||||
template = columnStringToTemplateArray(msg.columns || "", ",") || ['']
|
||||
}
|
||||
else {
|
||||
template = Object.keys(inputData[0]) || ['']
|
||||
}
|
||||
}
|
||||
if (last) { newMessage.complete = true; }
|
||||
send(newMessage);
|
||||
stringBuilder.push(templateArrayToColumnString(template, true))
|
||||
if (sendHeadersOnce) { node.hdrSent = true }
|
||||
}
|
||||
if (has_parts && last && len === 0) {
|
||||
send({complete:true});
|
||||
|
||||
// build csv lines
|
||||
for (let s = 0; s < inputData.length; s++) {
|
||||
let row = inputData[s]
|
||||
if (isArrayOfArrays) {
|
||||
/*** row is an array of arrays ***/
|
||||
const _hasTemplate = hasTemplate(template)
|
||||
const len = _hasTemplate ? template.length : row.length
|
||||
const result = []
|
||||
for (let t = 0; t < len; t++) {
|
||||
let cell = row[t]
|
||||
if (cell === undefined) { cell = "" }
|
||||
if(_hasTemplate) {
|
||||
const header = template[t]
|
||||
if (header) {
|
||||
result[t] = addQuotes(RED.util.ensureString(cell))
|
||||
}
|
||||
} else {
|
||||
result[t] = addQuotes(RED.util.ensureString(cell))
|
||||
}
|
||||
}
|
||||
stringBuilder.push(result.join(node.sep))
|
||||
} else {
|
||||
/*** row is an object ***/
|
||||
if (hasTemplate(template) === false && (msg.hasOwnProperty("columns"))) {
|
||||
template = columnStringToTemplateArray(msg.columns || "", ",")
|
||||
}
|
||||
if (hasTemplate(template) === false) {
|
||||
/*** row is an object but we still don't have a template ***/
|
||||
if (badTemplateWarnOnce === true) {
|
||||
node.warn(RED._("csv.errors.obj_csv"))
|
||||
badTemplateWarnOnce = false
|
||||
}
|
||||
const rowData = []
|
||||
for (let header in inputData[0]) {
|
||||
if (row.hasOwnProperty(header)) {
|
||||
const cell = row[header]
|
||||
if (typeof cell !== "object") {
|
||||
let cellValue = ""
|
||||
if (cell !== undefined) {
|
||||
cellValue += cell
|
||||
}
|
||||
rowData.push(addQuotes(cellValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
stringBuilder.push(rowData.join(node.sep))
|
||||
} else {
|
||||
/*** row is an object and we have a template ***/
|
||||
const rowData = []
|
||||
for (let t = 0; t < template.length; t++) {
|
||||
if (!template[t]) {
|
||||
rowData.push('')
|
||||
}
|
||||
else {
|
||||
let cellValue = inputData[s][template[t]]
|
||||
if (cellValue === undefined) { cellValue = "" }
|
||||
cellValue = RED.util.ensureString(cellValue)
|
||||
rowData.push(addQuotes(cellValue))
|
||||
}
|
||||
}
|
||||
stringBuilder.push(rowData.join(node.sep)); // add separator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// join lines, don't forget to add the last new line
|
||||
msg.payload = stringBuilder.join(node.ret) + node.ret
|
||||
msg.columns = templateArrayToColumnString(template)
|
||||
if (msg.payload !== '') { send(msg) }
|
||||
done()
|
||||
}
|
||||
catch (e) {
|
||||
done(e)
|
||||
}
|
||||
node.linecount = 0;
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
else if (typeof inputData == "string") { // convert CSV string to object
|
||||
try {
|
||||
let firstLine = true; // is this the first line
|
||||
let last = false
|
||||
let linecount = 0
|
||||
const has_parts = msg.hasOwnProperty("parts")
|
||||
|
||||
// determine if this is a multi part message and if so what part we are processing
|
||||
if (msg.hasOwnProperty("parts")) {
|
||||
linecount = msg.parts.index
|
||||
if (msg.parts.index > node.skip) { firstLine = false }
|
||||
if (msg.parts.hasOwnProperty("count") && (msg.parts.index + 1 >= msg.parts.count)) { last = true }
|
||||
}
|
||||
|
||||
// If skip is set, compute the cursor position to start parsing from
|
||||
let _cursor = 0
|
||||
if (node.skip > 0 && linecount < node.skip) {
|
||||
for (; _cursor < inputData.length; _cursor++) {
|
||||
if (firstLine && (linecount < node.skip)) {
|
||||
if (inputData[_cursor] === "\r" || inputData[_cursor] === "\n") {
|
||||
linecount += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if (_cursor >= inputData.length) {
|
||||
return // skip this line
|
||||
}
|
||||
}
|
||||
|
||||
// count the number of line breaks in the string
|
||||
const noofCR = ((_cursor ? inputData.slice(_cursor) : inputData).match(/[\r\n]/g) || []).length
|
||||
|
||||
// if we have `parts` and we are outputting multiple objects and we have more than one line
|
||||
// then we need to set firstLine to true so that we process the header line
|
||||
if (has_parts && node.multi === "mult" && noofCR > 1) {
|
||||
firstLine = true
|
||||
}
|
||||
|
||||
// if we are processing the first line and the node has been set to extract the header line
|
||||
// update the template with the header line
|
||||
if (firstLine && node.hdrin === true) {
|
||||
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
|
||||
const csvOptionsForHeaderRow = {
|
||||
cursor: _cursor,
|
||||
separator: node.sep,
|
||||
quote: node.quo,
|
||||
dataHasHeaderRow: true,
|
||||
headersOnly: true,
|
||||
outputStyle: 'array',
|
||||
strict: true // enforce strict parsing of the header row
|
||||
}
|
||||
try {
|
||||
const csvHeader = csv.parse(inputData, csvOptionsForHeaderRow)
|
||||
template = csvHeader.headers
|
||||
_cursor = csvHeader.cursor
|
||||
} catch (e) {
|
||||
// node.warn(RED._("csv.errors.bad_template")) // add warning?
|
||||
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// now we process the data lines
|
||||
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
|
||||
const csvOptions = {
|
||||
cursor: _cursor,
|
||||
separator: node.sep,
|
||||
quote: node.quo,
|
||||
dataHasHeaderRow: false,
|
||||
headers: hasTemplate(template) ? template : null,
|
||||
outputStyle: 'object',
|
||||
includeNullValues: node.include_null_values,
|
||||
includeEmptyStrings: node.include_empty_strings,
|
||||
parseNumeric: node.parsestrings,
|
||||
strict: false // relax the strictness of the parser for data rows
|
||||
}
|
||||
const csvParseResult = csv.parse(inputData, csvOptions)
|
||||
const data = csvParseResult.data
|
||||
|
||||
// output results
|
||||
if (node.multi !== "one") {
|
||||
if (has_parts && noofCR <= 1) {
|
||||
if (data.length > 0) {
|
||||
node.store.push(...data)
|
||||
}
|
||||
if (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store
|
||||
msg.columns = csvParseResult.header
|
||||
// msg._mode = 'RFC4180 mode'
|
||||
delete msg.parts
|
||||
send(msg)
|
||||
node.store = []
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg.columns = csvParseResult.header
|
||||
// msg._mode = 'RFC4180 mode'
|
||||
msg.payload = data
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
else {
|
||||
const len = data.length
|
||||
for (let row = 0; row < len; row++) {
|
||||
const newMessage = RED.util.cloneMessage(msg)
|
||||
newMessage.columns = csvParseResult.header
|
||||
newMessage.payload = data[row]
|
||||
if (!has_parts) {
|
||||
newMessage.parts = {
|
||||
id: msg._msgid,
|
||||
index: row,
|
||||
count: len
|
||||
}
|
||||
}
|
||||
else {
|
||||
newMessage.parts.index -= node.skip
|
||||
newMessage.parts.count -= node.skip
|
||||
if (node.hdrin) { // if we removed the header line then shift the counts by 1
|
||||
newMessage.parts.index -= 1
|
||||
newMessage.parts.count -= 1
|
||||
}
|
||||
}
|
||||
if (last) { newMessage.complete = true }
|
||||
// newMessage._mode = 'RFC4180 mode'
|
||||
send(newMessage)
|
||||
}
|
||||
if (has_parts && last && len === 0) {
|
||||
// send({complete:true, _mode: 'RFC4180 mode'})
|
||||
send({ complete: true })
|
||||
}
|
||||
}
|
||||
|
||||
node.linecount = 0
|
||||
done()
|
||||
}
|
||||
catch (e) {
|
||||
done(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// RFC-vs-legacy mode difference: In RFC mode, we throw catchable errors and provide a status message
|
||||
const err = new Error(RED._("csv.errors.csv_js"))
|
||||
node.status({ fill: "red", shape: "dot", text: err.message })
|
||||
done(err)
|
||||
}
|
||||
}
|
||||
else { node.warn(RED._("csv.errors.csv_js")); done(); }
|
||||
}
|
||||
else {
|
||||
if (!msg.hasOwnProperty("reset")) {
|
||||
node.send(msg); // If no payload and not reset - just pass it on.
|
||||
else {
|
||||
if (!msg.hasOwnProperty("reset")) {
|
||||
node.send(msg); // If no payload and not reset - just pass it on.
|
||||
}
|
||||
done()
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType("csv",CSVNode);
|
||||
|
||||
RED.nodes.registerType("csv",CSVNode)
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@
|
||||
<option value="html" data-i18n="html.output.html"></option>
|
||||
<option value="text" data-i18n="html.output.text"></option>
|
||||
<option value="attr" data-i18n="html.output.attr"></option>
|
||||
<option value="compl" data-i18n="html.output.compl"></option>
|
||||
<!-- <option value="val">return the value from a form element</option> -->
|
||||
</select>
|
||||
</div>
|
||||
@@ -28,6 +29,10 @@
|
||||
<label for="node-input-outproperty"> </label>
|
||||
<span data-i18n="html.label.in" style="padding-left:8px; padding-right:2px; vertical-align:-1px;"></span> <input type="text" id="node-input-outproperty" style="width:64%">
|
||||
</div>
|
||||
<div id='html-prefix-row' class="form-row" style="display: none;">
|
||||
<label for="node-input-chr" style="width: 230px;"><i class="fa fa-tag"></i> <span data-i18n="html.label.prefix"></span></label>
|
||||
<input type="text" id="node-input-chr" style="text-align:center; width: 40px;" placeholder="_">
|
||||
</div>
|
||||
<br/>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
@@ -41,11 +46,12 @@
|
||||
color:"#DEBD5C",
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
property: {value:"payload"},
|
||||
outproperty: {value:"payload"},
|
||||
property: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
|
||||
outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
|
||||
tag: {value:""},
|
||||
ret: {value:"html"},
|
||||
as: {value:"single"}
|
||||
as: {value:"single"},
|
||||
chr: { value: "_" }
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
@@ -59,6 +65,13 @@
|
||||
oneditprepare: function() {
|
||||
$("#node-input-property").typedInput({default:'msg',types:['msg']});
|
||||
$("#node-input-outproperty").typedInput({default:'msg',types:['msg']});
|
||||
$('#node-input-ret').on( 'change', () => {
|
||||
if ( $('#node-input-ret').val() == "compl" ) {
|
||||
$('#html-prefix-row').show()
|
||||
} else {
|
||||
$('#html-prefix-row').hide()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@@ -25,6 +25,7 @@ module.exports = function(RED) {
|
||||
this.tag = n.tag;
|
||||
this.ret = n.ret || "html";
|
||||
this.as = n.as || "single";
|
||||
this.chr = n.chr || "_";
|
||||
var node = this;
|
||||
this.on("input", function(msg,send,done) {
|
||||
var value = RED.util.getMessageProperty(msg,node.property);
|
||||
@@ -47,6 +48,11 @@ module.exports = function(RED) {
|
||||
if (node.ret === "attr") {
|
||||
pay2 = Object.assign({},this.attribs);
|
||||
}
|
||||
if (node.ret === "compl") {
|
||||
var bse = {}
|
||||
bse[node.chr] = $(this).html().trim()
|
||||
pay2 = Object.assign(bse, this.attribs);
|
||||
}
|
||||
//if (node.ret === "val") { pay2 = $(this).val(); }
|
||||
/* istanbul ignore else */
|
||||
if (pay2) {
|
||||
@@ -69,6 +75,11 @@ module.exports = function(RED) {
|
||||
var attribs = Object.assign({},this.attribs);
|
||||
pay.push( attribs );
|
||||
}
|
||||
if (node.ret === "compl") {
|
||||
var bse = {}
|
||||
bse[node.chr] = $(this).html().trim()
|
||||
pay.push( Object.assign(bse, this.attribs) )
|
||||
}
|
||||
//if (node.ret === "val") { pay.push( $(this).val() ); }
|
||||
}
|
||||
index++;
|
||||
|
@@ -32,6 +32,7 @@
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
property: {value:"payload",required:true,
|
||||
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true}),
|
||||
label:RED._("node-red:json.label.property")},
|
||||
action: {value:""},
|
||||
pretty: {value:false}
|
||||
|
@@ -27,7 +27,8 @@
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
property: {value:"payload",required:true,
|
||||
label:RED._("node-red:common.label.property")},
|
||||
label:RED._("node-red:common.label.property"),
|
||||
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true })},
|
||||
attr: {value:""},
|
||||
chr: {value:""}
|
||||
},
|
||||
|
@@ -33,8 +33,7 @@ module.exports = function(RED) {
|
||||
parseString(value, options, function (err, result) {
|
||||
if (err) { done(err); }
|
||||
else {
|
||||
value = result;
|
||||
RED.util.setMessageProperty(msg,node.property,value);
|
||||
RED.util.setMessageProperty(msg,node.property,result);
|
||||
send(msg);
|
||||
done();
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@
|
||||
color:"#DEBD5C",
|
||||
defaults: {
|
||||
property: {value:"payload",required:true,
|
||||
validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }),
|
||||
label:RED._("node-red:common.label.property")},
|
||||
name: {value:""}
|
||||
},
|
||||
|
324
packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
vendored
Normal file
@@ -0,0 +1,324 @@
|
||||
|
||||
/**
|
||||
* @typedef {Object} CSVParseOptions
|
||||
* @property {number} [cursor=0] - an index into the CSV to start parsing from
|
||||
* @property {string} [separator=','] - the separator character
|
||||
* @property {string} [quote='"'] - the quote character
|
||||
* @property {boolean} [headersOnly=false] - only parse the headers and return them
|
||||
* @property {string[]} [headers=[]] - an array of headers to use instead of the first row of the CSV data
|
||||
* @property {boolean} [dataHasHeaderRow=true] - whether the CSV data to parse has a header row
|
||||
* @property {boolean} [outputHeader=true] - whether the output data should include a header row (only applies to array output)
|
||||
* @property {boolean} [parseNumeric=false] - parse numeric values into numbers
|
||||
* @property {boolean} [includeNullValues=false] - include null values in the output
|
||||
* @property {boolean} [includeEmptyStrings=true] - include empty strings in the output
|
||||
* @property {string} [outputStyle='object'] - output an array of arrays or an array of objects
|
||||
* @property {boolean} [strict=false] - throw an error if the CSV is malformed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a CSV string into an array of arrays or an array of objects.
|
||||
*
|
||||
* NOTES:
|
||||
* * Deviations from the RFC4180 spec (for the sake of user fiendliness, system implementations and flexibility), this parser will:
|
||||
* * accept any separator character, not just `,`
|
||||
* * accept any quote character, not just `"`
|
||||
* * parse `\r`, `\n` or `\r\n` as line endings (RRFC4180 2.1 states lines are separated by CRLF)
|
||||
* * Only single character `quote` is supported
|
||||
* * `quote` is `"` by default
|
||||
* * Any cell that contains a `quote` or `separator` will be quoted
|
||||
* * Any `quote` characters inside a cell will be escaped as per RFC 4180 2.6
|
||||
* * Only single character `separator` is supported
|
||||
* * Only `array` and `object` output styles are supported
|
||||
* * `array` output style is an array of arrays [[],[],[]]
|
||||
* * `object` output style is an array of objects [{},{},{}]
|
||||
* * Only `headers` or `dataHasHeaderRow` are supported, not both
|
||||
* @param {string} csvIn - the CSV string to parse
|
||||
* @param {CSVParseOptions} parseOptions - options
|
||||
* @throws {Error}
|
||||
*/
|
||||
function parse(csvIn, parseOptions) {
|
||||
/* Normalise options */
|
||||
parseOptions = parseOptions || {};
|
||||
const separator = parseOptions.separator ?? ',';
|
||||
const quote = parseOptions.quote ?? '"';
|
||||
const headersOnly = parseOptions.headersOnly ?? false;
|
||||
const headers = Array.isArray(parseOptions.headers) ? parseOptions.headers : []
|
||||
const dataHasHeaderRow = parseOptions.dataHasHeaderRow ?? true;
|
||||
const outputHeader = parseOptions.outputHeader ?? true;
|
||||
const parseNumeric = parseOptions.parseNumeric ?? false;
|
||||
const includeNullValues = parseOptions.includeNullValues ?? false;
|
||||
const includeEmptyStrings = parseOptions.includeEmptyStrings ?? true;
|
||||
const outputStyle = ['array', 'object'].includes(parseOptions.outputStyle) ? parseOptions.outputStyle : 'object'; // 'array [[],[],[]]' or 'object [{},{},{}]
|
||||
const strict = parseOptions.strict ?? false
|
||||
|
||||
/* Local variables */
|
||||
const cursorMax = csvIn.length;
|
||||
const ouputArrays = outputStyle === 'array';
|
||||
const headersSupplied = headers.length > 0
|
||||
// The original regex was an "is-a-number" positive logic test. /^ *[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+ *$/i;
|
||||
// Below, is less strict and inverted logic but coupled with +cast it is 13%+ faster than original regex+parsefloat
|
||||
// and has the benefit of understanding hexadecimals, binary and octal numbers.
|
||||
const skipNumberConversion = /^ *(\+|-0\d|0\d)/
|
||||
const cellBuilder = []
|
||||
let rowBuilder = []
|
||||
let cursor = typeof parseOptions.cursor === 'number' ? parseOptions.cursor : 0;
|
||||
let newCell = true, inQuote = false, closed = false, output = [];
|
||||
|
||||
/* inline helper functions */
|
||||
const finaliseCell = () => {
|
||||
let cell = cellBuilder.join('')
|
||||
cellBuilder.length = 0
|
||||
// push the cell:
|
||||
// NOTE: if cell is empty but newCell==true, then this cell had zero chars - push `null`
|
||||
// otherwise push empty string
|
||||
return rowBuilder.push(cell || (newCell ? null : ''))
|
||||
}
|
||||
const finaliseRow = () => {
|
||||
if (cellBuilder.length) {
|
||||
finaliseCell()
|
||||
}
|
||||
if (rowBuilder.length) {
|
||||
output.push(rowBuilder)
|
||||
rowBuilder = []
|
||||
}
|
||||
}
|
||||
|
||||
/* Main parsing loop */
|
||||
while (cursor < cursorMax) {
|
||||
const char = csvIn[cursor]
|
||||
if (inQuote) {
|
||||
if (char === quote && csvIn[cursor + 1] === quote) {
|
||||
cellBuilder.push(quote)
|
||||
cursor += 2;
|
||||
newCell = false;
|
||||
closed = false;
|
||||
} else if (char === quote) {
|
||||
inQuote = false;
|
||||
cursor += 1;
|
||||
newCell = false;
|
||||
closed = true;
|
||||
} else {
|
||||
cellBuilder.push(char)
|
||||
newCell = false;
|
||||
closed = false;
|
||||
cursor++;
|
||||
}
|
||||
} else {
|
||||
if (char === separator) {
|
||||
finaliseCell()
|
||||
cursor += 1;
|
||||
newCell = true;
|
||||
closed = false;
|
||||
} else if (char === quote) {
|
||||
if (newCell) {
|
||||
inQuote = true;
|
||||
cursor += 1;
|
||||
newCell = false;
|
||||
closed = false;
|
||||
}
|
||||
else if (strict) {
|
||||
throw new UnquotedQuoteError(cursor)
|
||||
} else {
|
||||
// not strict, keep 1 quote if the next char is not a cell/record separator
|
||||
cursor++
|
||||
if (csvIn[cursor] && csvIn[cursor] !== '\n' && csvIn[cursor] !== '\r' && csvIn[cursor] !== separator) {
|
||||
cellBuilder.push(char)
|
||||
if (csvIn[cursor] === quote) {
|
||||
cursor++ // skip the next quote
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (char === '\n' || char === '\r') {
|
||||
finaliseRow()
|
||||
if (csvIn[cursor + 1] === '\n') {
|
||||
cursor += 2;
|
||||
} else {
|
||||
cursor++
|
||||
}
|
||||
newCell = true;
|
||||
closed = false;
|
||||
if (headersOnly) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if (closed) {
|
||||
if (strict) {
|
||||
throw new DataAfterCloseError(cursor)
|
||||
} else {
|
||||
cursor--; // move back to grab the previously discarded char
|
||||
closed = false
|
||||
}
|
||||
} else {
|
||||
cellBuilder.push(char)
|
||||
newCell = false;
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (strict && inQuote) {
|
||||
throw new ParseError(`Missing quote, unclosed cell`, cursor)
|
||||
}
|
||||
// finalise the last cell/row
|
||||
finaliseRow()
|
||||
let firstRowIsHeader = false
|
||||
// if no headers supplied, generate them
|
||||
if (output.length >= 1) {
|
||||
if (headersSupplied) {
|
||||
// headers already supplied
|
||||
} else if (dataHasHeaderRow) {
|
||||
// take the first row as the headers
|
||||
headers.push(...output[0])
|
||||
firstRowIsHeader = true
|
||||
} else {
|
||||
// generate headers col1, col2, col3, etc
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
headers.push("col" + (i + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalResult = {
|
||||
/** @type {String[]} headers as an array of string */
|
||||
headers: headers,
|
||||
/** @type {String} headers as a comma-separated string */
|
||||
header: null,
|
||||
/** @type {Any[]} Result Data (may include header row: check `firstRowIsHeader` flag) */
|
||||
data: [],
|
||||
/** @type {Boolean|undefined} flag to indicate if the first row is a header row (only applies when `outputStyle` is 'array') */
|
||||
firstRowIsHeader: undefined,
|
||||
/** @type {'array'|'object'} flag to indicate the output style */
|
||||
outputStyle: outputStyle,
|
||||
/** @type {Number} The current cursor position */
|
||||
cursor: cursor,
|
||||
}
|
||||
|
||||
const quotedHeaders = []
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
if (!headers[i]) {
|
||||
continue
|
||||
}
|
||||
quotedHeaders.push(quoteCell(headers[i], { quote, separator: ',' }))
|
||||
}
|
||||
finalResult.header = quotedHeaders.join(',') // always quote headers and join with comma
|
||||
|
||||
// output is an array of arrays [[],[],[]]
|
||||
if (ouputArrays || headersOnly) {
|
||||
if (!firstRowIsHeader && !headersOnly && outputHeader && headers.length > 0) {
|
||||
if (output.length > 0) {
|
||||
output.unshift(headers)
|
||||
} else {
|
||||
output = [headers]
|
||||
}
|
||||
firstRowIsHeader = true
|
||||
}
|
||||
if (headersOnly) {
|
||||
delete finalResult.firstRowIsHeader
|
||||
return finalResult
|
||||
}
|
||||
finalResult.firstRowIsHeader = firstRowIsHeader
|
||||
finalResult.data = (firstRowIsHeader && !outputHeader) ? output.slice(1) : output
|
||||
return finalResult
|
||||
}
|
||||
|
||||
// output is an array of objects [{},{},{}]
|
||||
const outputObjects = []
|
||||
let i = firstRowIsHeader ? 1 : 0
|
||||
for (; i < output.length; i++) {
|
||||
const rowObject = {}
|
||||
let isEmpty = true
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
if (!headers[j]) {
|
||||
continue
|
||||
}
|
||||
let v = output[i][j] === undefined ? null : output[i][j]
|
||||
if (v === null && !includeNullValues) {
|
||||
continue
|
||||
} else if (v === "" && !includeEmptyStrings) {
|
||||
continue
|
||||
} else if (parseNumeric === true && v && !skipNumberConversion.test(v)) {
|
||||
const vTemp = +v
|
||||
const isNumber = !isNaN(vTemp)
|
||||
if(isNumber) {
|
||||
v = vTemp
|
||||
}
|
||||
}
|
||||
rowObject[headers[j]] = v
|
||||
isEmpty = false
|
||||
}
|
||||
// determine if this row is empty
|
||||
if (!isEmpty) {
|
||||
outputObjects.push(rowObject)
|
||||
}
|
||||
}
|
||||
finalResult.data = outputObjects
|
||||
delete finalResult.firstRowIsHeader
|
||||
return finalResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes a cell in a CSV string if necessary. Addiionally, any double quotes inside the cell will be escaped as per RFC 4180 2.6 (https://datatracker.ietf.org/doc/html/rfc4180#section-2).
|
||||
* @param {string} cell - the string to quote
|
||||
* @param {*} options - options
|
||||
* @param {string} [options.quote='"'] - the quote character
|
||||
* @param {string} [options.separator=','] - the separator character
|
||||
* @param {string[]} [options.quoteables] - an array of characters that, when encountered, will trigger the application of outer quotes
|
||||
* @returns
|
||||
*/
|
||||
function quoteCell(cell, { quote = '"', separator = ",", quoteables } = {
|
||||
quote: '"',
|
||||
separator: ",",
|
||||
quoteables: [quote, separator, '\r', '\n']
|
||||
}) {
|
||||
quoteables = quoteables || [quote, separator, '\r', '\n'];
|
||||
|
||||
let doubleUp = false;
|
||||
if (cell.indexOf(quote) !== -1) { // add double quotes if any quotes
|
||||
doubleUp = true;
|
||||
}
|
||||
const quoteChar = quoteables.some(q => cell.includes(q)) ? quote : '';
|
||||
return quoteChar + (doubleUp ? cell.replace(/"/g, '""') : cell) + quoteChar;
|
||||
}
|
||||
|
||||
// #region Custom Error Classes
|
||||
class ParseError extends Error {
|
||||
/**
|
||||
* @param {string} message - the error message
|
||||
* @param {number} cursor - the cursor index where the error occurred
|
||||
*/
|
||||
constructor(message, cursor) {
|
||||
super(message)
|
||||
this.name = 'ParseError'
|
||||
this.cursor = cursor
|
||||
}
|
||||
}
|
||||
|
||||
class UnquotedQuoteError extends ParseError {
|
||||
/**
|
||||
* @param {number} cursor - the cursor index where the error occurred
|
||||
*/
|
||||
constructor(cursor) {
|
||||
super('Quote found in the middle of an unquoted field', cursor)
|
||||
this.name = 'UnquotedQuoteError'
|
||||
}
|
||||
}
|
||||
|
||||
class DataAfterCloseError extends ParseError {
|
||||
/**
|
||||
* @param {number} cursor - the cursor index where the error occurred
|
||||
*/
|
||||
constructor(cursor) {
|
||||
super('Data found after closing quote', cursor)
|
||||
this.name = 'DataAfterCloseError'
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
exports.parse = parse
|
||||
exports.quoteCell = quoteCell
|
||||
exports.ParseError = ParseError
|
||||
exports.UnquotedQuoteError = UnquotedQuoteError
|
||||
exports.DataAfterCloseError = DataAfterCloseError
|
@@ -15,7 +15,11 @@
|
||||
-->
|
||||
|
||||
<script type="text/html" data-template-name="split">
|
||||
<div class="form-row"><span data-i18n="[html]split.intro"></span></div>
|
||||
<!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label>
|
||||
<input type="text" id="node-input-property" style="width:70%;"/>
|
||||
</div>
|
||||
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label>
|
||||
@@ -39,10 +43,9 @@
|
||||
<label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label>
|
||||
<input type="text" id="node-input-addname" style="width:70%">
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
@@ -57,7 +60,8 @@
|
||||
arraySplt: {value:1},
|
||||
arraySpltType: {value:"len"},
|
||||
stream: {value:false},
|
||||
addname: {value:""}
|
||||
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
|
||||
property: {value:"payload",required:true}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
@@ -69,6 +73,10 @@
|
||||
return this.name?"node_label_italic":"";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
if (this.property === undefined) {
|
||||
$("#node-input-property").val("payload");
|
||||
}
|
||||
$("#node-input-property").typedInput({default:'msg',types:['msg']});
|
||||
$("#node-input-splt").typedInput({
|
||||
default: 'str',
|
||||
typeField: $("#node-input-spltType"),
|
||||
@@ -208,7 +216,22 @@
|
||||
validate:RED.validators.typedInput("propertyType", false)
|
||||
},
|
||||
propertyType: { value:"msg"},
|
||||
key: {value:"topic"},
|
||||
key: {value:"topic", validate: (function () {
|
||||
const typeValidator = RED.validators.typedInput({ type: 'msg' })
|
||||
return function(v, opt) {
|
||||
const joinMode = $("#node-input-mode").val() || this.mode
|
||||
if (joinMode !== 'custom') {
|
||||
return true
|
||||
}
|
||||
const buildType = $("#node-input-build").val() || this.build
|
||||
if (buildType !== 'object') {
|
||||
return true
|
||||
} else {
|
||||
return typeValidator(v, opt)
|
||||
}
|
||||
}
|
||||
})()
|
||||
},
|
||||
joiner: { value:"\\n"},
|
||||
joinerType: { value:"str"},
|
||||
accumulate: { value:"false" },
|
||||
@@ -224,7 +247,12 @@
|
||||
outputs:1,
|
||||
icon: "join.svg",
|
||||
label: function() {
|
||||
return this.name||this._("join.join");
|
||||
var nam = this.name||this._("join.join");
|
||||
if (this.mode === "custom" && !isNaN(Number(this.count))) {
|
||||
nam += " "+this.count;
|
||||
if (this.accumulate === true) { nam+= "+"; }
|
||||
}
|
||||
return nam;
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name?"node_label_italic":"";
|
||||
|
@@ -19,13 +19,13 @@ module.exports = function(RED) {
|
||||
|
||||
function sendArray(node,msg,array,send) {
|
||||
for (var i = 0; i < array.length-1; i++) {
|
||||
msg.payload = array[i];
|
||||
RED.util.setMessageProperty(msg,node.property,array[i]);
|
||||
msg.parts.index = node.c++;
|
||||
if (node.stream !== true) { msg.parts.count = array.length; }
|
||||
send(RED.util.cloneMessage(msg));
|
||||
}
|
||||
if (node.stream !== true) {
|
||||
msg.payload = array[i];
|
||||
RED.util.setMessageProperty(msg,node.property,array[i]);
|
||||
msg.parts.index = node.c++;
|
||||
msg.parts.count = array.length;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -40,10 +40,12 @@ module.exports = function(RED) {
|
||||
node.stream = n.stream;
|
||||
node.spltType = n.spltType || "str";
|
||||
node.addname = n.addname || "";
|
||||
node.property = n.property||"payload";
|
||||
try {
|
||||
if (node.spltType === "str") {
|
||||
this.splt = (n.splt || "\\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t").replace(/\\e/g,"\e").replace(/\\f/g,"\f").replace(/\\0/g,"\0");
|
||||
} else if (node.spltType === "bin") {
|
||||
}
|
||||
else if (node.spltType === "bin") {
|
||||
var spltArray = JSON.parse(n.splt);
|
||||
if (Array.isArray(spltArray)) {
|
||||
this.splt = Buffer.from(spltArray);
|
||||
@@ -51,7 +53,8 @@ module.exports = function(RED) {
|
||||
throw new Error("not an array");
|
||||
}
|
||||
this.spltBuffer = spltArray;
|
||||
} else if (node.spltType === "len") {
|
||||
}
|
||||
else if (node.spltType === "len") {
|
||||
this.splt = parseInt(n.splt);
|
||||
if (isNaN(this.splt) || this.splt < 1) {
|
||||
throw new Error("invalid split length: "+n.splt);
|
||||
@@ -69,18 +72,22 @@ module.exports = function(RED) {
|
||||
node.buffer = Buffer.from([]);
|
||||
node.pendingDones = [];
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
var value = RED.util.getMessageProperty(msg,node.property);
|
||||
if (value !== undefined) {
|
||||
if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack
|
||||
else { msg.parts = {}; }
|
||||
msg.parts.id = RED.util.generateId(); // generate a random id
|
||||
if (node.property !== "payload") {
|
||||
msg.parts.property = node.property;
|
||||
}
|
||||
delete msg._msgid;
|
||||
if (typeof msg.payload === "string") { // Split String into array
|
||||
msg.payload = (node.remainder || "") + msg.payload;
|
||||
if (typeof value === "string") { // Split String into array
|
||||
value = (node.remainder || "") + value;
|
||||
msg.parts.type = "string";
|
||||
if (node.spltType === "len") {
|
||||
msg.parts.ch = "";
|
||||
msg.parts.len = node.splt;
|
||||
var count = msg.payload.length/node.splt;
|
||||
var count = value.length/node.splt;
|
||||
if (Math.floor(count) !== count) {
|
||||
count = Math.ceil(count);
|
||||
}
|
||||
@@ -89,9 +96,9 @@ module.exports = function(RED) {
|
||||
node.c = 0;
|
||||
}
|
||||
var pos = 0;
|
||||
var data = msg.payload;
|
||||
var data = value;
|
||||
for (var i=0; i<count-1; i++) {
|
||||
msg.payload = data.substring(pos,pos+node.splt);
|
||||
RED.util.setMessageProperty(msg,node.property,data.substring(pos,pos+node.splt));
|
||||
msg.parts.index = node.c++;
|
||||
pos += node.splt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -102,7 +109,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
node.remainder = data.substring(pos);
|
||||
if ((node.stream !== true) || (node.remainder.length === node.splt)) {
|
||||
msg.payload = node.remainder;
|
||||
RED.util.setMessageProperty(msg,node.property,node.remainder);
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
node.pendingDones.forEach(d => d());
|
||||
@@ -119,47 +126,48 @@ module.exports = function(RED) {
|
||||
if (!node.spltBufferString) {
|
||||
node.spltBufferString = node.splt.toString();
|
||||
}
|
||||
a = msg.payload.split(node.spltBufferString);
|
||||
a = value.split(node.spltBufferString);
|
||||
msg.parts.ch = node.spltBuffer; // pass the split char to other end for rejoin
|
||||
} else if (node.spltType === "str") {
|
||||
a = msg.payload.split(node.splt);
|
||||
a = value.split(node.splt);
|
||||
msg.parts.ch = node.splt; // pass the split char to other end for rejoin
|
||||
}
|
||||
sendArray(node,msg,a,send);
|
||||
done();
|
||||
}
|
||||
}
|
||||
else if (Array.isArray(msg.payload)) { // then split array into messages
|
||||
else if (Array.isArray(value)) { // then split array into messages
|
||||
msg.parts.type = "array";
|
||||
var count = msg.payload.length/node.arraySplt;
|
||||
var count = value.length/node.arraySplt;
|
||||
if (Math.floor(count) !== count) {
|
||||
count = Math.ceil(count);
|
||||
}
|
||||
msg.parts.count = count;
|
||||
var pos = 0;
|
||||
var data = msg.payload;
|
||||
var data = value;
|
||||
msg.parts.len = node.arraySplt;
|
||||
for (var i=0; i<count; i++) {
|
||||
msg.payload = data.slice(pos,pos+node.arraySplt);
|
||||
var m = data.slice(pos,pos+node.arraySplt);
|
||||
if (node.arraySplt === 1) {
|
||||
msg.payload = msg.payload[0];
|
||||
m = m[0];
|
||||
}
|
||||
RED.util.setMessageProperty(msg,node.property,m);
|
||||
msg.parts.index = i;
|
||||
pos += node.arraySplt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
}
|
||||
done();
|
||||
}
|
||||
else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) {
|
||||
else if ((typeof value === "object") && !Buffer.isBuffer(value)) {
|
||||
var j = 0;
|
||||
var l = Object.keys(msg.payload).length;
|
||||
var pay = msg.payload;
|
||||
var l = Object.keys(value).length;
|
||||
var pay = value;
|
||||
msg.parts.type = "object";
|
||||
for (var p in pay) {
|
||||
if (pay.hasOwnProperty(p)) {
|
||||
msg.payload = pay[p];
|
||||
RED.util.setMessageProperty(msg,node.property,pay[p]);
|
||||
if (node.addname !== "") {
|
||||
msg[node.addname] = p;
|
||||
RED.util.setMessageProperty(msg,node.addname,p);
|
||||
}
|
||||
msg.parts.key = p;
|
||||
msg.parts.index = j;
|
||||
@@ -170,9 +178,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
done();
|
||||
}
|
||||
else if (Buffer.isBuffer(msg.payload)) {
|
||||
var len = node.buffer.length + msg.payload.length;
|
||||
var buff = Buffer.concat([node.buffer, msg.payload], len);
|
||||
else if (Buffer.isBuffer(value)) {
|
||||
var len = node.buffer.length + value.length;
|
||||
var buff = Buffer.concat([node.buffer, value], len);
|
||||
msg.parts.type = "buffer";
|
||||
if (node.spltType === "len") {
|
||||
var count = buff.length/node.splt;
|
||||
@@ -186,7 +194,7 @@ module.exports = function(RED) {
|
||||
var pos = 0;
|
||||
msg.parts.len = node.splt;
|
||||
for (var i=0; i<count-1; i++) {
|
||||
msg.payload = buff.slice(pos,pos+node.splt);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(pos,pos+node.splt));
|
||||
msg.parts.index = node.c++;
|
||||
pos += node.splt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -197,7 +205,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
node.buffer = buff.slice(pos);
|
||||
if ((node.stream !== true) || (node.buffer.length === node.splt)) {
|
||||
msg.payload = node.buffer;
|
||||
RED.util.setMessageProperty(msg,node.property,node.buffer);
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
node.pendingDones.forEach(d => d());
|
||||
@@ -230,7 +238,7 @@ module.exports = function(RED) {
|
||||
var i = 0, p = 0;
|
||||
pos = buff.indexOf(node.splt);
|
||||
while (pos > -1) {
|
||||
msg.payload = buff.slice(p,pos);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(p,pos));
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
i++;
|
||||
@@ -242,7 +250,7 @@ module.exports = function(RED) {
|
||||
node.pendingDones = [];
|
||||
}
|
||||
if ((node.stream !== true) && (p < buff.length)) {
|
||||
msg.payload = buff.slice(p,buff.length);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(p,buff.length));
|
||||
msg.parts.index = node.c++;
|
||||
msg.parts.count = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -251,7 +259,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
else {
|
||||
node.buffer = buff.slice(p,buff.length);
|
||||
node.pendingDones.push(done);
|
||||
if (node.buffer.length > 0) {
|
||||
node.pendingDones.push(done);
|
||||
}
|
||||
}
|
||||
if (node.buffer.length == 0) {
|
||||
done();
|
||||
@@ -296,7 +306,6 @@ module.exports = function(RED) {
|
||||
return exp
|
||||
}
|
||||
|
||||
|
||||
function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) {
|
||||
var msgInfo = msgInfos.shift();
|
||||
exp.assign("I", msgInfo.msg.parts.index);
|
||||
@@ -328,6 +337,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reduceAndSendGroup(node, group, done) {
|
||||
var is_right = node.reduce_right;
|
||||
var flag = is_right ? -1 : 1;
|
||||
@@ -476,7 +486,7 @@ module.exports = function(RED) {
|
||||
var completeSend = function(partId) {
|
||||
var group = inflight[partId];
|
||||
if (group.timeout) { clearTimeout(group.timeout); }
|
||||
if ((node.accumulate !== true) || group.msg.hasOwnProperty("complete")) { delete inflight[partId]; }
|
||||
if (node.mode === 'auto' || node.accumulate !== true || group.msg.hasOwnProperty("complete")) { delete inflight[partId]; }
|
||||
if (group.type === 'array' && group.arrayLen > 1) {
|
||||
var newArray = [];
|
||||
group.payload.forEach(function(n) {
|
||||
@@ -513,13 +523,13 @@ module.exports = function(RED) {
|
||||
if (typeof group.joinChar !== 'string') {
|
||||
groupJoinChar = group.joinChar.toString();
|
||||
}
|
||||
RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar));
|
||||
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload.join(groupJoinChar));
|
||||
}
|
||||
else {
|
||||
if (node.propertyType === 'full') {
|
||||
group.msg = RED.util.cloneMessage(group.msg);
|
||||
}
|
||||
RED.util.setMessageProperty(group.msg,node.property,group.payload);
|
||||
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload);
|
||||
}
|
||||
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
|
||||
group.msg.parts = group.msg.parts.parts;
|
||||
@@ -587,7 +597,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
|
||||
if (node.mode === 'auto' && (!msg.hasOwnProperty("parts")||!msg.parts.hasOwnProperty("id"))) {
|
||||
// if a blank reset messag erest it all.
|
||||
// if a blank reset message reset it all.
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) {
|
||||
clearTimeout(inflight[partId].timeout);
|
||||
@@ -601,6 +611,15 @@ module.exports = function(RED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.mode === 'custom' && msg.hasOwnProperty('parts')) {
|
||||
if (msg.parts.hasOwnProperty('parts')) {
|
||||
msg.parts = { parts: msg.parts.parts };
|
||||
}
|
||||
else {
|
||||
delete msg.parts;
|
||||
}
|
||||
}
|
||||
|
||||
var payloadType;
|
||||
var propertyKey;
|
||||
var targetCount;
|
||||
@@ -616,6 +635,7 @@ module.exports = function(RED) {
|
||||
propertyKey = msg.parts.key;
|
||||
arrayLen = msg.parts.len;
|
||||
propertyIndex = msg.parts.index;
|
||||
property = RED.util.getMessageProperty(msg,msg.parts.property||"payload");
|
||||
}
|
||||
else if (node.mode === 'reduce') {
|
||||
return processReduceMessageQueue({msg, send, done});
|
||||
@@ -627,6 +647,9 @@ module.exports = function(RED) {
|
||||
joinChar = node.joiner;
|
||||
if (n.count === "" && msg.hasOwnProperty('parts')) {
|
||||
targetCount = msg.parts.count || 0;
|
||||
if (msg.parts.hasOwnProperty('id')) {
|
||||
partId = msg.parts.id;
|
||||
}
|
||||
}
|
||||
if (node.build === 'object') {
|
||||
propertyKey = RED.util.getMessageProperty(msg,node.key);
|
||||
@@ -714,6 +737,8 @@ module.exports = function(RED) {
|
||||
completeSend(partId)
|
||||
}, node.timer)
|
||||
}
|
||||
if (node.mode === "auto") { inflight[partId].prop = msg.parts.property; }
|
||||
else { inflight[partId].prop = node.property; }
|
||||
}
|
||||
inflight[partId].dones.push(done);
|
||||
|
||||
|
@@ -107,7 +107,14 @@
|
||||
outputs:1,
|
||||
icon: "batch.svg",
|
||||
label: function() {
|
||||
return this.name||this._("batch.batch");;
|
||||
var nam = this.name||this._("batch.batch");
|
||||
if (this.mode === "count" && !isNaN(Number(this.count))) {
|
||||
nam += " "+this.count;
|
||||
}
|
||||
if (this.mode === "interval" && !isNaN(Number(this.interval))) {
|
||||
nam += " "+this.interval+"s";
|
||||
}
|
||||
return nam;
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
|
4
packages/node_modules/@node-red/nodes/core/storage/10-file.html
vendored
Executable file → Normal file
@@ -198,7 +198,7 @@
|
||||
category: 'storage',
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
filename: {value:""},
|
||||
filename: {value:"", validate: RED.validators.typedInput({ typeField: 'filenameType' })},
|
||||
filenameType: {value:"str"},
|
||||
appendNewline: {value:true},
|
||||
createDir: {value:false},
|
||||
@@ -297,7 +297,7 @@
|
||||
category: 'storage',
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
filename: {value:""},
|
||||
filename: {value:"", validate: RED.validators.typedInput({ typeField: 'filenameType' }) },
|
||||
filenameType: {value:"str"},
|
||||
format: {value:"utf8"},
|
||||
chunk: {value:false},
|
||||
|
@@ -68,9 +68,15 @@ module.exports = function(RED) {
|
||||
node.error(err,msg);
|
||||
return done();
|
||||
} else {
|
||||
filename = value;
|
||||
if (typeof value !== 'string' && value !== null && value !== undefined) {
|
||||
value = value.toString();
|
||||
}
|
||||
processMsg2(msg,nodeSend,value,done);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processMsg2(msg,nodeSend,filename,done) {
|
||||
filename = filename || "";
|
||||
msg.filename = filename;
|
||||
var fullFilename = filename;
|
||||
@@ -117,7 +123,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
if (typeof data === "boolean") { data = data.toString(); }
|
||||
if (typeof data === "number") { data = data.toString(); }
|
||||
if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; }
|
||||
var aflg = true;
|
||||
if (msg.hasOwnProperty("parts") && msg.parts.type === "string" && (msg.parts.count === msg.parts.index + 1)) { aflg = false; }
|
||||
if ((node.appendNewline) && (!Buffer.isBuffer(data)) && aflg) { data += os.EOL; }
|
||||
var buf;
|
||||
if (node.encoding === "setbymsg") {
|
||||
buf = encode(data, msg.encoding || "none");
|
||||
@@ -273,7 +281,6 @@ module.exports = function(RED) {
|
||||
}
|
||||
RED.nodes.registerType("file",FileNode);
|
||||
|
||||
|
||||
function FileInNode(n) {
|
||||
// Read a file
|
||||
RED.nodes.createNode(this,n);
|
||||
@@ -309,12 +316,17 @@ module.exports = function(RED) {
|
||||
node.error(err,msg);
|
||||
return done();
|
||||
} else {
|
||||
filename = (value || "").replace(/\t|\r|\n/g,'');
|
||||
if (typeof value !== 'string' && value !== null && value !== undefined) {
|
||||
value = value.toString();
|
||||
}
|
||||
processMsg2(msg, nodeSend, (value || "").replace(/\t|\r|\n/g,''), nodeDone);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function processMsg2(msg, nodeSend, filename, nodeDone) {
|
||||
filename = filename || "";
|
||||
var fullFilename = filename;
|
||||
var filePath = "";
|
||||
if (filename && RED.settings.fileWorkingDirectory && !path.isAbsolute(filename)) {
|
||||
fullFilename = path.resolve(path.join(RED.settings.fileWorkingDirectory,filename));
|
||||
}
|
||||
@@ -433,7 +445,8 @@ module.exports = function(RED) {
|
||||
nodeDone();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.on('close', function() {
|
||||
node.status({});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 603 B |
1
packages/node_modules/@node-red/nodes/icons/arduino.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#fff"><path d="m9.7884 22.379c-5.2427-0.41732-9.6475 5.7885-7.4975 10.585 2.0949 5.2041 9.9782 6.6154 13.727 2.4477 3.633-3.5613 5.0332-9.0411 9.4821-11.853 4.5205-3.0872 11.797-0.172 12.68 5.3144 0.86 5.2537-4.8017 10.364-9.9231 8.8205-3.7873-0.85449-6.5051-4.0905-8.0487-7.4975-1.9019-3.2526-4.3882-6.7257-8.2693-7.6077-0.6891-0.15656-1.4003-0.21831-2.1059-0.21721z" stroke-width="3.3"/><path d="m6.7012 29.821h6.6154" stroke-width="1.4"/><path d="m26.988 29.821h5.5128m-2.8115-2.7564v5.5128" stroke-width="1.8"/></g></svg>
|
After Width: | Height: | Size: 635 B |
Before Width: | Height: | Size: 2.3 KiB |
1
packages/node_modules/@node-red/nodes/icons/bluetooth.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m8.3474 17.75 22.298 22.444-10.747 13.013v-46.497l10.747 12.428-22.298 21.859" fill="none" stroke="#fff" stroke-width="4"/></svg>
|
After Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 1.6 KiB |
1
packages/node_modules/@node-red/nodes/icons/leveldb.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m2.7078 12.986c0 7.7994-0.36386 21.569 0 32.545s35.118 9.8751 34.848 0c-0.26959-9.8751 0-24.82 0-32.545 0-7.7243-34.848-7.7995-34.848 0z" fill="none" stroke="#fff"/><g fill="#fff"><path d="m3.8741 13.406v8.955c0.021834 3.5781 19.543 5.0789 25.575 3.2543 0 0 0.02229-2.6683 0.02998-2.6673l5.5325 0.7238c0.64508 0.0844 1.1345-0.74597 1.134-1.3284v-8.573l-0.99896 0.93349-15.217-2.2765c4.5883 2.1798 9.808 4.1312 9.808 4.1312-9.3667 3.1562-25.846-0.31965-25.864-3.1525z"/><path d="m3.886 26.607v8.1052c3.2188 6.1087 29.901 5.8574 32.272 0v-8.1052c-3.3598 4.6685-29.204 5.1534-32.272 0z"/><path d="m4.0032 39.082v7.1522c2.556 7.4622 28.918 7.6072 32.272 0v-7.1522c-3.2345 4.9471-29.087 5.359-32.272 0z"/></g></svg>
|
After Width: | Height: | Size: 806 B |
Before Width: | Height: | Size: 414 B |
1
packages/node_modules/@node-red/nodes/icons/mongodb.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m23.515 13.831c-4.7594-5.8789-2.6084-5.7751-7.3474 0-8.0368 10.477-8.3322 24.431 2.5476 32.935 0.13181 2.0418 0.46056 4.9803 0.46056 4.9803h1.315s0.32875-2.9219 0.46017-4.9803c2.8458-2.2339 16.799-14.619 2.5641-32.935z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 671 B |
1
packages/node_modules/@node-red/nodes/icons/mouse.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#fff" stroke-width="3"><path d="m6 30c6 5 24 4 29-0.07"/><path d="m21 33 0.1-19c0.02-4 4-3 4-6s-4-2-4-5"/><path d="m6 22c0-11 29-10 29 0v21c0 18-29 19-29 0s4e-7 -11 0-21z"/></g></svg>
|
After Width: | Height: | Size: 293 B |
BIN
packages/node_modules/@node-red/nodes/icons/rbe.png
vendored
Before Width: | Height: | Size: 252 B |
1
packages/node_modules/@node-red/nodes/icons/rbe.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><path d="m29 12s0.1 30 0.05 31-3 5-7 5-19 0.04-19 0.04c6-4 9-5 17-5 0 0 4-0.1 4-2 0-2 8e-3 -29 8e-3 -29z" fill="#fff"/><path d="m12 47s-0.1-30-0.05-31 3-5 7-5 19-0.04 19-0.04c-6 4-9 5-17 5 0 0-4 0.1-4 2 0 2-8e-3 29-8e-3 29z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 736 B |
1
packages/node_modules/@node-red/nodes/icons/redis.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="40" height="60" viewBox="0, 0, 40, 60" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#fff" stroke-width="3"><path class="cls-4" d="m17.639 30.221c-1.7087-0.88225-12.465-5.6284-14.414-6.636-1.9492-1.0075-1.9868-1.7073-0.075164-2.5188 1.9117-0.81145 12.643-5.3861 14.91-6.2738 2.2675-0.8877 3.0517-0.91493 4.9785-0.14704 1.9267 0.76789 12.026 5.1329 13.923 5.8898 1.8966 0.75699 1.9843 1.386 0.02631 2.4861-1.958 1.1001-12.1 5.6611-14.285 6.8729s-3.355 1.2091-5.0636 0.32685z"/><path class="cls-4" d="m32.23 25.251c2.8239 1.2039 4.155 1.764 4.7307 1.9938 1.8966 0.75699 1.9843 1.386 0.0263 2.4861s-12.1 5.6611-14.285 6.8729c-2.1848 1.2117-3.3548 1.209-5.0634 0.32676-1.7087-0.88225-12.465-5.6284-14.414-6.636-1.9492-1.0075-1.9868-1.7073-0.075164-2.5188 10.883-4.6196-9.1087 3.8612 4.9598-2.1076"/><path class="cls-4" d="m32.23 31.961c2.8239 1.2039 4.155 1.764 4.7307 1.9938 1.8966 0.75699 1.9843 1.386 0.0263 2.4861s-12.1 5.6611-14.285 6.8729c-2.1848 1.2117-3.3548 1.209-5.0634 0.32676-1.7087-0.88225-12.465-5.6284-14.414-6.636-1.9492-1.0075-1.9868-1.7073-0.075164-2.5188 10.883-4.6196-9.1087 3.8612 4.9598-2.1076"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
0
packages/node_modules/@node-red/nodes/locales/de/common/20-inject.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/common/21-debug.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/common/25-catch.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/common/25-status.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/common/60-link.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/common/90-comment.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/common/98-unknown.html
vendored
Executable file → Normal file
2
packages/node_modules/@node-red/nodes/locales/de/function/10-function.html
vendored
Executable file → Normal file
@@ -25,7 +25,7 @@
|
||||
<p>Wenn ein promise-Objekt aus dem Start-Code zurückgegeben wird,
|
||||
beginnt danach die reguläre Verarbeitung der Eingangsnachrichten.</p>
|
||||
<h3>Details</h3>
|
||||
<p>Siehe <a target="_blank" href="http://nodered.org/docs/writing-functions.html">Onlinedokumentation</a>
|
||||
<p>Siehe <a target="_blank" href="https://nodered.org/docs/writing-functions.html">Onlinedokumentation</a>
|
||||
für weitere Informationen zum Schreiben von Funktionen.</p>
|
||||
<h4><b>Nachrichten senden</b></h4>
|
||||
<p>Die Funktion kann die Nachrichten zurückgeben, die sie an die nächsten Nodes im Flow weitergeben möchte,
|
||||
|
0
packages/node_modules/@node-red/nodes/locales/de/function/10-switch.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/function/15-change.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/function/16-range.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/function/80-template.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/function/89-delay.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/function/89-trigger.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/function/90-exec.html
vendored
Executable file → Normal file
9
packages/node_modules/@node-red/nodes/locales/de/messages.json
vendored
Executable file → Normal file
@@ -98,6 +98,7 @@
|
||||
},
|
||||
"scope": {
|
||||
"all": "allen Nodes",
|
||||
"group": "in gleicher Gruppe",
|
||||
"selected": "ausgewählten Nodes"
|
||||
}
|
||||
},
|
||||
@@ -110,6 +111,7 @@
|
||||
},
|
||||
"scope": {
|
||||
"all": "allen Nodes",
|
||||
"group": "in gleicher Gruppe",
|
||||
"selected": "ausgewählten Nodes"
|
||||
}
|
||||
},
|
||||
@@ -214,7 +216,8 @@
|
||||
"initialize": "Start",
|
||||
"finalize": "Stopp",
|
||||
"outputs": "Ausgänge",
|
||||
"modules": "Module"
|
||||
"modules": "Module",
|
||||
"timeout": "Timeout"
|
||||
},
|
||||
"text": {
|
||||
"initialize": "// Der Code hier wird ausgeführt,\n// wenn der Node gestartet wird\n",
|
||||
@@ -362,6 +365,7 @@
|
||||
"port": "Port",
|
||||
"keepalive": "Keep-Alive",
|
||||
"cleansession": "Bereinigte Sitzung (clean session) verwenden",
|
||||
"autoUnsubscribe": "Abonnement bei Verbindungsende automatisch beenden",
|
||||
"cleanstart": "Verwende bereinigten Start",
|
||||
"use-tls": "TLS",
|
||||
"tls-config": "TLS-Konfiguration",
|
||||
@@ -512,7 +516,8 @@
|
||||
"path1": "Standardmäßig enthält <code>payload</code> die Daten, die über einen WebSocket gesendet oder von einem WebSocket empfangen werden. Der Empfänger (Listener) kann so konfiguriert werden, dass er das gesamte Nachrichtenobjekt als eine JSON-formatierte Zeichenfolge (string) sendet oder empfängt.",
|
||||
"path2": "Dieser Pfad ist relativ zu <code>__path__</code>.",
|
||||
"url1": "URL sollte ws:// oder wss:// Schema verwenden und auf einen vorhandenen WebSocket-Listener verweisen.",
|
||||
"url2": "Standardmäßig enthält <code>payload</code> die Daten, die über einen WebSocket gesendet oder von einem WebSocket empfangen werden. Der Client kann so konfiguriert werden, dass er das gesamte Nachrichtenobjekt als eine JSON-formatierte Zeichenfolge (string) sendet oder empfängt."
|
||||
"url2": "Standardmäßig enthält <code>payload</code> die Daten, die über einen WebSocket gesendet oder von einem WebSocket empfangen werden. Der Client kann so konfiguriert werden, dass er das gesamte Nachrichtenobjekt als eine JSON-formatierte Zeichenfolge (string) sendet oder empfängt.",
|
||||
"headers": "Header werden nur während des Protokollaktualisierungsmechanismus übermittelt, von HTTP auf das WS/WSS-Protokoll."
|
||||
},
|
||||
"status": {
|
||||
"connected": "Verbunden __count__",
|
||||
|
0
packages/node_modules/@node-red/nodes/locales/de/network/05-tls.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/network/06-httpproxy.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/network/10-mqtt.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/network/21-httpin.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/network/22-websocket.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/network/31-tcpin.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/network/32-udp.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/parsers/70-CSV.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/parsers/70-HTML.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/parsers/70-JSON.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/parsers/70-XML.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/parsers/70-YAML.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/sequence/17-split.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/sequence/18-sort.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/sequence/19-batch.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/storage/10-file.html
vendored
Executable file → Normal file
0
packages/node_modules/@node-red/nodes/locales/de/storage/23-watch.html
vendored
Executable file → Normal file
@@ -36,5 +36,5 @@ greater than one day you should consider using a scheduler node that can cope wi
|
||||
<p><b>Note</b>: The <i>"Interval between times"</i> and <i>"at a specific time"</i> options use the standard cron system.
|
||||
This means that 20 minutes will be at the next hour, 20 minutes past and 40 minutes past - not in 20 minutes time.
|
||||
If you want every 20 minutes from now - use the <i>"interval"</i> option.</p>
|
||||
<p><b>Note</b>: To include a newline in a string you must use a Function node to create the payload.</p>
|
||||
<p><b>Note</b>: To include a newline in a string you must use the Function or Template node to create the payload.</p>
|
||||
</script>
|
||||
|