Merge branch 'dev' into master

This commit is contained in:
Stephen McLaughlin
2022-05-03 09:35:32 +01:00
committed by GitHub
267 changed files with 29180 additions and 21709 deletions

View File

@@ -0,0 +1,5 @@
<!--
05-junction.html
This file exists so that the runtime loads the Junction node into the registry,
but it is empty so it doesn't appear in the editor palette
-->

View File

@@ -0,0 +1,12 @@
module.exports = function(RED) {
"use strict";
function JunctionNode(n) {
RED.nodes.createNode(this,n);
this.on("input",function(msg, send, done) {
send(msg);
done();
});
}
RED.nodes.registerType("junction",JunctionNode);
}

View File

@@ -225,28 +225,47 @@
color:"#a6bbcf",
defaults: {
name: {value:""},
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v) {
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) {
if (!v || v.length === 0) { return true }
for (var i=0;i<v.length;i++) {
if (/msg|flow|global/.test(v[i].vt)) {
if (!RED.utils.validatePropertyExpression(v[i].v)) {
return false;
return 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 false;}
try{ jsonata(v[i].v); }
catch(e){
return 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 false;}
try{ JSON.parse(v[i].v); }
catch(e){
return 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 });
}
}
}
return true;
}
},
repeat: {value:"", validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 2147483))) }},
repeat: {
value:"", validate: function(v, opt) {
if ((v === "") ||
(RED.validators.number(v) &&
(v >= 0) && (v <= 2147483))) {
return true;
}
return RED._("node-red:inject.errors.invalid-repeat");
}
},
crontab: {value:""},
once: {value:false},
onceDelay: {value:0.1},
topic: {value:""},
payload: {value:"", validate: RED.validators.typedInput("payloadType")},
payload: {value:"", validate: RED.validators.typedInput("payloadType", false) },
payloadType: {value:"date"},
},
icon: "inject.svg",
@@ -370,13 +389,6 @@
var id = $("#inject-time-type-select").val();
$(".inject-time-row").hide();
$("#inject-time-row-"+id).show();
if ((id == "none") || (id == "interval") || (id == "interval-time")) {
$("#node-once").show();
}
else {
$("#node-once").hide();
$("#node-input-once").prop('checked', false);
}
// Scroll down
var scrollDiv = $("#dialog-form").parent();

View File

@@ -74,7 +74,7 @@
RED.nodes.registerType('debug',{
category: 'common',
defaults: {
name: {value:""},
name: {value:"_DEFAULT_"},
active: {value:true},
tosidebar: {value:true},
console: {value:false},
@@ -160,6 +160,10 @@
},
messageSourceClick: function(sourceId, aliasId, path) {
// Get all of the nodes that could have logged this message
if (RED.nodes.workspace(sourceId)) {
RED.view.reveal(sourceId);
return
}
var candidateNodes = [RED.nodes.node(sourceId)]
if (path) {
for (var i=2;i<path.length;i++) {
@@ -235,10 +239,11 @@
// sourceNode should be the top-level node - one that is on a flow.
var sourceNode;
var pathParts;
var pathHierarchy;
if (o.path) {
// Path is a `/`-separated list of ids that identifies the
// complete parentage of the node that generated this message.
// flow-id/subflow-A-instance/subflow-A-type/subflow-B-instance/subflow-B-type/node-id
// flow-id/subflow-A-instance/subflow-B-instance
// If it has one id, that is a top level flow
// each subsequent id is the instance id of a subflow node
@@ -251,11 +256,41 @@
// Highlight the subflow instance node.
sourceNode = RED.nodes.node(pathParts[1]);
}
pathHierarchy = pathParts.map((id,index) => {
if (index === 0) {
return {
id: id,
label: RED.nodes.workspace(id).label
}
} else {
var instanceNode = RED.nodes.node(id)
return {
id: id,
label: (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8)).name)
}
}
})
if (pathParts.length === 1) {
pathHierarchy.push({
id: o.id,
label: sourceNode.name || sourceNode.type+":"+sourceNode.id
})
}
if (o._alias) {
let aliasNode = RED.nodes.node(o._alias)
if (aliasNode) {
pathHierarchy.push({
id: o._alias,
label: aliasNode.name || aliasNode.type+":"+aliasNode.id
})
}
}
} else {
// This is probably redundant...
sourceNode = RED.nodes.node(o.id) || RED.nodes.node(o.z);
}
if (sourceNode) {
var sourceFlow = RED.nodes.workspace(sourceNode.z)
o._source = {
id:sourceNode.id,
z:sourceNode.z,
@@ -266,7 +301,9 @@
// the top-level subflow instance node.
// This means the node's name is displayed in the sidebar.
_alias:o._alias,
path: pathParts
flowName: sourceFlow?(sourceFlow.label||sourceNode.z):sourceNode.z,
path: pathParts,
pathHierarchy: pathHierarchy
};
}
RED.debug.handleDebugMessage(o);
@@ -416,9 +453,16 @@
label: RED._("node-red:debug.autostatus"),
hasValue: false
};
var counter = {
value: "counter",
label: RED._("node-red:debug.messageCount"),
hasValue: false
};
$("#node-input-typed-status").typedInput({
default: "auto",
types:[autoType, "msg", "jsonata"],
types:[autoType, "msg", "jsonata", counter],
typeField: $("#node-input-statusType")
});
var that = this;
@@ -455,7 +499,7 @@
types:['msg', fullType, "jsonata"],
typeField: $("#node-input-targetType")
});
if (this.targetType === "jsonata") {
if (this.targetType === "jsonata") {
var property = this.complete || "";
$("#node-input-typed-complete").typedInput('type','jsonata');
$("#node-input-typed-complete").typedInput('value',property);
@@ -477,12 +521,16 @@
$("#node-input-tostatus").on('change',function() {
if ($(this).is(":checked")) {
if (!that.hasOwnProperty("statusVal") || that.statusVal === "") {
if (that.statusType === "counter") {
that.statusVal = "";
}
else if (!that.hasOwnProperty("statusVal") || that.statusVal === "") {
var type = $("#node-input-typed-complete").typedInput('type');
var comp = "payload";
if (type !== 'full') {
comp = $("#node-input-typed-complete").typedInput('value');
}
console.log('hihi')
that.statusType = "auto";
that.statusVal = comp;
}
@@ -507,6 +555,12 @@
$("#node-input-complete").val($("#node-input-typed-complete").typedInput('value'));
}
$("#node-input-statusVal").val($("#node-input-typed-status").typedInput('value'));
},
onadd: function() {
if (this.name === '_DEFAULT_') {
this.name = ''
RED.actions.invoke("core:generate-node-names", this)
}
}
});
})();

View File

@@ -21,6 +21,9 @@ module.exports = function(RED) {
this.statusType = n.statusType || "auto";
this.statusVal = n.statusVal || this.complete;
this.tosidebar = n.tosidebar;
this.counter = 0;
this.lastTime = new Date().getTime();
this.timeout = null;
if (this.tosidebar === undefined) { this.tosidebar = true; }
this.active = (n.active === null || typeof n.active === "undefined") || n.active;
if (this.tostatus) {
@@ -32,6 +35,12 @@ module.exports = function(RED) {
var statExpression = hasStatExpression ? n.statusVal : null;
var node = this;
if ( node.statusType === "counter" ){
node.status({fill:"blue", shape:"ring", text: node.counter});
}
else {
node.status({fill:"", shape:"", text: ""});
}
var preparedEditExpression = null;
var preparedStatExpression = null;
if (editExpression) {
@@ -106,46 +115,64 @@ module.exports = function(RED) {
if (this.oldState) {
this.status({});
}
if (this.timeout) {
clearTimeout(this.timeout)
}
})
this.on("input", function(msg, send, done) {
if (hasOwnProperty.call(msg, "status") && msg.status &&
hasOwnProperty.call(msg.status, "source") && msg.status.source &&
hasOwnProperty.call(msg.status.source, "id") && (msg.status.source.id === node.id)) {
if (hasOwnProperty.call(msg, "status") && hasOwnProperty.call(msg.status, "source") && hasOwnProperty.call(msg.status.source, "id") && (msg.status.source.id === node.id)) {
done();
return;
}
if (node.tostatus === true) {
prepareStatus(msg, function(err,debugMsg) {
if (err) { node.error(err); return; }
var output = debugMsg.msg;
var st = (typeof output === 'string') ? output : util.inspect(output);
var fill = "grey";
var shape = "dot";
if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) {
fill = output.fill;
shape = output.shape;
st = output.text;
if ( node.statusType === "counter" ){
const differenceOfTime = (new Date().getTime() - node.lastTime);
node.lastTime = new Date().getTime();
node.counter++;
if ( differenceOfTime > 100 ){
node.status({fill:"blue", shape:"ring", text: node.counter});
}
if (node.statusType === "auto") {
if (hasOwnProperty.call(msg, "error")) {
fill = "red";
st = msg.error.message;
}
if (hasOwnProperty.call(msg, "status") &&
msg.status) {
fill = msg.status.fill || "grey";
shape = msg.status.shape || "ring";
st = msg.status.text || "";
else {
if (node.timeout) {
clearTimeout(node.timeout)
}
node.timeout = setTimeout(() => {
node.status({fill:"blue", shape:"ring", text: node.counter})
}, 200)
}
} else {
prepareStatus(msg, function(err,debugMsg) {
if (err) { node.error(err); return; }
var output = debugMsg.msg;
var st = (typeof output === 'string') ? output : util.inspect(output);
var fill = "grey";
var shape = "dot";
if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) {
fill = output.fill;
shape = output.shape;
st = output.text;
}
if (node.statusType === "auto") {
if (hasOwnProperty.call(msg, "error")) {
fill = "red";
st = msg.error.message;
}
if (hasOwnProperty.call(msg, "status")) {
fill = msg.status.fill || "grey";
shape = msg.status.shape || "ring";
st = msg.status.text || "";
}
}
if (st.length > 32) { st = st.substr(0,32) + "..."; }
var newStatus = {fill:fill, shape:shape, text:st};
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to
node.status(newStatus);
node.oldState = JSON.stringify(newStatus);
}
});
if (st.length > 32) { st = st.substr(0,32) + "..."; }
var newStatus = {fill:fill, shape:shape, text:st};
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to
node.status(newStatus);
node.oldState = JSON.stringify(newStatus);
}
});
}
}
if (this.complete === "true") {
@@ -288,7 +315,7 @@ module.exports = function(RED) {
res.sendFile(
req.params[0],
options,
err => {
err => {
if (err) {
res.sendStatus(404);
}

View File

@@ -32,14 +32,23 @@
<label for="node-input-timeout"><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 style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
<div class="form-row node-input-link-row"></div>
<div class="form-row">
<label for="node-input-linkType" data-i18n="link.linkCallType"></label>
<select id="node-input-linkType" style="width: 70%">
<option value="static" data-i18n="link.staticLinkCall"></option>
<option value="dynamic" data-i18n="link.dynamicLinkCall"></option>
</select>
</div>
<div class="link-call-target-tree" style="position:relative; height: 30px; text-align: right;">
<div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div>
</div>
<div class="form-row node-input-link-row link-call-target-tree"></div>
</script>
<script type="text/javascript">
(function() {
var treeList;
let treeList;
function onEditPrepare(node,targetType) {
if (!node.links) {
@@ -47,7 +56,7 @@
}
node.oldLinks = [];
var activeSubflow = RED.nodes.subflow(node.z);
const activeSubflow = RED.nodes.subflow(node.z);
treeList = $("<div>")
.css({width: "100%", height: "100%"})
@@ -67,10 +76,10 @@
RED.view.redraw();
}
});
var candidateNodes = RED.nodes.filterNodes({type:targetType});
var candidateNodesCount = 0;
const candidateNodes = RED.nodes.filterNodes({type:targetType});
let candidateNodesCount = 0;
var search = $("#node-input-link-target-filter").searchBox({
const search = $("#node-input-link-target-filter").searchBox({
style: "compact",
delay: 300,
change: function() {
@@ -79,7 +88,7 @@
treeList.treeList("filter", null);
search.searchBox("count","");
} else {
var count = treeList.treeList("filter", function(item) {
const count = treeList.treeList("filter", function(item) {
return item.label.toLowerCase().indexOf(val) > -1 || (item.node && item.node.type.toLowerCase().indexOf(val) > -1)
});
search.searchBox("count",count+" / "+candidateNodesCount);
@@ -87,25 +96,27 @@
}
});
var flows = [];
var flowMap = {};
const flows = [];
const flowMap = {};
if (activeSubflow) {
flowMap[activeSubflow.id] = {
id: activeSubflow.id,
class: 'red-ui-palette-header',
label: "Subflow : "+(activeSubflow.name || activeSubflow.id),
label: "Subflow : " + (activeSubflow.name || activeSubflow.id),
expanded: true,
children: []
};
flows.push(flowMap[activeSubflow.id])
} else {
RED.nodes.eachWorkspace(function(ws) {
}
if (!activeSubflow || node.type === "link call") {
// Only "Link Call" can look outside of its own subflow
// Link In and Link Out nodes outside of a subflow should be ignored
RED.nodes.eachWorkspace(function (ws) {
flowMap[ws.id] = {
id: ws.id,
class: 'red-ui-palette-header',
label: (ws.label || ws.id)+(node.z===ws.id ? " *":""),
label: (ws.label || ws.id) + (node.z === ws.id ? " *" : ""),
expanded: true,
children: []
}
@@ -113,22 +124,21 @@
})
}
candidateNodes.forEach(function(n) {
candidateNodes.forEach(function (n) {
if (flowMap[n.z]) {
if (targetType === "link out" && n.mode === 'return') {
// Link In nodes looking for Link Out nodes should not
// include return-mode nodes.
return
return;
}
var isChecked = false;
isChecked = (node.links.indexOf(n.id) !== -1) || (n.links||[]).indexOf(node.id) !== -1;
const isChecked = (node.links.indexOf(n.id) !== -1) || (n.links || []).indexOf(node.id) !== -1;
if (isChecked) {
node.oldLinks.push(n.id);
}
flowMap[n.z].children.push({
id: n.id,
node: n,
label: n.name||n.id,
label: n.name || n.id,
selected: isChecked,
checkbox: node.type !== "link call",
radio: node.type === "link call"
@@ -136,8 +146,8 @@
candidateNodesCount++;
}
});
flows = flows.filter(function(f) { return f.children.length > 0 })
treeList.treeList('data',flows);
const flowsFiltered = flows.filter(function(f) { return f.children.length > 0 })
treeList.treeList('data',flowsFiltered);
setTimeout(function() {
treeList.treeList('show',node.z);
},100);
@@ -209,6 +219,10 @@
}
function onAdd() {
if (this.name === '_DEFAULT_') {
this.name = ''
RED.actions.invoke("core:generate-node-names", this)
}
for (var i=0;i<this.links.length;i++) {
var n = RED.nodes.node(this.links[i]);
if (n && n.links.indexOf(this.id) === -1) {
@@ -221,7 +235,7 @@
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
name: { value: "_DEFAULT_" },
links: { value: [], type:"link out[]" }
},
inputs:0,
@@ -257,9 +271,14 @@
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
links: { value: [], type:"link in[]"},
timeout: { value: "30", validate:RED.validators.number(true) }
name: { value: "" },
links: { value: [], type:"link in[]" },
linkType: { value:"static" },
timeout: {
value: "30",
label: RED._("node-red:link.timeout"),
validate:RED.validators.number(true)
}
},
inputs: 1,
outputs: 1,
@@ -271,7 +290,9 @@
if (this.name) {
return this.name;
}
if (this.links.length > 0) {
if (this.linkType === "dynamic") {
return this._("link.dynamicLinkLabel");
} else if (this.links.length > 0) {
var targetNode = RED.nodes.node(this.links[0]);
return targetNode && (targetNode.name || this._("link.linkCall"));
}
@@ -281,6 +302,22 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
console.log("link call oneditprepare")
const updateVisibility = function() {
const static = $('#node-input-linkType').val() !== "dynamic";
if(static) {
$("div.link-call-target-tree").show();
} else {
$("div.link-call-target-tree").hide();
}
}
$("#node-input-linkType").on("change",function(d){
updateVisibility();
});
if (["static","dynamic"].indexOf(this.linkType) < 0) {
$("#node-input-linkType").val('static');
}
updateVisibility();
onEditPrepare(this,"link in");
},
oneditsave: function() {
@@ -293,9 +330,9 @@
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
name: { value:"_DEFAULT_" },
mode: { value: "link" },// link || return
links: { value: [], type:"link in[]"}
links: { value: [], type:"link in[]" }
},
align:"right",
inputs:1,

View File

@@ -14,10 +14,119 @@
* limitations under the License.
**/
/**
* @typedef LinkTarget
* @type {object}
* @property {string} id - ID of the target node.
* @property {string} name - Name of target Node
* @property {number} flowId - ID of flow where the target node exists
* @property {string} flowName - Name of flow where the target node exists
* @property {boolean} isSubFlow - True if the link-in node exists in a subflow instance
*/
module.exports = function(RED) {
"use strict";
const crypto = require("crypto");
const targetCache = (function () {
const registry = { id: {}, name: {} };
function getIndex(/** @type {[LinkTarget]}*/ targets, id) {
for (let index = 0; index < (targets || []).length; index++) {
const element = targets[index];
if (element.id === id) {
return index;
}
}
return -1;
}
/**
* Generate a target object from a node
* @param {LinkInNode} node
* @returns {LinkTarget} a link target object
*/
function generateTarget(node) {
const isSubFlow = node._flow.TYPE === "subflow";
return {
id: node.id,
name: node.name || node.id,
flowId: node._flow.flow.id,
flowName: isSubFlow ? node._flow.subflowDef.name : node._flow.flow.label,
isSubFlow: isSubFlow
}
}
return {
/**
* Get a list of targets registerd to this name
* @param {string} name Name of the target
* @param {boolean} [excludeSubflows] set `true` to exclude
* @returns {[LinkTarget]} Targets registerd to this name.
*/
getTargets(name, excludeSubflows) {
const targets = registry.name[name] || [];
if (excludeSubflows) {
return targets.filter(e => e.isSubFlow != true);
}
return targets;
},
/**
* Get a single target by registered name.
* To restrict to a single flow, include the `flowId`
* If there is no targets OR more than one target, null is returned
* @param {string} name Name of the node
* @param {string} [flowId]
* @returns {LinkTarget} target
*/
getTarget(name, flowId) {
/** @type {[LinkTarget]}*/
let possibleTargets = this.getTargets(name);
/** @type {LinkTarget}*/
let target;
if (possibleTargets.length && flowId) {
possibleTargets = possibleTargets.filter(e => e.flowId == flowId);
}
if (possibleTargets.length === 1) {
target = possibleTargets[0];
}
return target;
},
/**
* Get a target by node ID
* @param {string} nodeId ID of the node
* @returns {LinkTarget} target
*/
getTargetById(nodeId) {
return registry.id[nodeId];
},
register(/** @type {LinkInNode} */ node) {
const target = generateTarget(node);
const tByName = this.getTarget(target.name, target.flowId);
if (!tByName || tByName.id !== target.id) {
registry.name[target.name] = registry.name[target.name] || [];
registry.name[target.name].push(target)
}
registry.id[target.id] = target;
return target;
},
remove(node) {
const target = generateTarget(node);
const tn = this.getTarget(target.name, target.flowId);
if (tn) {
const targs = this.getTargets(tn.name);
const idx = getIndex(targs, tn.id);
if (idx > -1) {
targs.splice(idx, 1);
}
if (targs.length === 0) {
delete registry.name[tn.name];
}
}
delete registry.id[target.id];
},
clear() {
registry = { id: {}, name: {} };
}
}
})();
function LinkInNode(n) {
RED.nodes.createNode(this,n);
@@ -27,12 +136,14 @@ module.exports = function(RED) {
msg._event = n.event;
node.receive(msg);
}
targetCache.register(node);
RED.events.on(event,handler);
this.on("input", function(msg, send, done) {
send(msg);
done();
});
this.on("close",function() {
targetCache.remove(node);
RED.events.removeListener(event,handler);
});
}
@@ -74,31 +185,69 @@ module.exports = function(RED) {
function LinkCallNode(n) {
RED.nodes.createNode(this,n);
const node = this;
const target = n.links[0];
const staticTarget = typeof n.links === "string" ? n.links : n.links[0];
const linkType = n.linkType;
const messageEvents = {};
let timeout = parseFloat(n.timeout || "30")*1000;
let timeout = parseFloat(n.timeout || "30") * 1000;
if (isNaN(timeout)) {
timeout = 30000;
}
function getTargetNode(msg) {
const dynamicMode = linkType === "dynamic";
const target = dynamicMode ? msg.target : staticTarget
this.on("input", function(msg, send, done) {
msg._linkSource = msg._linkSource || [];
const messageEvent = {
id: crypto.randomBytes(14).toString('hex'),
node: node.id,
////1st see if the target is a direct node id
let foundNode;
if (targetCache.getTargetById(target)) {
foundNode = RED.nodes.getNode(target)
}
messageEvents[messageEvent.id] = {
msg: RED.util.cloneMessage(msg),
send,
done,
ts: setTimeout(function() {
timeoutMessage(messageEvent.id)
}, timeout )
};
msg._linkSource.push(messageEvent);
var targetNode = RED.nodes.getNode(target);
if (targetNode) {
targetNode.receive(msg);
if (target && !foundNode && dynamicMode) {
//next, look in **this flow only** for the node
let cachedTarget = targetCache.getTarget(target, node._flow.flow.id);
if (!cachedTarget) {
//single target node not found in registry!
//get all possible targets from regular flows (exclude subflow instances)
const possibleTargets = targetCache.getTargets(target, true);
if (possibleTargets.length === 1) {
//only 1 link-in found with this name - good, lets use it
cachedTarget = possibleTargets[0];
} else if (possibleTargets.length > 1) {
//more than 1 link-in has this name, raise an error
throw new Error(`Multiple link-in nodes named '${target}' found`);
}
}
if (cachedTarget) {
foundNode = RED.nodes.getNode(cachedTarget.id);
}
}
if (foundNode instanceof LinkInNode) {
return foundNode;
}
throw new Error(`target link-in node '${target || ""}' not found`);
}
this.on("input", function (msg, send, done) {
try {
const targetNode = getTargetNode(msg);
if (targetNode instanceof LinkInNode) {
msg._linkSource = msg._linkSource || [];
const messageEvent = {
id: crypto.randomBytes(14).toString('hex'),
node: node.id,
}
messageEvents[messageEvent.id] = {
msg: RED.util.cloneMessage(msg),
send,
done,
ts: setTimeout(function () {
timeoutMessage(messageEvent.id)
}, timeout)
};
msg._linkSource.push(messageEvent);
targetNode.receive(msg);
}
} catch (error) {
node.error(error, msg);
}
});

View File

@@ -581,12 +581,45 @@ RED.debug = (function() {
var metaRow = $('<div class="red-ui-debug-msg-meta"></div>').appendTo(msg);
$('<span class="red-ui-debug-msg-date">'+ getTimestamp()+'</span>').appendTo(metaRow);
if (sourceNode) {
$('<a>',{href:"#",class:"red-ui-debug-msg-name"}).text('node: '+(o.name||sourceNode.name||sourceNode.id))
var nodeLink = $('<a>',{href:"#",class:"red-ui-debug-msg-name"}).text("node: "+(o.name||sourceNode.name||sourceNode.id))
.appendTo(metaRow)
.on("click", function(evt) {
evt.preventDefault();
config.messageSourceClick(sourceNode.id, sourceNode._alias, sourceNode.path);
});
if (sourceNode.pathHierarchy) {
RED.popover.create({
tooltip: true,
target:nodeLink,
trigger: "hover",
size: "small",
direction: "bottom",
interactive: true,
content: function() {
const content = $("<div>")
sourceNode.pathHierarchy.forEach((pathPart,idx) => {
const link = $("<a>", {href:"#" ,style:'display: block'})
.css({
paddingLeft:((idx*10)+((idx === sourceNode.pathHierarchy.length - 1)?10:0))+"px",
paddingRight:'2px'
})
.text(pathPart.label)
.appendTo(content)
.on("click", function(evt) {
evt.preventDefault();
config.messageSourceClick(pathPart.id);
})
if (idx < sourceNode.pathHierarchy.length - 1) {
$('<i class="fa fa-angle-down" style="margin-right: 3px"></i>').prependTo(link)
}
})
return content
},
delay: { show: 50, hide: 150 }
});
}
} else if (name) {
$('<span class="red-ui-debug-msg-name">'+name+'</span>').appendTo(metaRow);
}

View File

@@ -355,27 +355,41 @@
color:"#fdd0a2",
category: 'function',
defaults: {
name: {value:""},
name: {value:"_DEFAULT_"},
func: {value:"\nreturn msg;"},
outputs: {value:1},
noerr: {value:0,required:true,validate:function(v) { return !v; }},
noerr: {value:0,required:true,
validate: function(v, opt) {
if (!v) {
return true;
}
return RED._("node-red:function.error.invalid-js");
}},
initialize: {value:""},
finalize: {value:""},
libs: {value: [], validate: function(v) {
libs: {value: [], validate: function(v, opt) {
if (!v) { return true; }
for (var i=0,l=v.length;i<l;i++) {
var m = v[i];
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
return false
return RED._("node-red:function.error.moduleNotAllowed", {
module: m.module
});
}
if (m.var === "" || / /.test(m.var)) {
return false;
return RED._("node-red:function.error.moduleNameError", {
name: m.var
});
}
if (missingModules.indexOf(m.module) > -1) {
return false;
return RED._("node-red:function.error.missing-module", {
module: m.module
});
}
if (invalidModuleVNames.indexOf(m.var) !== -1){
return false;
return RED._("node-red:function.error.moduleNameError", {
name: m.var
});
}
}
return true;
@@ -399,11 +413,19 @@
$("#func-tabs-content").children().hide();
$("#" + tab.id).show();
let editor = $("#" + tab.id).find('.monaco-editor').first();
if(editor.length) {
if(editor.length) {
if(that.editor.nodered && that.editor.type == "monaco") {
that.editor.nodered.refreshModuleLibs(getLibsList());
}
RED.tray.resize();
//auto focus editor on tab switch
if (that.initEditor.getDomNode() == editor[0]) {
that.initEditor.focus();
} else if (that.editor.getDomNode() == editor[0]) {
that.editor.focus();
} else if (that.finalizeEditor.getDomNode() == editor[0]) {
that.finalizeEditor.focus();
}
}
}
});
@@ -438,11 +460,13 @@
}
});
var buildEditor = function(id, value, defaultValue, extraLibs) {
var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs) {
var editor = RED.editor.createEditor({
id: id,
mode: 'ace/mode/nrjavascript',
value: value || defaultValue || "",
stateId: stateId,
focus: true,
globals: {
msg:true,
context:true,
@@ -462,11 +486,12 @@
if (defaultValue && value === "") {
editor.moveCursorTo(defaultValue.split("\n").length - 1, 0);
}
editor.__stateId = stateId;
return editor;
}
this.initEditor = buildEditor('node-input-init-editor',$("#node-input-initialize").val(),RED._("node-red:function.text.initialize"))
this.editor = buildEditor('node-input-func-editor',$("#node-input-func").val(), undefined, that.libs || [])
this.finalizeEditor = buildEditor('node-input-finalize-editor',$("#node-input-finalize").val(),RED._("node-red:function.text.finalize"))
this.initEditor = buildEditor('node-input-init-editor', this.id + "/" + "initEditor", false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize"))
this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || [])
this.finalizeEditor = buildEditor('node-input-finalize-editor', this.id + "/" + "finalizeEditor", false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize"))
RED.library.create({
url:"functions", // where to get the data from
@@ -505,28 +530,33 @@
],
ext:"js"
});
this.editor.focus();
var expandButtonClickHandler = function(editor) {
return function(e) {
return function (e) {
e.preventDefault();
var value = editor.getValue();
editor.saveView(`inside function-expandButtonClickHandler ${editor.__stateId}`);
var extraLibs = that.libs || [];
RED.editor.editJavaScript({
value: value,
width: "Infinity",
cursor: editor.getCursorPosition(),
stateId: editor.__stateId,
mode: "ace/mode/nrjavascript",
complete: function(v,cursor) {
editor.setValue(v, -1);
editor.gotoLine(cursor.row+1,cursor.column,false);
setTimeout(function() {
focus: true,
cancel: function () {
setTimeout(function () {
editor.focus();
},300);
}, 250);
},
complete: function (v, cursor) {
editor.setValue(v, -1);
setTimeout(function () {
editor.restoreView();
editor.focus();
}, 250);
},
extraLibs: extraLibs
})
});
}
}
$("#node-init-expand-js").on("click", expandButtonClickHandler(this.initEditor));
@@ -605,6 +635,12 @@
this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height - 192)+"px");
},
onadd: function() {
if (this.name === '_DEFAULT_') {
this.name = ''
RED.actions.invoke("core:generate-node-names", this)
}
}
});
})();

View File

@@ -163,7 +163,9 @@
category: 'function',
defaults: {
name: {value:""},
property: {value:"payload", required:true, validate: RED.validators.typedInput("propertyType")},
property: {value:"payload", required:true,
label:RED._("node-red:common.label.payload"),
validate: RED.validators.typedInput("propertyType", false)},
propertyType: { value:"msg" },
rules: {value:[{t:"eq", v:"", vt:"str"}]},
checkall: {value:"true", required:true},

View File

@@ -55,6 +55,7 @@ module.exports = function(RED) {
catch(e) { return false;}
}
else if (b === "null") { return a === null; }
else if (b === "number") { return typeof a === b && !isNaN(a) }
else { return typeof a === b && !Array.isArray(a) && !Buffer.isBuffer(a) && a !== null; }
},
'head': function(a, b, c, d, parts) {

View File

@@ -19,38 +19,66 @@
<script type="text/javascript">
(function() {
function validateProperty(v,vt) {
function isInvalidProperty(v,vt) {
if (/msg|flow|global/.test(vt)) {
if (!RED.utils.validatePropertyExpression(v)) {
return false;
return RED._("node-red:change.errors.invalid-prop", {
property: v
});
}
} else if (vt === "jsonata") {
try{jsonata(v);}catch(e){return false;}
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 false;}
try{ JSON.parse(v); } catch(e) {
return RED._("node-red:change.errors.invalid-json-data", {
error: e.message
});
}
}
return true;
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) {
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 (!validateProperty(r.p,r.pt) || !validateProperty(r.to,r.tot)) {
return false;
if (msg = isInvalidProperty(r.p,r.pt)) {
return msg;
}
if (msg = isInvalidProperty(r.to,r.tot)) {
return msg;
}
} else if (r.t === 'change') {
if (!validateProperty(r.p,r.pt) || !validateProperty(r.from,r.fromt) || !validateProperty(r.to,r.tot)) {
return false;
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 (!validateProperty(r.p,r.pt)) {
return false;
if (msg = isInvalidProperty(r.p,r.pt)) {
return msg;
}
if (msg = isInvalidProperty(r.to,r.tot)) {
return msg;
}
}
}

View File

@@ -41,13 +41,22 @@
color: "#E2D96E",
category: 'function',
defaults: {
minin: {value:"",required:true,validate:RED.validators.number()},
maxin: {value:"",required:true,validate:RED.validators.number()},
minout: {value:"",required:true,validate:RED.validators.number()},
maxout: {value:"",required:true,validate:RED.validators.number()},
minin: {value:"", required: true,
label:RED._("node-red:range.label.minin"),
validate:RED.validators.number(false)},
maxin: {value:"", required: true,
label:RED._("node-red:range.label.maxin"),
validate:RED.validators.number(false)},
minout: {value:"", required:true,
label:RED._("node-red:range.label.minout"),
validate:RED.validators.number(false)},
maxout: {value:"", required:true,
label:RED._("node-red:range.label.maxout"),
validate:RED.validators.number(false)},
action: {value:"scale"},
round: {value:false},
property: {value:"payload",required:true},
property: {value:"payload",required:true,
label:RED._("node-red:common.label.property")},
name: {value:""}
},
inputs: 1,

View File

@@ -18,7 +18,7 @@
<option value="handlebars">mustache</option>
<option value="html">HTML</option>
<option value="json">JSON</option>
<option value="javascript">Javascript</option>
<option value="javascript">JavaScript</option>
<option value="css">CSS</option>
<option value="markdown">Markdown</option>
<option value="python">Python</option>
@@ -56,7 +56,9 @@
category: 'function',
defaults: {
name: {value:""},
field: {value:"payload", validate:RED.validators.typedInput("fieldType")},
field: {value:"payload",
label:"payload",
validate:RED.validators.typedInput("fieldType", false)},
fieldType: {value:"msg"},
format: {value:"handlebars"},
syntax: {value:"mustache"},
@@ -73,7 +75,8 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var that = this;
const that = this;
const stateId = RED.editor.generateViewStateId("node", this, "");
if (!this.field) {
this.field = 'payload';
$("#node-input-field").val("payload");
@@ -90,10 +93,10 @@
types: ['msg','flow','global'],
typeField: $("#node-input-fieldType")
});
this.editor = RED.editor.createEditor({
id: 'node-input-template-editor',
mode: 'ace/mode/html',
stateId: stateId,
value: $("#node-input-template").val()
});
RED.library.create({
@@ -103,7 +106,6 @@
fields:['name','format','output','syntax'],
ext: "txt"
});
this.editor.focus();
$("#node-input-format").on("change", function() {
var mod = "ace/mode/"+$("#node-input-format").val();
@@ -113,20 +115,22 @@
});
});
RED.popover.tooltip($("#node-template-expand-editor"), RED._("node-red:common.label.expand"));
$("#node-template-expand-editor").on("click", function(e) {
$("#node-template-expand-editor").on("click", function (e) {
e.preventDefault();
var value = that.editor.getValue();
const value = that.editor.getValue();
that.editor.saveView();
RED.editor.editText({
mode: $("#node-input-format").val(),
value: value,
stateId: stateId,
width: "Infinity",
cursor: that.editor.getCursorPosition(),
complete: function(v,cursor) {
focus: true,
complete: function (v, cursor) {
that.editor.setValue(v, -1);
that.editor.gotoLine(cursor.row+1,cursor.column,false);
setTimeout(function() {
setTimeout(function () {
that.editor.restoreView();
that.editor.focus();
},300);
}, 250);
}
})
})

View File

@@ -111,14 +111,54 @@
defaults: {
name: {value:""},
pauseType: {value:"delay", required:true},
timeout: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
timeout: {
value:"5", required:true,
label:RED._("node-red:delay.label.delay"),
validate:function(v,opt) {
if (RED.validators.number(v) && (v >= 0)) {
return true;
}
return RED._("node-red:delay.errors.invalid-timeout");
}},
timeoutUnits: {value:"seconds"},
rate: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
nbRateUnits: {value:"1", required:false,
validate:function(v) { return v === undefined || (RED.validators.number(v) && (v >= 0)); }},
rate: {
value:"1",
required:true,
label:RED._("node-red:delay.label.rate"),
validate:function(v,opt) {
if (RED.validators.number(v) && (v >= 0)) {
return true;
}
return RED._("node-red:delay.errors.invalid-rate");
}
},
nbRateUnits: {
value:"1",
required:false,
validate:function(v,opt) {
if (v === undefined || (RED.validators.number(v) && (v >= 0))) {
return true;
}
return RED._("node-red:delay.errors.invalid-rate-unit");
}
},
rateUnits: {value: "second"},
randomFirst: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomLast: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomFirst: {
value:"1", required:true,
validate:function(v,opt) {
if (RED.validators.number(v) && (v >= 0)) {
return true;
}
return RED._("node-red:delay.errors.invalid-random-first");
}},
randomLast: {
value:"5", required:true,
validate:function(v,opt) {
if (RED.validators.number(v) && (v >= 0)) {
return true;
}
return RED._("node-red:delay.errors.invalid-random-last");
}},
randomUnits: {value: "seconds"},
drop: {value:false},
allowrate: {value:false},

View File

@@ -87,17 +87,25 @@
color:"#E6E0F8",
defaults: {
name: {value:""},
op1: {value:"1", validate: RED.validators.typedInput("op1type")},
op2: {value:"0", validate: RED.validators.typedInput("op2type")},
op1: {value:"1",
label: RED._("node-red:trigger.send"),
validate: RED.validators.typedInput("op1type", false)},
op2: {value:"0",
label: RED._("node-red:trigger.then-send"),
validate: RED.validators.typedInput("op2type", false)},
op1type: {value:"val"},
op2type: {value:"val"},
duration: {value:"250",required:true,validate:RED.validators.number()},
duration: {
value:"250", required:true,
label:RED._("node-red:trigger.label.duration"),
validate:RED.validators.number(false)},
extend: {value:"false"},
overrideDelay: {value:"false"},
units: {value:"ms"},
reset: {value:""},
bytopic: {value:"all"},
topic: {value:"topic",required:true},
topic: {value:"topic", required:true,
label:RED._("node-red:trigger.label.topic")},
outputs: {value:1}
},
inputs:1,

View File

@@ -49,12 +49,16 @@
defaults: {
name: {value:""},
func: {value:"rbe"},
gap: {value:"",validate:RED.validators.regex(/^(\d*[.]*\d*|)(%|)$/)},
gap: {value:"",
label: RED._("node-red:rbe.label.gap"),
validate:RED.validators.regex(/^(\d*[.]*\d*|)(%|)$/)},
start: {value:""},
inout: {value:"out"},
septopics: {value:true},
property: {value:"payload",required:true},
topi: {value:"topic",required:true}
property: {value:"payload", required:true,
label:RED._("node-red:rbe.label.property")},
topi: {value:"topic", required:true,
label:RED._("node-red:rbe.label.topic")}
},
inputs:1,
outputs:1,

View File

@@ -83,19 +83,25 @@
category: 'config',
defaults: {
name: {value:""},
cert: {value:"", validate: function(v) {
cert: {value:"", validate: function(v,opt) {
var currentKey = $("#node-config-input-key").val();
if (currentKey === undefined) {
currentKey = this.key;
}
return currentKey === '' || v != '';
if (currentKey === '' || v != '') {
return true;
}
return RED._("node-red:tls.error.invalid-cert");
}},
key: {value:"", validate: function(v) {
key: {value:"", validate: function(v,opt) {
var currentCert = $("#node-config-input-cert").val();
if (currentCert === undefined) {
currentCert = this.cert;
}
return currentCert === '' || v != '';
if (currentCert === '' || v != '') {
return true;
}
return RED._("node-red:tls.error.invalid-key");
}},
ca: {value:""},
certname: {value:""},

View File

@@ -50,7 +50,16 @@
category: 'config',
defaults: {
name: {value:''},
url: {value:'',validate:function(v) { return (v && (v.indexOf('://') !== -1) && (v.trim().indexOf('http') === 0)); }},
url: {
value:'',
validate:function(v, opt) {
if ((v && (v.indexOf('://') !== -1) &&
(v.trim().indexOf('http') === 0))) {
return true;
}
return RED._("node-red:httpin.errors.invalid-url");
}
},
noproxy: {value:[]}
},
credentials: {

View File

@@ -65,6 +65,11 @@
/* opacity: 0.3;
pointer-events: none; */
}
.form-row.form-row-mqtt-datatype-tip > .form-tips {
width: calc(70% - 18px);
display: inline-block;
margin-top: -8px;
}
</style>
@@ -121,6 +126,7 @@
<div class="form-row">
<label for="node-input-datatype"><i class="fa fa-sign-out"></i> <span data-i18n="mqtt.label.output"></span></label>
<select id="node-input-datatype" style="width:70%;">
<option value="auto-detect" data-i18n="mqtt.output.auto-detect"></option>
<option value="auto" data-i18n="mqtt.output.auto"></option>
<option value="buffer" data-i18n="mqtt.output.buffer"></option>
<option value="utf8" data-i18n="mqtt.output.string"></option>
@@ -128,6 +134,10 @@
<option value="base64" data-i18n="mqtt.output.base64"></option>
</select>
</div>
<div class="form-row form-row-mqtt-datatype-tip">
<label> &nbsp; </label>
<div class="form-tips" id="mqtt-in-datatype-depreciated-tip"><span data-i18n="mqtt.label.auto-mode-depreciated"></span></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">
@@ -442,40 +452,62 @@
}
return defaultContentType || 'none'
}
/**
* Test a topic string is valid for publishing
* @param {string} topic
* @returns `true` if it is a valid topic
*/
function validateMQTTPublishTopic(topic, opts) {
if(!topic || topic == "" || !/[\+#\b\f\n\r\t\v\0]/.test(topic)) {
return true;
}
return RED._("node-red:mqtt.errors.invalid-topic");
}
RED.nodes.registerType('mqtt-broker',{
category: 'config',
defaults: {
name: {value:""},
broker: {value:"",required:true},
port: {value:1883,required:false,validate:RED.validators.number(true)},
tls: {type:"tls-config",required: false},
clientid: {value:"", validate: function(v) {
port: {
value:1883,required:false,
label: RED._("node-red:mqtt.label.port"),
validate:RED.validators.number(true)},
tls: {type:"tls-config",required: false,
label:RED._("node-red:mqtt.label.use-tls") },
clientid: {value:"", validate: function(v, opt) {
var ok = false;
if ($("#node-config-input-clientid").length) {
// Currently editing the node
return $("#node-config-input-cleansession").is(":checked") || (v||"").length > 0;
ok = $("#node-config-input-cleansession").is(":checked") || (v||"").length > 0;
} else {
return (this.cleansession===undefined || this.cleansession) || (v||"").length > 0;
ok = (this.cleansession===undefined || this.cleansession) || (v||"").length > 0;
}
if (ok) {
return ok;
}
return RED._("node-red:mqtt.errors.invalid-client-id");
}},
autoConnect: {value: true},
usetls: {value: false},
verifyservercert: { value: false},
compatmode: { value: false},
protocolVersion: { value: 4},
keepalive: {value:60,validate:RED.validators.number()},
keepalive: {
value:60,
label: RED._("node-red:mqtt.label.keepalive"),
validate:RED.validators.number(false)},
cleansession: {value: true},
birthTopic: {value:""},
birthTopic: {value:"", validate:validateMQTTPublishTopic},
birthQos: {value:"0"},
birthRetain: {value:false},
birthPayload: {value:""},
birthMsg: { value: {}},
closeTopic: {value:""},
closeTopic: {value:"", validate:validateMQTTPublishTopic},
closeQos: {value:"0"},
closeRetain: {value:false},
closePayload: {value:""},
closeMsg: { value: {}},
willTopic: {value:""},
willTopic: {value:"", validate:validateMQTTPublishTopic},
willQos: {value:"0"},
willRetain: {value:false},
willPayload: {value:""},
@@ -750,18 +782,21 @@
name: {value:""},
topic: {
value:"",
validate: function(v) {
validate: function(v, opt) {
var isDynamic = this.inputs === 1;
var topicTypeSelect = $("#node-input-topicType");
if (topicTypeSelect.length) {
isDynamic = topicTypeSelect.val()==='dynamic'
}
return isDynamic || ((!!v) && RED.validators.regex(/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/)(v));
if (isDynamic || ((!!v) && RED.validators.regex(/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/)(v))) {
return true;
}
return RED._("node-red:mqtt.errors.invalid-topic");
}
},
qos: {value: "2"},
datatype: {value:"auto",required:true},
broker: {type:"mqtt-broker", required:true},
datatype: {value:"auto-detect",required:true},
broker: {type:"mqtt-broker", required:true, label:RED._("node-red:mqtt.label.broker")},
// subscriptionIdentifier: {value:0},
nl: {value:false},
rap: {value:true},
@@ -797,6 +832,16 @@
$("div.form-row-mqtt5").toggleClass("form-row-mqtt5-active",!!v5);
$("div.form-row.form-row-mqtt-static").toggleClass("form-row-mqtt-static-disabled", !!dynamic)
}
$("#node-input-datatype").on("change", function() {
if($(this).val() === "auto") {
$(".form-row.form-row-mqtt-datatype-tip").show();
} else {
$(".form-row.form-row-mqtt-datatype-tip").hide();
}
})
$("#node-input-datatype").trigger("change");
$("#node-input-broker").on("change",function(d){
updateVisibility();
});
@@ -817,7 +862,7 @@
$("#node-input-qos").val("2");
}
if (this.datatype === undefined) {
$("#node-input-datatype").val("auto");
$("#node-input-datatype").val("auto-detect");
}
},
oneditsave: function() {
@@ -831,7 +876,7 @@
category: 'network',
defaults: {
name: {value:""},
topic: {value:""},
topic: {value:"", validate:validateMQTTPublishTopic},
qos: {value:""},
retain: {value:""},
respTopic: {value:""},
@@ -839,7 +884,8 @@
userProps: {value:''},
correl: {value:''},
expiry: {value:''},
broker: {type:"mqtt-broker", required:true}
broker: {type:"mqtt-broker", required:true,
label:RED._("node-red:mqtt.label.broker") }
},
color:"#d8bfd8",
inputs:1,

View File

@@ -20,7 +20,30 @@ module.exports = function(RED) {
var isUtf8 = require('is-utf8');
var HttpsProxyAgent = require('https-proxy-agent');
var url = require('url');
const knownMediaTypes = {
"text/css":"string",
"text/html":"string",
"text/plain":"string",
"text/html":"string",
"application/json":"json",
"application/octet-stream":"buffer",
"application/pdf":"buffer",
"application/x-gtar":"buffer",
"application/x-gzip":"buffer",
"application/x-tar":"buffer",
"application/xml":"string",
"application/zip":"buffer",
"audio/aac":"buffer",
"audio/ac3":"buffer",
"audio/basic":"buffer",
"audio/mp4":"buffer",
"audio/ogg":"buffer",
"image/bmp":"buffer",
"image/gif":"buffer",
"image/jpeg":"buffer",
"image/tiff":"buffer",
"image/png":"buffer",
}
//#region "Supporting functions"
function matchTopic(ts,t) {
if (ts == "#") {
@@ -68,12 +91,21 @@ module.exports = function(RED) {
}
/**
* Test a topic string is valid
* Test a topic string is valid for subscription
* @param {string} topic
* @returns `true` if it is a valid topic
*/
function isValidSubscriptionTopic(topic) {
return /^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(topic)
return /^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(topic);
}
/**
* Test a topic string is valid for publishing
* @param {string} topic
* @returns `true` if it is a valid topic
*/
function isValidPublishTopic(topic) {
return !/[\+#\b\f\n\r\t\v\0]/.test(topic);
}
/**
@@ -188,24 +220,7 @@ module.exports = function(RED) {
*/
function subscriptionHandler(node, datatype ,topic, payload, packet) {
const v5 = node.brokerConn.options && node.brokerConn.options.protocolVersion == 5;
if (datatype === "buffer") {
// payload = payload;
} else if (datatype === "base64") {
payload = payload.toString('base64');
} else if (datatype === "utf8") {
payload = payload.toString('utf8');
} else if (datatype === "json") {
if (isUtf8(payload)) {
payload = payload.toString();
try { payload = JSON.parse(payload); }
catch(e) { node.error(RED._("mqtt.errors.invalid-json-parse"),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
}
else { node.error((RED._("mqtt.errors.invalid-json-string")),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
} else {
if (isUtf8(payload)) { payload = payload.toString(); }
}
var msg = {topic:topic, payload:payload, qos:packet.qos, retain:packet.retain};
var msg = {topic:topic, payload:null, qos:packet.qos, retain:packet.retain};
if(v5 && packet.properties) {
setStrProp(packet.properties, msg, "responseTopic");
setBufferProp(packet.properties, msg, "correlationData");
@@ -215,6 +230,76 @@ module.exports = function(RED) {
setStrProp(packet.properties, msg, "reasonString");
setUserProperties(packet.properties.userProperties, msg);
}
const v5isUtf8 = v5 ? msg.payloadFormatIndicator === true : null;
const v5HasMediaType = v5 ? !!msg.contentType : null;
const v5MediaTypeLC = v5 ? (msg.contentType + "").toLowerCase() : null;
if (datatype === "buffer") {
// payload = payload;
} else if (datatype === "base64") {
payload = payload.toString('base64');
} else if (datatype === "utf8") {
payload = payload.toString('utf8');
} else if (datatype === "json") {
if (v5isUtf8 || isUtf8(payload)) {
try {
payload = JSON.parse(payload.toString());
} catch (e) {
node.error(RED._("mqtt.errors.invalid-json-parse"), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
}
} else {
node.error((RED._("mqtt.errors.invalid-json-string")), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
}
} else {
//"auto" (legacy) or "auto-detect" (new default)
if (v5isUtf8 || v5HasMediaType) {
const outputType = knownMediaTypes[v5MediaTypeLC]
switch (outputType) {
case "string":
payload = payload.toString();
break;
case "buffer":
//no change
break;
case "json":
try {
//since v5 type states this should be JSON, parse it & error out if NOT JSON
payload = payload.toString()
const obj = JSON.parse(payload);
if (datatype === "auto-detect") {
payload = obj; //as mode is "auto-detect", return the parsed JSON
}
} catch (e) {
node.error(RED._("mqtt.errors.invalid-json-parse"), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
}
break;
default:
if (v5isUtf8 || isUtf8(payload)) {
payload = payload.toString(); //auto String
if (datatype === "auto-detect") {
try {
payload = JSON.parse(payload); //auto to parsed object (attempt)
} catch (e) {
/* mute error - it simply isnt JSON, just leave payload as a string */
}
}
}
break;
}
} else if (isUtf8(payload)) {
payload = payload.toString(); //auto String
if (datatype === "auto-detect") {
try {
payload = JSON.parse(payload);
} catch (e) {
/* mute error - it simply isnt JSON, just leave payload as a string */
}
}
} //else {
//leave as buffer
//}
}
msg.payload = payload;
if ((node.brokerConn.broker === "localhost")||(node.brokerConn.broker === "127.0.0.1")) {
msg._topic = topic;
}
@@ -264,38 +349,15 @@ module.exports = function(RED) {
msg.messageExpiryInterval = node.messageExpiryInterval;
}
}
if (msg.userProperties && typeof msg.userProperties !== "object") {
delete msg.userProperties;
}
if (hasProperty(msg, "topicAlias") && !isNaN(msg.topicAlias) && (msg.topicAlias === 0 || bsp.topicAliasMaximum === 0 || msg.topicAlias > bsp.topicAliasMaximum)) {
delete msg.topicAlias;
}
if (hasProperty(msg, "payload")) {
//check & sanitise topic
let topicOK = hasProperty(msg, "topic") && (typeof msg.topic === "string") && (msg.topic !== "");
if (!topicOK && v5) {
//NOTE: A value of 0 (in server props topicAliasMaximum) indicates that the Server does not accept any Topic Aliases on this connection
if (hasProperty(msg, "topicAlias") && !isNaN(msg.topicAlias) && msg.topicAlias >= 0 && bsp.topicAliasMaximum && bsp.topicAliasMaximum >= msg.topicAlias) {
topicOK = true;
msg.topic = ""; //must be empty string
} else if (hasProperty(msg, "responseTopic") && (typeof msg.responseTopic === "string") && (msg.responseTopic !== "")) {
//TODO: if topic is empty but responseTopic has a string value, use that instead. Is this desirable?
topicOK = true;
msg.topic = msg.responseTopic;
//TODO: delete msg.responseTopic - to prevent it being resent?
// send the message
node.brokerConn.publish(msg, function(err) {
if(err && err.warn) {
node.warn(err);
return;
}
}
topicOK = topicOK && !/[\+#\b\f\n\r\t\v\0]/.test(msg.topic);
if (topicOK) {
node.brokerConn.publish(msg, done); // send the message
} else {
node.warn(RED._("mqtt.errors.invalid-topic"));
done();
}
done(err);
});
} else {
done();
}
@@ -362,7 +424,7 @@ module.exports = function(RED) {
node.brokerConn.connect(function () {
done();
});
}, true)
})
} else {
// Without force flag, we will refuse to cycle an active connection
done(new Error(RED._('mqtt.errors.invalid-action-alreadyconnected')));
@@ -754,35 +816,48 @@ module.exports = function(RED) {
}
}
};
node.disconnect = function (callback, force) {
const _callback = function (resetNodeConnectedState, _force) {
setStatusDisconnected(node, true);
if(resetNodeConnectedState || _force) {
node.client.removeAllListeners();
node.closing = true;
node.connecting = false;
node.connected = false;
node.disconnect = function (callback) {
const _callback = function () {
if(node.connected || node.connecting) {
setStatusDisconnected(node, true);
}
if(node.client) { node.client.removeAllListeners(); }
node.connecting = false;
node.connected = false;
callback && typeof callback == "function" && callback();
};
if(node.closing) {
return _callback(false, force);
}
var endCallBack = function endCallBack() {
}
if(!node.client) { return _callback(); }
if(node.closing) { return _callback(); }
let waitEnd = (client, ms) => {
return new Promise( (resolve, reject) => {
node.closing = true;
if(!client) {
resolve();
} else {
const t = setTimeout(reject, ms);
client.end(() => {
clearTimeout(t);
resolve()
});
}
});
};
if(node.connected && node.closeMessage) {
node.publish(node.closeMessage, function (err) {
node.client.end(endCallBack);
_callback(true, force);
waitEnd(node.client, 2000).then(() => {
_callback();
}).catch((e) => {
_callback();
})
});
} else if(node.connected) {
node.client.end(endCallBack);
_callback(true, force);
} else if(node.connecting) {
node.client.end();
_callback(true, true);
} else {
_callback(false, force);
waitEnd(node.client, 2000).then(() => {
_callback();
}).catch((e) => {
_callback();
})
}
}
node.subscriptionIds = {};
@@ -864,8 +939,18 @@ module.exports = function(RED) {
qos: msg.qos || 0,
retain: msg.retain || false
};
let topicOK = hasProperty(msg, "topic") && (typeof msg.topic === "string") && (isValidPublishTopic(msg.topic));
//https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
if(node.options.protocolVersion == 5) {
const bsp = node.serverProperties || {};
if (msg.userProperties && typeof msg.userProperties !== "object") {
delete msg.userProperties;
}
if (hasProperty(msg, "topicAlias") && !isNaN(Number(msg.topicAlias))) {
msg.topicAlias = parseInt(msg.topicAlias);
} else {
delete msg.topicAlias;
}
options.properties = options.properties || {};
setStrProp(msg, options.properties, "responseTopic");
setBufferProp(msg, options.properties, "correlationData");
@@ -875,29 +960,46 @@ module.exports = function(RED) {
setIntProp(msg, options.properties, "topicAlias", 1, node.serverProperties.topicAliasMaximum || 0);
setBoolProp(msg, options.properties, "payloadFormatIndicator");
//FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
if (options.properties.topicAlias) {
if (!node.topicAliases.hasOwnProperty(options.properties.topicAlias) && msg.topic == "") {
//check & sanitise topic
if (topicOK && options.properties.topicAlias) {
let aliasValid = (bsp.topicAliasMaximum && bsp.topicAliasMaximum >= options.properties.topicAlias);
if (!aliasValid) {
done("Invalid topicAlias");
return
}
if (node.topicAliases[options.properties.topicAlias] === msg.topic) {
msg.topic = ""
msg.topic = "";
} else {
node.topicAliases[options.properties.topicAlias] = msg.topic
node.topicAliases[options.properties.topicAlias] = msg.topic;
}
} else if (!msg.topic && options.properties.responseTopic) {
msg.topic = msg.responseTopic;
topicOK = isValidPublishTopic(msg.topic);
delete msg.responseTopic; //prevent responseTopic being resent?
}
}
node.client.publish(msg.topic, msg.payload, options, function(err) {
done && done(err);
return
});
if (topicOK) {
node.client.publish(msg.topic, msg.payload, options, function(err) {
done && done(err);
return
});
} else {
const error = new Error(RED._("mqtt.errors.invalid-topic"));
error.warn = true;
done(error);
}
}
};
node.on('close', function(done) {
node.closing = true;
node.disconnect(done);
node.disconnect(function() {
if(node.client) {
node.client.removeAllListeners();
}
done();
});
});
}
@@ -1074,6 +1176,7 @@ module.exports = function(RED) {
node.brokerConn.unsubscribe(node.topic,node.id, removed);
}
node.brokerConn.deregister(node, done);
node.brokerConn = null;
} else {
done();
}
@@ -1138,6 +1241,7 @@ module.exports = function(RED) {
node.on('close', function(done) {
if (node.brokerConn) {
node.brokerConn.deregister(node,done);
node.brokerConn = null;
} else {
done();
}

View File

@@ -70,7 +70,8 @@
color:"rgb(231, 231, 174)",
defaults: {
name: {value:""},
url: {value:"",required:true},
url: {value:"", required:true,
label:RED._("node-red:httpin.label.url")},
method: {value:"get",required:true},
upload: {value:false},
swaggerDoc: {type:"swagger-doc", required:false}
@@ -146,7 +147,10 @@
color:"rgb(231, 231, 174)",
defaults: {
name: {value:""},
statusCode: {value:"",validate: RED.validators.number(true)},
statusCode: {
value:"",
label: RED._("node-red:httpin.label.status"),
validate: RED.validators.number(true)},
headers: {value:{}}
},
inputs:1,

View File

@@ -100,14 +100,107 @@
<option value="obj" data-i18n="httpin.json"></option>
</select>
</div>
<div class="form-row form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></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-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">
</div>
<div class="form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
</script>
<script type="text/javascript">
(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: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
{ value: "msg", label: "msg.", hasValue: true },
]
const headerOptions = {};
const defaultOptions = [
{ value: "other", label: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
{ value: "msg", label: "msg.", hasValue: true },
];
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;
}
RED.nodes.registerType('http request',{
category: 'network',
color:"rgb(231, 231, 174)",
@@ -116,12 +209,25 @@
method:{value:"GET"},
ret: {value:"txt"},
paytoqs: {value: false},
url:{value:"",validate:function(v) { return (v.trim().length === 0) || (v.indexOf("://") === -1) || (v.trim().indexOf("http") === 0)} },
tls: {type:"tls-config",required: false},
url:{
value:"",
validate: function(v, opt) {
if ((v.trim().length === 0) ||
(v.indexOf("://") === -1) ||
(v.trim().indexOf("http") === 0)) {
return true;
}
return RED._("node-red:httpin.errors.invalid-url");
}
},
tls: {type:"tls-config",required: false,
label:RED._("node-red:httpin.tls-config") },
persist: {value:false},
proxy: {type:"http proxy",required: false},
proxy: {type:"http proxy",required: false,
label:RED._("node-red:httpin.proxy-config") },
authType: {value: ""},
senderr: {value: false}
senderr: {value: false},
headers: { value: [] }
},
credentials: {
user: {type:"text"},
@@ -144,6 +250,7 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
const node = this;
$("#node-input-useAuth").on("change", function() {
if ($(this).is(":checked")) {
$(".node-input-useAuth-row").show();
@@ -157,9 +264,10 @@
$('#node-input-user').val('');
$('#node-input-password').val('');
}
RED.tray.resize();
});
$("#node-input-authType-select").on("change", function() {
var val = $(this).val();
const val = $(this).val();
$("#node-input-authType").val(val);
if (val === "basic" || val === "digest") {
$(".node-input-basic-row").show();
@@ -171,6 +279,7 @@
$('#node-span-token').show();
$('#node-input-user').val('');
}
RED.tray.resize();
});
$("#node-input-method").on("change", function() {
if ($(this).val() == "GET") {
@@ -178,17 +287,18 @@
} else {
$(".node-input-paytoqs-row").hide();
}
RED.tray.resize();
});
if (this.paytoqs === true || this.paytoqs == "query") {
if (node.paytoqs === true || node.paytoqs == "query") {
$("#node-input-paytoqs").val("query");
} else if (this.paytoqs === "body") {
} else if (node.paytoqs === "body") {
$("#node-input-paytoqs").val("body");
} else {
$("#node-input-paytoqs").val("ignore");
}
if (this.authType) {
if (node.authType) {
$('#node-input-useAuth').prop('checked', true);
$("#node-input-authType-select").val(this.authType);
$("#node-input-authType-select").val(node.authType);
$("#node-input-authType-select").change();
} else {
$('#node-input-useAuth').prop('checked', false);
@@ -201,8 +311,9 @@
} else {
$("#node-row-tls").hide();
}
RED.tray.resize();
}
if (this.tls) {
if (node.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
@@ -218,8 +329,9 @@
} else {
$("#node-input-useProxy-row").hide();
}
RED.tray.resize();
}
if (this.proxy) {
if (node.proxy) {
$("#node-input-useProxy").prop("checked", true);
} else {
$("#node-input-useProxy").prop("checked", false);
@@ -235,7 +347,70 @@
} else {
$("#tip-json").hide();
}
RED.tray.resize();
});
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 == "msg" || 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 == "msg" || valueType == "other") {
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')));
});
},
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() {
if (!$("#node-input-usetls").is(':checked')) {
@@ -244,6 +419,36 @@
if (!$("#node-input-useProxy").is(":checked")) {
$("#node-input-proxy").val("_ADD_");
}
const headers = $("#node-input-headers-container").editableList('items');
const node = this;
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');
if (keyType !== '' || keyType === 'other' || keyType === 'msg') {
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);
}
}
});
})();
</script>

View File

@@ -73,7 +73,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
var paytobody = false;
var redirectList = [];
var sendErrorsToCatch = n.senderr;
node.headers = n.headers || [];
var nodeHTTPPersistent = n["persist"];
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
@@ -105,6 +105,37 @@ in your Node-RED user directory (${RED.settings.userDir}).
timingLog = RED.settings.httpRequestTimingLog;
}
/**
* Case insensitive header value update util function
* @param {object} headersObject The opt.headers object to update
* @param {string} name The header name
* @param {string} value The header value to set (if blank, header is removed)
*/
const updateHeader = function(headersObject, name, value ) {
const hn = name.toLowerCase();
const keys = Object.keys(headersObject);
const matchingKeys = keys.filter(e => e.toLowerCase() == hn)
const updateKey = (k,v) => {
delete headersObject[k]; //delete incase of case change
if(v) { headersObject[name] = v } //re-add with requested name & value
}
if(matchingKeys.length == 0) {
updateKey(name, value)
} else {
matchingKeys.forEach(k => {
updateKey(k, value);
});
}
}
/**
* @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
@@ -183,7 +214,6 @@ in your Node-RED user directory (${RED.settings.userDir}).
// TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
opts.decompress = false;
opts.method = method;
opts.headers = {};
opts.retry = 0;
opts.responseType = 'buffer';
opts.maxRedirects = 21;
@@ -229,34 +259,85 @@ in your Node-RED user directory (${RED.settings.userDir}).
]
}
var ctSet = "Content-Type"; // set default camel case
var clSet = "Content-Length";
let ctSet = "Content-Type"; // set default camel case
let clSet = "Content-Length";
const normaliseKnownHeader = function (name) {
const _name = name.toLowerCase();
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
switch (_name) {
case "content-type":
ctSet = name;
name = _name;
break;
case "content-length":
clSet = name;
name = _name;
break;
}
return name;
}
opts.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')) {
var headerHash = msg.headers['x-node-red-request-node'];
const headerHash = msg.headers['x-node-red-request-node'];
delete msg.headers['x-node-red-request-node'];
var hash = hashSum(msg.headers);
const hash = hashSum(msg.headers);
if (hash === headerHash) {
delete msg.headers;
}
}
if (msg.headers) {
for (var v in msg.headers) {
if (msg.headers.hasOwnProperty(v)) {
var name = v.toLowerCase();
if (name !== "content-type" && name !== "content-length") {
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
name = v;
}
else if (name === 'content-type') { ctSet = v; }
else { clSet = v; }
opts.headers[name] = msg.headers[v];
}
for (let hn in msg.headers) {
const name = normaliseKnownHeader(hn);
updateHeader(opts.headers, name, msg.headers[hn]);
}
}
}
//add/remove/update headers from UI.
if (node.headers.length) {
for (let index = 0; index < node.headers.length; index++) {
const header = node.headers[index];
let headerName, headerValue;
if (header.keyType === "other") {
headerName = header.keyValue
} else if (header.keyType === "msg") {
RED.util.evaluateNodeProperty(header.keyValue, header.keyType, node, msg, (err, value) => {
if (err) {
//ignore header
} else {
headerName = value;
}
});
} else {
headerName = header.keyType
}
if (!headerName) {
continue; //skip if header name is empyy
}
if (header.valueType === "other") {
headerValue = header.valueValue
} else if (header.valueType === "msg") {
RED.util.evaluateNodeProperty(header.valueValue, header.valueType, node, msg, (err, value) => {
if (err) {
//ignore header
} else {
headerValue = value;
}
});
} else {
headerValue = header.valueType
}
const hn = normaliseKnownHeader(headerName);
updateHeader(opts.headers, hn, headerValue);
}
}
if (msg.hasOwnProperty('followRedirects')) {
opts.followRedirect = !!msg.followRedirects;
}

View File

@@ -83,13 +83,19 @@
return true;
}
else {
return RED.nodes.node(this.server) != null;
if (RED.nodes.node(this.server) != null) {
return true;
}
return RED._("node-red:websocket.errors.missing-server");
}
}
function ws_validateclient() {
if ($("#node-input-mode").val() === 'client' || (this.client && !this.server)) {
return RED.nodes.node(this.client) != null;
if (RED.nodes.node(this.client) != null) {
return true;
}
return RED._("node-red:websocket.errors.missing-client");
}
else {
return true;
@@ -138,7 +144,9 @@
RED.nodes.registerType('websocket-listener',{
category: 'config',
defaults: {
path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
path: {value:"",required:true,
label:RED._("node-red:websocket.label.path"),
validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
wholemsg: {value:"false"}
},
inputs:0,
@@ -174,10 +182,16 @@
RED.nodes.registerType('websocket-client',{
category: 'config',
defaults: {
path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
path: {
value:"",required:true,
label:RED._("node-red:websocket.label.path"),
validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
tls: {type:"tls-config",required: false},
wholemsg: {value:"false"},
hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) },
hb: {
value: "",
label:RED._("node-red:websocket.sendheartbeat"),
validate: RED.validators.number(/*blank allowed*/true) },
subprotocol: {value:"",required: false}
},
inputs:0,

View File

@@ -50,7 +50,8 @@
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:110px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional"><br/>
<input type="checkbox" id="node-input-trim" style="display:inline-block; width:auto; vertical-align:top;"> <span data-i18n="tcpin.label.reattach"></span>
</div>
<div class="form-row">
@@ -70,14 +71,27 @@
defaults: {
name: {value:""},
server: {value:"server", required:true},
host: {value:"", validate:function(v) { return (this.server == "server")||v.length > 0;} },
port: {value:"", required:true, validate:RED.validators.number()},
host: {
value:"",
validate:function(v, opt) {
if ((this.server == "server")||v.length > 0) {
return true;
}
return RED._("node-red:tcpin.errors.invalid-host");
}
},
port: {
value:"", required:true,
label:RED._("node-red:tcpin.label.port"),
validate:RED.validators.number(false)},
datamode:{value:"stream"},
datatype:{value:"buffer"},
newline:{value:""},
topic: {value:""},
trim: {value:false},
base64: {/*deprecated*/ value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
tls: {type:"tls-config", value:'', required:false,
label:RED._("node-red:httpin.tls-config") }
},
inputs:0,
outputs:1,
@@ -186,12 +200,29 @@
color: "Silver",
defaults: {
name: {value:""},
host: {value:"",validate:function(v) { return (this.beserver != "client")||v.length > 0;} },
port: {value:"",validate:function(v) { return (this.beserver == "reply")||RED.validators.number()(v); } },
host: {
value:"",
validate:function(v, opt) {
if ((this.beserver != "client")||v.length > 0) {
return true;
}
return RED._("node-red:tcpin.errors.invalid-host");
}
},
port: {
value:"",
validate:function(v) {
if ((this.beserver == "reply")||RED.validators.number()(v)) {
return true;
}
return RED._("node-red:tcpin.errors.invalid-port");
}
},
beserver: {value:"client", required:true},
base64: {value:false, required:true},
end: {value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
tls: {type:"tls-config", value:'', required:false,
label:RED._("node-red:httpin.tls-config") }
},
inputs:1,
outputs:0,
@@ -286,7 +317,8 @@
<span id="node-units"></span>
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:162px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional"><br/>
<input type="checkbox" id="node-input-trim" style="display:inline-block; width:auto; vertical-align:top;"> <span data-i18n="tcpin.label.reattach"></span>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
@@ -301,12 +333,17 @@
defaults: {
name: {value:""},
server: {value:""},
port: {value:"", validate:RED.validators.regex(/^(\d*|)$/)},
port: {
value:"",
label: RED._("node-red:tcpin.label.port"),
validate:RED.validators.regex(/^(\d*|)$/)
},
out: {value:"time", required:true},
ret: {value:"buffer"},
splitc: {value:"0", required:true},
newline: {value:""},
tls: {type:"tls-config", value:'', required:false}
trim: {value:false},
tls: {type:"tls-config", value:'', required:false, label:RED._("node-red:httpin.tls-config")}
},
inputs:1,
outputs:1,

View File

@@ -88,6 +88,7 @@ module.exports = function(RED) {
this.datatype = n.datatype||'buffer'; /* buffer,utf8,base64 */
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.base64 = n.base64;
this.trim = n.trim || false;
this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server");
this.closing = false;
this.connected = false;
@@ -136,6 +137,7 @@ module.exports = function(RED) {
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i]};
if (node.trim == true) { msg.payload += node.newline; }
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@@ -230,6 +232,7 @@ module.exports = function(RED) {
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i], ip:socket.remoteAddress, port:socket.remotePort};
if (node.trim == true) { msg.payload += node.newline; }
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@@ -518,6 +521,7 @@ module.exports = function(RED) {
this.out = n.out;
this.ret = n.ret || "buffer";
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.trim = n.trim || false;
this.splitc = n.splitc;
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
@@ -653,7 +657,8 @@ module.exports = function(RED) {
let parts = chunk.split(node.newline);
for (var p=0; p<parts.length-1; p+=1) {
let m = RED.util.cloneMessage(msg);
m.payload = parts[p] + node.newline.trimEnd();
m.payload = parts[p];
if (node.trim == true) { m.payload += node.newline; }
nodeSend(m);
}
chunk = parts[parts.length-1];

View File

@@ -62,10 +62,22 @@
defaults: {
name: {value:""},
iface: {value:""},
port: {value:"",required:true,validate:RED.validators.number()},
port: {
value:"", required:true,
label:RED._("node-red:udp.label.port"),
validate:RED.validators.number(false)
},
ipv: {value:"udp4"},
multicast: {value:"false"},
group: {value:"",validate:function(v) { return (this.multicast !== "true")||v.length > 0;} },
group: {
value:"",
validate:function(v,opt) {
if ((this.multicast !== "true")||v.length > 0) {
return true;
}
return RED._("node-red:udp.errors.invalid-group");
}
},
datatype: {value:"buffer",required:true}
},
inputs:0,

View File

@@ -75,7 +75,10 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
sep: {value:',',required:true,validate:RED.validators.regex(/^.{1,2}$/)},
sep: {
value:',', required:true,
label:RED._("node-red:csv.label.separator"),
validate:RED.validators.regex(/^.{1,2}$/)},
//quo: {value:'"',required:true},
hdrin: {value:""},
hdrout: {value:"none"},

View File

@@ -31,7 +31,8 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
property: {value:"payload",required:true},
property: {value:"payload",required:true,
label:RED._("node-red:json.label.property")},
action: {value:""},
pretty: {value:false}
},

View File

@@ -26,7 +26,8 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
property: {value:"payload",required:true},
property: {value:"payload",required:true,
label:RED._("node-red:common.label.property")},
attr: {value:""},
chr: {value:""}
},

View File

@@ -15,7 +15,8 @@
category: 'parser',
color:"#DEBD5C",
defaults: {
property: {value:"payload",required:true},
property: {value:"payload",required:true,
label:RED._("node-red:common.label.property")},
name: {value:""}
},
inputs:1,

View File

@@ -202,7 +202,11 @@
name: {value:""},
mode: {value:"auto"},
build: { value:"object"},
property: { value:"payload", validate:RED.validators.typedInput("propertyType")},
property: {
value:"payload",
label: RED._("node-red:join.message-prop"),
validate:RED.validators.typedInput("propertyType", false)
},
propertyType: { value:"msg"},
key: {value:"topic"},
joiner: { value:"\\n"},

View File

@@ -47,7 +47,7 @@
<div class="form-row">
<input type="checkbox" id="node-input-allowEmptySequence" style="margin-left:20px; margin-right: 10px; vertical-align:top; width:auto;">
<label for="node-input-allowEmptySequence" style="width:auto;" data-i18n="batch.interval.empty"></label>
</div>
</div>
</div>
<div class="node-row-msg-concat">
@@ -73,9 +73,33 @@
defaults: {
name: {value:""},
mode: {value:"count"},
count: {value:10,validate:function(v) { return RED.validators.number(v) && (v >= 1); }},
overlap: {value:0,validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
interval: {value:10,validate:function(v) { return RED.validators.number(v) && (v >= 1); }},
count: {
value:10,
validate:function(v, opt) {
if (RED.validators.number(v) && (v >= 1)) {
return true;
}
return RED._("node-red:batch.error.invalid-count");
}
},
overlap: {
value:0,
validate:function(v, opt) {
if (RED.validators.number(v) && (v >= 0)) {
return true;
}
return RED._("node-red:batch.error.invalid-overlap");
}
},
interval: {
value:10,
validate:function(v, opt) {
if (RED.validators.number(v) && (v >= 1)) {
return true;
}
return RED._("node-red:batch.error.invalid-interval");
}
},
allowEmptySequence: {value:false},
topics: {value:[{topic:""}]}
},

View File

@@ -3,6 +3,7 @@
<div class="form-row node-input-filename">
<label for="node-input-filename"><i class="fa fa-file"></i> <span data-i18n="file.label.filename"></span></label>
<input id="node-input-filename" type="text">
<input type="hidden" id="node-input-filenameType">
</div>
<div class="form-row">
<label for="node-input-overwriteFile"><i class="fa fa-random"></i> <span data-i18n="file.label.action"></span></label>
@@ -29,7 +30,7 @@
</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">
<input type="text" id="node-input-name">
</div>
<div class="form-tips"><span data-i18n="file.tip"></span></div>
</script>
@@ -37,7 +38,8 @@
<script type="text/html" data-template-name="file in">
<div class="form-row">
<label for="node-input-filename"><i class="fa fa-file"></i> <span data-i18n="file.label.filename"></span></label>
<input id="node-input-filename" type="text" data-i18n="[placeholder]file.label.filename">
<input id="node-input-filename" type="text">
<input type="hidden" id="node-input-filenameType">
</div>
<div class="form-row">
<label for="node-input-format"><i class="fa fa-sign-out"></i> <span data-i18n="file.label.outputas"></span></label>
@@ -60,7 +62,7 @@
</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">
<input type="text" id="node-input-name">
</div>
<div class="form-tips"><span data-i18n="file.tip"></span></div>
</script>
@@ -196,7 +198,8 @@
category: 'storage',
defaults: {
name: {value:""},
filename: {value:""},
filename: {value:"filename"},
filenameType: {value:"msg"},
appendNewline: {value:true},
createDir: {value:false},
overwriteFile: {value:"false"},
@@ -207,10 +210,13 @@
outputs:1,
icon: "file-out.svg",
label: function() {
var fn = this.filename;
if(this.filenameType != "str" && this.filenameType != "env" ) { fn = ""; }
if(this.filenameType === "env") { fn = "env."+fn; }
if (this.overwriteFile === "delete") {
return this.name||this._("file.label.deletelabel",{file:this.filename});
return this.name||this._("file.label.deletelabel",{file:fn});
} else {
return this.name||this.filename||this._("file.label.write");
return this.name||fn||this._("file.label.write");
}
},
paletteLabel: RED._("node-red:file.label.write"),
@@ -229,6 +235,31 @@
value: "setbymsg",
label: node._("file.encoding.setbymsg")
}).text(label).appendTo(encSel);
$("#node-input-filename").typedInput({
default: "msg",
types:[{ value: "str", label:"", icon:"red/images/typedInput/az.svg"}, "msg", "jsonata", "env"],
typeField: $("#node-input-filenameType")
});
if(typeof node.filenameType == 'undefined') {
//existing node AND filenameType is not set - inplace (compatible) upgrade to new typedInput
if(node.filename == "") { //was using empty value to denote msg.filename - set typedInput to match
node.filename = "filename";
node.filenameType = "msg";
$("#node-input-filename").typedInput("type", node.filenameType);
$("#node-input-filename").typedInput("value", node.filename);
} else if(/^\${[^}]+}$/.test(node.filename)) { //was using an ${ENV_VAR}
node.filenameType = "env";
node.filename = node.filename.replace(/\${([^}]+)}/g, function(match, name) {
return (name === undefined)?"":name;
});
$("#node-input-filename").typedInput("type", node.filenameType);
$("#node-input-filename").typedInput("value", node.filename);
} else { //was using a static filename - set typedInput type to str
node.filenameType = "str";
$("#node-input-filename").typedInput("type", node.filenameType);
$("#node-input-filename").typedInput("value", node.filename);
}
}
encodings.forEach(function(item) {
if(Array.isArray(item)) {
var group = $("<optgroup/>", {
@@ -266,7 +297,8 @@
category: 'storage',
defaults: {
name: {value:""},
filename: {value:""},
filename: {value:"filename"},
filenameType: {value:"msg"},
format: {value:"utf8"},
chunk: {value:false},
sendError: {value: false},
@@ -291,7 +323,10 @@
},
icon: "file-in.svg",
label: function() {
return this.name||this.filename||this._("file.label.read");
var fn = this.filename;
if(this.filenameType != "str" && this.filenameType != "env" ) { fn = ""; }
if(this.filenameType === "env") { fn = "env."+fn; }
return this.name||fn||this._("file.label.read");
},
paletteLabel: RED._("node-red:file.label.read"),
labelStyle: function() {
@@ -305,6 +340,31 @@
value: "none",
label: label
}).text(label).appendTo(encSel);
$("#node-input-filename").typedInput({
default: "msg",
types:[{ value: "str", label:"", icon:"red/images/typedInput/az.svg"}, "msg", "jsonata", "env"],
typeField: $("#node-input-filenameType")
});
if(typeof node.filenameType == 'undefined') {
//existing node AND filenameType is not set - inplace (compatible) upgrade to new typedInput
if(node.filename == "") { //was using empty value to denote msg.filename - set typedInput to match
node.filename = "filename";
node.filenameType = "msg";
$("#node-input-filename").typedInput("type", node.filenameType);
$("#node-input-filename").typedInput("value", node.filename);
} else if(/^\${[^}]+}$/.test(node.filename)) { //was using an ${ENV_VAR}
node.filenameType = "env";
node.filename = node.filename.replace(/\${([^}]+)}/g, function(match, name) {
return (name === undefined)?"":name;
});
$("#node-input-filename").typedInput("type", node.filenameType);
$("#node-input-filename").typedInput("value", node.filename);
} else { //was using a static filename - set typedInput type to str
node.filenameType = "str";
$("#node-input-filename").typedInput("type", node.filenameType);
$("#node-input-filename").typedInput("value", node.filename);
}
}
encodings.forEach(function(item) {
if(Array.isArray(item)) {
var group = $("<optgroup/>", {

View File

@@ -39,6 +39,7 @@ module.exports = function(RED) {
// Write/delete a file
RED.nodes.createNode(this,n);
this.filename = n.filename;
this.filenameType = n.filenameType;
this.appendNewline = n.appendNewline;
this.overwriteFile = n.overwriteFile.toString();
this.createDir = n.createDir || false;
@@ -50,7 +51,28 @@ module.exports = function(RED) {
node.closeCallback = null;
function processMsg(msg,nodeSend, done) {
var filename = node.filename || msg.filename || "";
var filename = node.filename || "";
//Pre V3 compatibility - if filenameType is empty, do in place upgrade
if(typeof node.filenameType == 'undefined' || node.filenameType == "") {
//existing node AND filenameType is not set - inplace (compatible) upgrade
if(filename == "") { //was using empty value to denote msg.filename
node.filename = "filename";
node.filenameType = "msg";
} else { //was using a static filename - set typedInput type to str
node.filenameType = "str";
}
}
RED.util.evaluateNodeProperty(node.filename,node.filenameType,node,msg,(err,value) => {
if (err) {
node.error(err,msg);
return done();
} else {
filename = value;
}
});
filename = filename || "";
msg.filename = filename;
var fullFilename = filename;
if (filename && RED.settings.fileWorkingDirectory && !path.isAbsolute(filename)) {
fullFilename = path.resolve(path.join(RED.settings.fileWorkingDirectory,filename));
@@ -158,7 +180,7 @@ module.exports = function(RED) {
done();
});
}
if (node.filename) {
if (node.filenameType === "str" || node.filenameType === "env") {
// Static filename - write and reuse the stream next time
node.wstream.write(buf, function() {
nodeSend(msg);
@@ -256,6 +278,7 @@ module.exports = function(RED) {
// Read a file
RED.nodes.createNode(this,n);
this.filename = n.filename;
this.filenameType = n.filenameType;
this.format = n.format;
this.chunk = false;
this.encoding = n.encoding || "none";
@@ -270,8 +293,28 @@ module.exports = function(RED) {
var node = this;
this.on("input",function(msg, nodeSend, nodeDone) {
var filename = (node.filename || msg.filename || "").replace(/\t|\r|\n/g,'');
var filename = node.filename || "";
//Pre V3 compatibility - if filenameType is empty, do in place upgrade
if(typeof node.filenameType == 'undefined' || node.filenameType == "") {
//existing node AND filenameType is not set - inplace (compatible) upgrade
if(filename == "") { //was using empty value to denote msg.filename
node.filename = "filename";
node.filenameType = "msg";
} else { //was using a static filename - set typedInput type to str
node.filenameType = "str";
}
}
RED.util.evaluateNodeProperty(node.filename,node.filenameType,node,msg,(err,value) => {
if (err) {
node.error(err,msg);
return done();
} else {
filename = (value || "").replace(/\t|\r|\n/g,'');
}
});
filename = filename || "";
var fullFilename = filename;
var filePath = "";
if (filename && RED.settings.fileWorkingDirectory && !path.isAbsolute(filename)) {
fullFilename = path.resolve(path.join(RED.settings.fileWorkingDirectory,filename));
}

View File

@@ -36,7 +36,8 @@
category: 'storage',
defaults: {
name: {value:""},
files: {value:"",required:true},
files: {value:"",required:true,
label:RED._("node-red:watch.label.files")},
recursive: {value:""}
},
color:"BurlyWood",

View File

@@ -16,24 +16,9 @@
module.exports = function(RED) {
"use strict";
var Notify = require("fs.notify");
var fs = require("fs");
var path = require("path");
var getAllDirs = function (dir, filelist) {
filelist = filelist || [];
fs.readdirSync(dir).forEach(file => {
try {
if (fs.statSync(path.join(dir, file)).isDirectory() ) {
filelist.push(path.join(dir, file));
getAllDirs(path.join(dir, file), filelist);
}
} catch (error) {
//should we raise an error?
}
});
return filelist;
}
const watch = require('node-watch')
const fs = require("fs")
const path = require("path")
function WatchNode(n) {
RED.nodes.createNode(this,n);
@@ -44,52 +29,45 @@ module.exports = function(RED) {
this.files[f] = this.files[f].trim();
}
this.p = (this.files.length === 1) ? this.files[0] : JSON.stringify(this.files);
var node = this;
const node = this;
if (node.recursive) {
for (var fi in node.files) {
if (node.files.hasOwnProperty(fi)) {
node.files = node.files.concat(getAllDirs( node.files[fi]));
}
}
}
const watcher = watch(this.files, { recursive: this.recursive });
var notifications = new Notify(node.files);
notifications.on('change', function (file, event, fpath) {
var stat;
watcher.on('change', function (event, fpath) {
const file = path.basename(fpath)
let stat;
try {
if (fs.statSync(fpath).isDirectory()) { fpath = path.join(fpath,file); }
stat = fs.statSync(fpath);
} catch(e) { }
var type = "none";
var msg = { payload:fpath, topic:node.p, file:file, filename:fpath };
let type = "none";
const msg = {
payload:fpath,
topic:node.p,
file:file,
filename:fpath,
event: event
};
if (stat) {
if (stat.isFile()) { type = "file"; msg.size = stat.size; }
else if (stat.isBlockDevice()) { type = "blockdevice"; }
else if (stat.isCharacterDevice()) { type = "characterdevice"; }
else if (stat.isSocket()) { type = "socket"; }
else if (stat.isFIFO()) { type = "fifo"; }
else if (stat.isDirectory()) {
type = "directory";
if (node.recursive) {
notifications.add([fpath]);
notifications.add(getAllDirs(fpath));
}
}
else if (stat.isDirectory()) { type = "directory"; }
else { type = "n/a"; }
}
msg.type = type;
node.send(msg);
});
notifications.on('error', function (error, fpath) {
var msg = { payload:fpath };
watcher.on('error', function (error) {
const msg = { payload: "" };
node.error(error,msg);
});
this.close = function() {
notifications.close();
watcher.close();
}
}
RED.nodes.registerType("watch",WatchNode);
RED.nodes.registerType("watch", WatchNode);
}

View File

@@ -423,6 +423,7 @@
"string": "Ein String",
"base64": "Ein Base64-kodierter String",
"auto": "Auto-Erkennung (string oder buffer)",
"auto-detect": "Auto-Erkennung (parsed JSON-Objekt, string oder buffer)",
"json": "Ein analysiertes (parsed) JSON-Objekt"
},
"true": "wahr",

View File

@@ -29,8 +29,8 @@
<p>Create virtual wires between flows.</p>
<h3>Details</h3>
<p>This node can be configured to either send messages to all <code>link in</code>
nodes it is connected to, or to send a response back to the <code>link call</code>
node that triggered the flow.</p>
nodes it is connected to, or to send a response back to the <code>link call</code>
node that triggered the flow.</p>
<p>When in 'send to all' mode, the wires between link nodes are only displayed when
the node is selected. If there are any wires to other tabs, a virtual node
is shown that can be clicked on to jump to the appropriate tab.</p>
@@ -39,12 +39,27 @@
<script type="text/html" data-help-name="link call">
<p>Calls a flow that starts with a <code>link in</code> node and passes on the response.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt class="optional">target<span class="property-type">string</span></dt>
<dd>When the option <b>Link Type</b> is set to "Dynamic target", set <code>msg.target</code> to the name of the
<code>link in</code> node you wish to call.</dd>
</dl>
<h3>Details</h3>
<p>This node can be connected to a <code>link in</code> node that exists on any tab.
The flow connected to that node must end with a <code>link out</code> node configured
in 'return' mode.</p>
<p>When this node receives a message, it is passed to the connected <code>link in</code> node.
It then waits for a response which it then sends on.</o>
It then waits for a response which it then sends on.</p>
<p>If no response is received within the configured timeout, default 30 seconds, the node
will log an error that can be caught using the <code>catch</code> node.</p>
<p>When the option <b>Link Type</b> is set to "Dynamic target" <code>msg.target</code> can be used to call a
<code>link in</code> by name. The target <code>link in</code> node must be named.
<ul>
<li>If there are 2 <code>link in</code> nodes with the same name, an error will be raised</li>
<li>A <code>link call</code> cannot call a <code>link in</code> node inside a subflow</li>
</ul>
</p>
The flow connected to that node must end with a <code>link out</code> node configured
in 'return' mode.</p>
</script>

View File

@@ -85,7 +85,11 @@
"errors": {
"failed": "inject failed, see log for details",
"toolong": "Interval too large",
"invalid-expr": "Invalid JSONata expression: __error__"
"invalid-expr": "Invalid JSONata expression: __error__",
"invalid-jsonata": "__prop__: invalid property expression: __error__",
"invalid-prop": "__prop__: invalid property expression: __error__",
"invalid-json": "__prop__: invalid JSON data: __error__",
"invalid-repeat": "Invalid repeat value"
}
},
"catch": {
@@ -125,6 +129,7 @@
"msgprop": "message property",
"msgobj": "complete msg object",
"autostatus": "same as debug output",
"messageCount": "message count",
"to": "To",
"debtab": "debug tab",
"tabcon": "debug tab and console",
@@ -170,6 +175,11 @@
"outMode": "Mode",
"sendToAll": "Send to all connected link nodes",
"returnToCaller": "Return to calling link node",
"timeout": "timeout",
"linkCallType": "Link Type",
"staticLinkCall": "Fixed target",
"dynamicLinkCall": "Dynamic target (msg.target)",
"dynamicLinkLabel": "Dynamic",
"error": {
"missingReturn": "Missing return node information"
}
@@ -196,7 +206,9 @@
"alpnprotocol":"for use with ALPN"
},
"error": {
"missing-file": "No certificate/key file provided"
"missing-file": "No certificate/key file provided",
"invalid-cert": "Certificate not specified",
"invalid-key": "Private key not specified"
}
},
"exec": {
@@ -251,7 +263,9 @@
"moduleNameError": "Invalid module variable name: __name__",
"moduleNameReserved": "Reserved variable name: __name__",
"inputListener":"Cannot add listener to 'input' event within Function",
"non-message-returned":"Function tried to send a message of type __type__"
"non-message-returned":"Function tried to send a message of type __type__",
"invalid-js": "Error in JavaScript code",
"missing-module": "Module __module__ missing"
}
},
"template": {
@@ -305,6 +319,9 @@
"limit": "limit",
"limitTopic": "limit topic",
"random": "random",
"rate": "rate",
"random-first": "first random value",
"random-last": "last random value",
"units" : {
"second": {
"plural" : "Seconds",
@@ -325,7 +342,12 @@
}
},
"errors": {
"too-many" : "too many pending messages in delay node"
"too-many" : "too many pending messages in delay node",
"invalid-timeout": "Invalid delay value",
"invalid-rate": "Invalid rate value",
"invalid-rate-unit": "Invalid rate unit value",
"invalid-random-first": "Invalid first random value",
"invalid-random-last": "Invalid last random value"
}
},
"trigger": {
@@ -362,7 +384,9 @@
"reset": "Reset the trigger if:",
"resetMessage":"msg.reset is set",
"resetPayload":"msg.payload equals",
"resetprompt": "optional"
"resetprompt": "optional",
"duration": "duration",
"topic": "topic"
}
},
"comment": {
@@ -420,7 +444,8 @@
"action": "Action",
"staticTopic": "Subscribe to single topic",
"dynamicTopic": "Dynamic subscription",
"auto-connect": "Connect automatically"
"auto-connect": "Connect automatically",
"auto-mode-depreciated": "This option is depreciated. Please use the new auto-detect mode."
},
"sections-label":{
"birth-message": "Message sent on connection (birth message)",
@@ -451,6 +476,7 @@
"string": "a String",
"base64": "a Base64 encoded string",
"auto": "auto-detect (string or buffer)",
"auto-detect": "auto-detect (parsed JSON object, string or buffer)",
"json": "a parsed JSON object"
},
"true": "true",
@@ -465,7 +491,9 @@
"invalid-json-parse": "Failed to parse JSON string",
"invalid-action-action": "Invalid action specified",
"invalid-action-alreadyconnected": "Disconnect from broker before connecting",
"invalid-action-badsubscription": "msg.topic is missing or invalid"
"invalid-action-badsubscription": "msg.topic is missing or invalid",
"invalid-client-id": "Missing Client ID"
}
},
"httpin": {
@@ -521,7 +549,8 @@
"invalid-transport":"non-http transport requested",
"timeout-isnan": "Timeout value is not a valid number, ignoring",
"timeout-isnegative": "Timeout value is negative, ignoring",
"invalid-payload": "Invalid payload"
"invalid-payload": "Invalid payload",
"invalid-url": "Invalid url"
},
"status": {
"requesting": "requesting"
@@ -554,7 +583,9 @@
"connect-error": "An error occurred on the ws connection: ",
"send-error": "An error occurred while sending: ",
"missing-conf": "Missing server configuration",
"duplicate-path": "Cannot have two WebSocket listeners on the same path: __path__"
"duplicate-path": "Cannot have two WebSocket listeners on the same path: __path__",
"missing-server": "Missing server configuration",
"missing-client": "Missing client configuration"
}
},
"watch": {
@@ -583,7 +614,8 @@
"ms": "ms",
"chars": "chars",
"close": "Close",
"optional": "(optional)"
"optional": "(optional)",
"reattach": "re-attach delimiter"
},
"type": {
"listen": "Listen on",
@@ -624,7 +656,9 @@
"no-host": "Host and/or port not set",
"connect-timeout": "connect timeout",
"connect-fail": "connect failed",
"bad-string": "failed to convert to string"
"bad-string": "failed to convert to string",
"invalid-host": "Invalid host",
"invalid-port": "Invalid port"
}
},
"udp": {
@@ -638,7 +672,8 @@
"send": "Send a",
"toport": "to port",
"address": "Address",
"decode-base64": "Decode Base64 encoded payload?"
"decode-base64": "Decode Base64 encoded payload?",
"port": "port"
},
"placeholder": {
"interface": "(optional) local interface or address to bind to",
@@ -685,7 +720,8 @@
"port-notset": "udp: port not set",
"port-invalid": "udp: port number not valid",
"alreadyused": "udp: port __port__ already in use",
"ifnotfound": "udp: interface __iface__ not found"
"ifnotfound": "udp: interface __iface__ not found",
"invalid-group": "invalid multicast group"
}
},
"switch": {
@@ -749,7 +785,9 @@
"invalid-from": "Invalid 'from' property: __error__",
"invalid-json": "Invalid 'to' JSON property",
"invalid-expr": "Invalid JSONata expression: __error__",
"no-override": "Cannot set property of non-object type: __property__"
"no-override": "Cannot set property of non-object type: __property__",
"invalid-prop": "Invalid property expression: __property__",
"invalid-json-data": "Invalid JSON data: __error__"
}
},
"range": {
@@ -760,7 +798,11 @@
"resultrange": "to the target range",
"from": "from",
"to": "to",
"roundresult": "Round result to the nearest integer?"
"roundresult": "Round result to the nearest integer?",
"minin": "input from",
"maxin": "input to",
"minout": "target from",
"maxout": "target to"
},
"placeholder": {
"min": "e.g. 0",
@@ -985,6 +1027,7 @@
"complete": "After a message with the <code>msg.complete</code> property set",
"tip": "This mode assumes this node is either paired with a <i>split</i> node or the received messages will have a properly configured <code>msg.parts</code> property.",
"too-many": "too many pending messages in join node",
"message-prop": "message property",
"merge": {
"topics-label": "Merged Topics",
"topics": "topics",
@@ -1042,7 +1085,12 @@
},
"too-many" : "too many pending messages in batch node",
"unexpected" : "unexpected mode",
"no-parts" : "no parts property in message"
"no-parts" : "no parts property in message",
"error": {
"invalid-count": "Invalid count",
"invalid-overlap": "Invalid overlap",
"invalid-interval": "Invalid interval"
}
},
"rbe": {
"rbe": "filter",
@@ -1051,7 +1099,10 @@
"init": "Send initial value",
"start": "Start value",
"name": "Name",
"septopics": "Apply mode separately for each "
"septopics": "Apply mode separately for each ",
"gap": "value change",
"property": "property",
"topic": "topic"
},
"placeholder":{
"bandgap": "e.g. 10 or 5%",

View File

@@ -25,7 +25,7 @@
<dd>If not configured in the node, this optional property sets the HTTP method of the request.
Must be one of <code>GET</code>, <code>PUT</code>, <code>POST</code>, <code>PATCH</code> or <code>DELETE</code>.</dd>
<dt class="optional">headers <span class="property-type">object</span></dt>
<dd>Sets the HTTP headers of the request.</dd>
<dd>Sets the HTTP headers of the request. NOTE: Any headers set in the node configuration will overwrite any matching headers in <code>msg.headers</code> </dd>
<dt class="optional">cookies <span class="property-type">object</span></dt>
<dd>If set, can be used to send cookies with the request.</dd>
<dt class="optional">payload</dt>

View File

@@ -20,7 +20,9 @@
<h3>Inputs</h3>
<dl class="message-properties">
<dt class="optional">filename <span class="property-type">string</span></dt>
<dd>If not configured in the node, this optional property sets the name of the file to be updated.</dd>
<dd>The name of the file to be updated can be provided in the node configuration, or as a message property.
By default it will use <code>msg.filename</code> but this can be customised in the node.
</dd>
<dt class="optional">encoding <span class="property-type">string</span></dt>
<dd>If encoding is configured to be set by msg, then this optional property can set the encoding.</dt>
</dl>
@@ -43,7 +45,9 @@
<h3>Inputs</h3>
<dl class="message-properties">
<dt class="optional">filename <span class="property-type">string</span></dt>
<dd>if not set in the node configuration, this property sets the filename to read.</dd>
<dd>The name of the file to be read can be provided in the node configuration, or as a message property.
By default it will use <code>msg.filename</code> but this can be customised in the node.
</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">

View File

@@ -32,9 +32,21 @@
<script type="text/html" data-help-name="link call">
<p><code>link in</code> </p>
<h3>入力</h3>
<dl class="message-properties">
<dt class="optional">target<span class="property-type">文字列</span></dt>
<dd><b>リンクの種類</b>""<code>msg.target</code><code>link in</code></dd>
</dl>
<h3>詳細</h3>
<p>本ノードは任意のタブ内に存在する <code>link in</code> `` <code>link out</code> </p>
<p>本ノードはメッセージを受信するとメッセージを接続した <code>link in</code>
その後応答を待った後にメッセージを送信します</o>
<p>もし設定したタイムアウト(デフォルト30秒)以内に応答がない場合は<code>catch</code> </p>
<p><b>リンクの種類</b>""<code>link in</code><code>msg.target</code><code>link in</code>
<ul>
<li>もし同じ名前を付けた<code>link in</code>2</li>
<li><code>link call</code><code>link in</code></li>
</ul>
</p>
本ノードから呼び出すフローは終端の<code>link out</code>''</p>
</script>

View File

@@ -85,7 +85,11 @@
"errors": {
"failed": "inject処理が失敗しました。詳細はログを確認してください。",
"toolong": "時間間隔が大き過ぎます",
"invalid-expr": "JSONata式が不正: __error__"
"invalid-expr": "JSONata式が不正: __error__",
"invalid-jsonata": "__prop__: プロパティ式が不正: __error__",
"invalid-prop": "__prop__: プロパティ式が不正: __error__",
"invalid-json": "__prop__: JSONデータが不正: __error__",
"invalid-repeat": "繰り返し数が不正"
}
},
"catch": {
@@ -170,6 +174,11 @@
"outMode": "モード",
"sendToAll": "接続された全てのlinkードへ送信",
"returnToCaller": "link callードへ返却",
"timeout": "タイムアウト",
"linkCallType": "リンクの種類",
"staticLinkCall": "対象を固定で指定",
"dynamicLinkCall": "対象を動的に指定 (msg.target)",
"dynamicLinkLabel": "動的",
"error": {
"missingReturn": "返却するノードの情報が存在しません"
}
@@ -196,7 +205,9 @@
"alpnprotocol": "ALPNで使用"
},
"error": {
"missing-file": "証明書と秘密鍵のファイルが設定されていません"
"missing-file": "証明書と秘密鍵のファイルが設定されていません",
"invalid-cert": "証明書が指定されていません",
"invalid-key": "秘密鍵が指定されていません"
}
},
"exec": {
@@ -251,7 +262,9 @@
"moduleNameError": "モジュール変数名が不正です: __name__",
"moduleNameReserved": "予約された変数名です: __name__",
"inputListener": "コード内で'input'イベントのリスナを設定できません",
"non-message-returned": "Functionードが __type__ 型のメッセージ送信を試みました"
"non-message-returned": "Functionードが __type__ 型のメッセージ送信を試みました",
"invalid-js": "JavaScriptコードのエラー",
"missing-module": "モジュール __module__ が存在しません"
}
},
"template": {
@@ -305,6 +318,9 @@
"limit": "limit",
"limitTopic": "limit topic",
"random": "random",
"rate": "流量",
"random-first": "ランダム最小値",
"random-last": "ランダム最大値",
"units": {
"second": {
"plural": "秒",
@@ -325,7 +341,12 @@
}
},
"errors": {
"too-many": "delayード内で保持しているメッセージが多すぎます"
"too-many": "delayード内で保持しているメッセージが多すぎます",
"invalid-timeout": "遅延時間が不正",
"invalid-rate": "流量値が不正",
"invalid-rate-unit": "流量単位時間が不正",
"invalid-random-first": "ランダム最小値が不正",
"invalid-random-last": "ランダム最大値が不正"
}
},
"trigger": {
@@ -362,7 +383,9 @@
"reset": "初期化条件:",
"resetMessage": "msg.resetを設定",
"resetPayload": "msg.payloadが次の値",
"resetprompt": "任意"
"resetprompt": "任意",
"duration": "時間間隔",
"topic": "トピック"
}
},
"comment": {
@@ -451,6 +474,7 @@
"string": "文字列",
"base64": "Base64文字列",
"auto": "自動判定(文字列もしくはバイナリバッファ)",
"auto-detect": "自動判定(JSONオブジェクト、文字列もしくはバイナリバッファ)",
"json": "JSONオブジェクト"
},
"true": "する",
@@ -465,7 +489,8 @@
"invalid-json-parse": "JSON文字列のパースに失敗しました",
"invalid-action-action": "指定された動作が不正です",
"invalid-action-alreadyconnected": "接続する前にブローカから切断してください",
"invalid-action-badsubscription": "msg.topicが存在しないか不正です"
"invalid-action-badsubscription": "msg.topicが存在しないか不正です",
"invalid-client-id": "クライアントIDが未指定"
}
},
"httpin": {
@@ -521,7 +546,8 @@
"invalid-transport": "httpでないトランスポートが要求されました",
"timeout-isnan": "タイムアウト値が数値ではないため無視します",
"timeout-isnegative": "タイムアウト値が負数のため無視します",
"invalid-payload": "不正なペイロード"
"invalid-payload": "不正なペイロード",
"invalid-url": "URLが不正"
},
"status": {
"requesting": "要求中"
@@ -554,7 +580,9 @@
"connect-error": "ws接続でエラーが発生しました: ",
"send-error": "送信中にエラーが発生しました: ",
"missing-conf": "サーバ設定が不足しています",
"duplicate-path": "同じパスに対して2つのWebSocketリスナは指定できません: __path__"
"duplicate-path": "同じパスに対して2つのWebSocketリスナは指定できません: __path__",
"missing-server": "サーバが設定されていません",
"missing-client": "クライアントが設定されていません"
}
},
"watch": {
@@ -623,7 +651,9 @@
"no-host": "ホスト名またはポートが設定されていません",
"connect-timeout": "接続がタイムアウトしました",
"connect-fail": "接続に失敗しました",
"bad-string": "文字列への変換に失敗しました"
"bad-string": "文字列への変換に失敗しました",
"invalid-host": "ホスト名が不正",
"invalid-port": "ポートが不正"
}
},
"udp": {
@@ -637,7 +667,8 @@
"send": "送信",
"toport": "ポート",
"address": "アドレス",
"decode-base64": "Base64形式のペイロードを復号"
"decode-base64": "Base64形式のペイロードを復号",
"port": "ポート"
},
"placeholder": {
"interface": "(任意) 使用するローカルインターフェイスもしくはアドレス",
@@ -684,7 +715,8 @@
"port-notset": "udp: ポートが設定されていません",
"port-invalid": "udp: ポート番号が不正です",
"alreadyused": "udp: 既に__port__番ポートが使用されています",
"ifnotfound": "udp: インターフェイス __iface__ がありません"
"ifnotfound": "udp: インターフェイス __iface__ がありません",
"invalid-group": "マルチキャストグループが不正"
}
},
"switch": {
@@ -748,7 +780,9 @@
"invalid-from": "操作対象のプロパティが不正: __error__",
"invalid-json": "対象の値のJSONプロパティが不正",
"invalid-expr": "JSONata式が不正: __error__",
"no-override": "オブジェクト型でないプロパティを設定できません: __property__"
"no-override": "オブジェクト型でないプロパティを設定できません: __property__",
"invalid-prop": "プロパティ式が不正: __property__",
"invalid-json-data": "JSONデータが不正: __error__"
}
},
"range": {
@@ -759,7 +793,11 @@
"resultrange": "出力値の範囲",
"from": "最小値",
"to": "最大値",
"roundresult": "小数値を四捨五入し整数値へ変換"
"roundresult": "小数値を四捨五入し整数値へ変換",
"minin": "入力最小値",
"maxin": "入力最大値",
"minout": "出力最小値",
"maxout": "出力最大値"
},
"placeholder": {
"min": "例) 0",
@@ -984,7 +1022,8 @@
"complete": "<code>msg.complete</code> プロパティが設定されたメッセージ受信後",
"tip": "このモードでは、本ノードが <i>split</i> ノードと組となるか、 <code>msg.parts</code> プロパティが設定されたメッセージを受け取ることが前提となります。",
"too-many": "joinード内部で保持しているメッセージが多すぎます",
"merge": {
"message-prop": "メッセージプロパティ",
"merge": {
"topics-label": "対象トピック",
"topics": "トピック",
"topic": "トピック",
@@ -1041,7 +1080,12 @@
},
"too-many": "batchード内で保持しているメッセージが多すぎます",
"unexpected": "想定外のモード",
"no-parts": "メッセージにpartsプロパティがありません"
"no-parts": "メッセージにpartsプロパティがありません",
"error": {
"invalid-count": "メッセージ数が不正",
"invalid-overlap": "オーバラップが不正",
"invalid-interval": "時間間隔が不正"
}
},
"rbe": {
"rbe": "filter",
@@ -1050,7 +1094,10 @@
"init": "初期値を送付",
"start": "初期値",
"name": "名前",
"septopics": "個別に動作を適用"
"septopics": "個別に動作を適用",
"gap": "変化量",
"property": "プロパティ",
"topic": "トピック"
},
"placeholder": {
"bandgap": "例:10、5%",

View File

@@ -362,6 +362,7 @@
"string": "문자열",
"base64": "Base64문자열",
"auto": "자동판정(문자열혹은 바이너리버퍼)",
"auto-detect": "자동판정(JSON오브젝트, 문자열혹은 바이너리버퍼)",
"json": "JSON오브젝트"
},
"true": "한다",

View File

@@ -385,6 +385,7 @@
"string": "строка",
"base64": "строка в кодировке Base64",
"auto": "автоопределение (строка или буфер)",
"auto-detect": "автоопределение (разобрать объект JSON, строка или буфер)",
"json": "объект JSON"
},
"true": "да",

View File

@@ -382,6 +382,7 @@
"string": "字符串",
"base64": "Base64编码字符串",
"auto": "自动检测 (字符串或buffer)",
"auto-detect": "自动检测 (已解析的JSON对象、字符串或buffer)",
"json": "解析的JSON对象"
},
"true": "是",

View File

@@ -386,6 +386,7 @@
"string": "字串",
"base64": "Base64編碼字串",
"auto": "自動檢測 (字符串或buffer)",
"auto-detect": "自动检测 (已解析的JSON对象、字符串或buffer)",
"json": "解析的JSON對象"
},
"true": "是",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "2.2.2",
"version": "3.0.0-beta.1",
"license": "Apache-2.0",
"repository": {
"type": "git",
@@ -15,32 +15,32 @@
}
],
"dependencies": {
"acorn": "8.7.0",
"acorn": "8.7.1",
"acorn-walk": "8.2.0",
"ajv": "8.10.0",
"body-parser": "1.19.1",
"ajv": "8.11.0",
"body-parser": "1.20.0",
"cheerio": "1.0.0-rc.10",
"content-type": "1.0.4",
"cookie-parser": "1.4.6",
"cookie": "0.4.2",
"cookie": "0.5.0",
"cors": "2.8.5",
"cronosjs": "1.7.1",
"denque": "2.0.1",
"form-data": "4.0.0",
"fs-extra": "10.0.0",
"fs.notify": "0.0.4",
"fs-extra": "10.1.0",
"got": "11.8.3",
"hash-sum": "2.0.0",
"hpagent": "0.1.2",
"https-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.1",
"is-utf8": "0.2.1",
"js-yaml": "3.14.1",
"js-yaml": "4.1.0",
"media-typer": "1.1.0",
"mqtt": "4.3.5",
"mqtt": "4.3.7",
"multer": "1.4.4",
"mustache": "4.2.0",
"node-watch": "0.7.3",
"on-headers": "1.0.2",
"raw-body": "2.4.3",
"raw-body": "2.5.1",
"tough-cookie": "4.0.0",
"uuid": "8.3.2",
"ws": "7.5.6",