Compare commits

..

26 Commits

Author SHA1 Message Date
Nick O'Leary
56ed32e4a1 Add background to node status 2023-06-23 16:09:34 +01:00
Nick O'Leary
11f9ad8ca3 Remove debug for wiring 2023-06-23 12:29:33 +01:00
Nick O'Leary
26fc942c79 Improve wiring for horizontally aligned nodes 2023-06-23 12:24:48 +01:00
Nick O'Leary
2448e137c8 Merge pull request #4229 from node-red/4125-httpstatic-middleware
Add support for httpStatic middleware
2023-06-21 16:57:00 +01:00
Nick O'Leary
33899763ef Add support for httpStatic middleware 2023-06-21 16:47:47 +01:00
Nick O'Leary
ce679f90ee Merge pull request #4177 from k1ln/adding-timeout-to-functio-node
adding timeout attribute to function node
2023-06-21 15:57:44 +01:00
Nick O'Leary
8fb379079b Merge pull request #4225 from node-red/4196-fix-jsonata-env-var-async
Add callback to getSetting to support async jsonata access
2023-06-21 15:56:11 +01:00
Nick O'Leary
b382d048de Merge pull request #4227 from node-red/4196-add-callback-opt-to-env.get
Adds optional callback to env.get in function node
2023-06-21 15:55:40 +01:00
Nick O'Leary
4d9fcaeebf Update env.get type hint 2023-06-21 15:00:27 +01:00
Nick O'Leary
aa0225f59f Apply suggestions from code review 2023-06-21 14:27:32 +01:00
Kilian Hertel
20d2c11154 Merge branch 'dev' into adding-timeout-to-functio-node 2023-06-21 15:24:24 +02:00
Nick O'Leary
3f604e9d93 Adds optional callback to env.get in function node 2023-06-21 14:20:23 +01:00
Nick O'Leary
aafb86ef09 Merge branch 'dev' into adding-timeout-to-functio-node 2023-06-21 13:35:48 +01:00
Steve-Mcl
502dacd865 fix failure to return after calling callback 2023-06-17 22:44:55 +01:00
Steve-Mcl
31bc99cd61 remove .only 2023-06-17 22:29:39 +01:00
Steve-Mcl
5435c9ebd2 fix test (missing getUserSettings stub 🤷‍♂️) 2023-06-17 22:12:09 +01:00
Steve-Mcl
ceb9a320ba expand existing env var test for all scenarios 2023-06-17 22:11:02 +01:00
Steve-Mcl
ee8b2a0b58 Delete stray it.only 2023-06-17 22:03:59 +01:00
Steve-Mcl
8202f1b7c6 Add env var is JSONata expr test 2023-06-17 21:54:32 +01:00
Steve-Mcl
4808cac89d Add async to all paths that JSONata env var calls 2023-06-17 21:14:56 +01:00
Kilian Hertel
220a621dc6 Merge branch 'dev' into adding-timeout-to-functio-node 2023-06-02 12:20:51 +02:00
Kilian Hertel
876053f858 Merge branch 'dev' into adding-timeout-to-functio-node 2023-05-30 14:55:56 +02:00
Nick O'Leary
2f9523a586 Merge branch 'dev' into adding-timeout-to-functio-node 2023-05-25 17:43:04 +01:00
Nick O'Leary
0697c26dd1 Merge branch 'dev' into adding-timeout-to-functio-node 2023-05-25 17:33:41 +01:00
Kilian Hertel
c2812b05a4 Merge branch 'master' into adding-timeout-to-functio-node 2023-05-22 17:42:59 +02:00
Kilian Hertel
2253417459 adding timeout attribute to function node
- [x] New feature (non-breaking change which adds functionality)

Discussion here:
https://discourse.nodered.org/t/function-node-doesnt-have-timeout-feature/78483

## Proposed changes

Adding a timeout attribute to the function node, so an endless funciton doesnt break the node red server.

## Checklist

- [x] I have read the [contribution guidelines](https://github.com/node-red/node-red/blob/master/CONTRIBUTING.md)
- [x] For non-bugfix PRs, I have discussed this change on the forum/slack team.
- [x] I have run `grunt` to verify the unit tests pass
- [x] I have added suitable unit tests to cover the new/changed functionality
2023-05-22 10:16:37 +02:00
18 changed files with 520 additions and 199 deletions

View File

@@ -1033,7 +1033,7 @@ RED.view = (function() {
})
}
function generateLinkPath(origX,origY, destX, destY, sc) {
function generateLinkPath(origX,origY, destX, destY, sc, hasStatus = false) {
var dy = destY-origY;
var dx = destX-origX;
var delta = Math.sqrt(dy*dy+dx*dx);
@@ -1050,62 +1050,110 @@ RED.view = (function() {
} else {
scale = 0.4-0.2*(Math.max(0,(node_width-Math.min(Math.abs(dx),Math.abs(dy)))/node_width));
}
function genCP(cp) {
return ` M ${cp[0]-5} ${cp[1]} h 10 M ${cp[0]} ${cp[1]-5} v 10 `
}
if (dx*sc > 0) {
return "M "+origX+" "+origY+
" C "+(origX+sc*(node_width*scale))+" "+(origY+scaleY*node_height)+" "+
(destX-sc*(scale)*node_width)+" "+(destY-scaleY*node_height)+" "+
destX+" "+destY
let cp = [
[(origX+sc*(node_width*scale)), (origY+scaleY*node_height)],
[(destX-sc*(scale)*node_width), (destY-scaleY*node_height)]
]
return `M ${origX} ${origY} C ${cp[0][0]} ${cp[0][1]} ${cp[1][0]} ${cp[1][1]} ${destX} ${destY}`
// + ` ${genCP(cp[0])} ${genCP(cp[1])}`
} else {
let topX, topY, bottomX, bottomY
let cp
let midX = Math.floor(destX-dx/2);
let midY = Math.floor(destY-dy/2);
if (Math.abs(dy) < 10) {
bottomY = Math.max(origY, destY) + (hasStatus?35:25)
let startCurveHeight = bottomY - origY
let endCurveHeight = bottomY - destY
cp = [
[ origX + sc*15 , origY ],
[ origX + sc*25 , origY + 5 ],
[ origX + sc*25 , origY + startCurveHeight/2 ],
var midX = Math.floor(destX-dx/2);
var midY = Math.floor(destY-dy/2);
//
if (dy === 0) {
midY = destY + node_height;
}
var cp_height = node_height/2;
var y1 = (destY + midY)/2
var topX =origX + sc*node_width*scale;
var topY = dy>0?Math.min(y1 - dy/2 , origY+cp_height):Math.max(y1 - dy/2 , origY-cp_height);
var bottomX = destX - sc*node_width*scale;
var bottomY = dy>0?Math.max(y1, destY-cp_height):Math.min(y1, destY+cp_height);
var x1 = (origX+topX)/2;
var scy = dy>0?1:-1;
var cp = [
// Orig -> Top
[x1,origY],
[topX,dy>0?Math.max(origY, topY-cp_height):Math.min(origY, topY+cp_height)],
// Top -> Mid
// [Mirror previous cp]
[x1,dy>0?Math.min(midY, topY+cp_height):Math.max(midY, topY-cp_height)],
// Mid -> Bottom
// [Mirror previous cp]
[bottomX,dy>0?Math.max(midY, bottomY-cp_height):Math.min(midY, bottomY+cp_height)],
// Bottom -> Dest
// [Mirror previous cp]
[(destX+bottomX)/2,destY]
];
if (cp[2][1] === topY+scy*cp_height) {
if (Math.abs(dy) < cp_height*10) {
cp[1][1] = topY-scy*cp_height/2;
cp[3][1] = bottomY-scy*cp_height/2;
}
cp[2][0] = topX;
}
return "M "+origX+" "+origY+
" C "+
cp[0][0]+" "+cp[0][1]+" "+
cp[1][0]+" "+cp[1][1]+" "+
topX+" "+topY+
" S "+
cp[2][0]+" "+cp[2][1]+" "+
midX+" "+midY+
" S "+
cp[3][0]+" "+cp[3][1]+" "+
bottomX+" "+bottomY+
" S "+
[ origX + sc*25 , origY + startCurveHeight - 5 ],
[ origX + sc*15 , origY + startCurveHeight ],
[ origX , origY + startCurveHeight ],
[ destX - sc*15, origY + startCurveHeight ],
[ destX - sc*25, origY + startCurveHeight - 5 ],
[ destX - sc*25, destY + endCurveHeight/2 ],
[ destX - sc*25, destY + 5 ],
[ destX - sc*15, destY ],
[ destX, destY ],
]
return "M "+origX+" "+origY+
" C "+
cp[0][0]+" "+cp[0][1]+" "+
cp[1][0]+" "+cp[1][1]+" "+
cp[2][0]+" "+cp[2][1]+" "+
" C " +
cp[3][0]+" "+cp[3][1]+" "+
cp[4][0]+" "+cp[4][1]+" "+
destX+" "+destY
cp[5][0]+" "+cp[5][1]+" "+
" h "+dx+
" C "+
cp[6][0]+" "+cp[6][1]+" "+
cp[7][0]+" "+cp[7][1]+" "+
cp[8][0]+" "+cp[8][1]+" "+
" C " +
cp[9][0]+" "+cp[9][1]+" "+
cp[10][0]+" "+cp[10][1]+" "+
cp[11][0]+" "+cp[11][1]+" "
// +genCP(cp[0])+genCP(cp[1])+genCP(cp[2])+genCP(cp[3])+genCP(cp[4])
// +genCP(cp[5])+genCP(cp[6])+genCP(cp[7])+genCP(cp[8])+genCP(cp[9])+genCP(cp[10])
} else {
var cp_height = node_height/2;
var y1 = (destY + midY)/2
topX = origX + sc*node_width*scale;
topY = dy>0?Math.min(y1 - dy/2 , origY+cp_height):Math.max(y1 - dy/2 , origY-cp_height);
bottomX = destX - sc*node_width*scale;
bottomY = dy>0?Math.max(y1, destY-cp_height):Math.min(y1, destY+cp_height);
var x1 = (origX+topX)/2;
var scy = dy>0?1:-1;
cp = [
// Orig -> Top
[x1,origY],
[topX,dy>0?Math.max(origY, topY-cp_height):Math.min(origY, topY+cp_height)],
// Top -> Mid
// [Mirror previous cp]
[x1,dy>0?Math.min(midY, topY+cp_height):Math.max(midY, topY-cp_height)],
// Mid -> Bottom
// [Mirror previous cp]
[bottomX,dy>0?Math.max(midY, bottomY-cp_height):Math.min(midY, bottomY+cp_height)],
// Bottom -> Dest
// [Mirror previous cp]
[(destX+bottomX)/2,destY]
];
if (cp[2][1] === topY+scy*cp_height) {
if (Math.abs(dy) < cp_height*10) {
cp[1][1] = topY-scy*cp_height/2;
cp[3][1] = bottomY-scy*cp_height/2;
}
cp[2][0] = topX;
}
return "M "+origX+" "+origY+
" C "+
cp[0][0]+" "+cp[0][1]+" "+
cp[1][0]+" "+cp[1][1]+" "+
topX+" "+topY+
" S "+
cp[2][0]+" "+cp[2][1]+" "+
midX+" "+midY+
" S "+
cp[3][0]+" "+cp[3][1]+" "+
bottomX+" "+bottomY+
" S "+
cp[4][0]+" "+cp[4][1]+" "+
destX+" "+destY
// +genCP(cp[0])+genCP(cp[1])+genCP(cp[2])+genCP(cp[3])+genCP(cp[4])
}
}
}
@@ -1722,7 +1770,7 @@ RED.view = (function() {
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
var sc = (drag_line.portType === PORT_TYPE_OUTPUT)?1:-1;
drag_line.el.attr("d",generateLinkPath(drag_line.node.x+sc*drag_line.node.w/2,drag_line.node.y+portY,mousePos[0],mousePos[1],sc));
drag_line.el.attr("d",generateLinkPath(drag_line.node.x+sc*drag_line.node.w/2,drag_line.node.y+portY,mousePos[0],mousePos[1],sc, !!drag_line.node.status));
}
d3.event.preventDefault();
} else if (mouse_mode == RED.state.MOVING) {
@@ -4112,21 +4160,27 @@ RED.view = (function() {
nodeEl.__statusGroup__.style.display = "none";
} else {
nodeEl.__statusGroup__.style.display = "inline";
let backgroundWidth = 12
var fill = status_colours[d.status.fill]; // Only allow our colours for now
if (d.status.shape == null && fill == null) {
backgroundWidth = 0
nodeEl.__statusShape__.style.display = "none";
nodeEl.__statusBackground__.setAttribute("x", 17)
nodeEl.__statusGroup__.setAttribute("transform","translate(-14,"+(d.h+3)+")");
} else {
nodeEl.__statusGroup__.setAttribute("transform","translate(3,"+(d.h+3)+")");
var statusClass = "red-ui-flow-node-status-"+(d.status.shape||"dot")+"-"+d.status.fill;
nodeEl.__statusShape__.style.display = "inline";
nodeEl.__statusShape__.setAttribute("class","red-ui-flow-node-status "+statusClass);
nodeEl.__statusBackground__.setAttribute("x", 3)
}
if (d.status.hasOwnProperty('text')) {
nodeEl.__statusLabel__.textContent = d.status.text;
} else {
nodeEl.__statusLabel__.textContent = "";
}
const textSize = nodeEl.__statusLabel__.getBBox()
nodeEl.__statusBackground__.setAttribute('width', backgroundWidth + textSize.width + 6)
}
delete d.dirtyStatus;
}
@@ -4532,17 +4586,30 @@ RED.view = (function() {
statusEl.style.display = "none";
node[0][0].__statusGroup__ = statusEl;
var statusRect = document.createElementNS("http://www.w3.org/2000/svg","rect");
statusRect.setAttribute("class","red-ui-flow-node-status");
statusRect.setAttribute("x",6);
statusRect.setAttribute("y",1);
statusRect.setAttribute("width",9);
statusRect.setAttribute("height",9);
statusRect.setAttribute("rx",2);
statusRect.setAttribute("ry",2);
statusRect.setAttribute("stroke-width","3");
statusEl.appendChild(statusRect);
node[0][0].__statusShape__ = statusRect;
var statusBackground = document.createElementNS("http://www.w3.org/2000/svg","rect");
statusBackground.setAttribute("class","red-ui-flow-node-status-background");
statusBackground.setAttribute("x",3);
statusBackground.setAttribute("y",-1);
statusBackground.setAttribute("width",200);
statusBackground.setAttribute("height",13);
statusBackground.setAttribute("rx",1);
statusBackground.setAttribute("ry",1);
statusEl.appendChild(statusBackground);
node[0][0].__statusBackground__ = statusBackground;
var statusIcon = document.createElementNS("http://www.w3.org/2000/svg","rect");
statusIcon.setAttribute("class","red-ui-flow-node-status");
statusIcon.setAttribute("x",6);
statusIcon.setAttribute("y",1);
statusIcon.setAttribute("width",9);
statusIcon.setAttribute("height",9);
statusIcon.setAttribute("rx",2);
statusIcon.setAttribute("ry",2);
statusIcon.setAttribute("stroke-width","3");
statusEl.appendChild(statusIcon);
node[0][0].__statusShape__ = statusIcon;
var statusLabel = document.createElementNS("http://www.w3.org/2000/svg","text");
statusLabel.setAttribute("class","red-ui-flow-node-status-label");
@@ -5067,7 +5134,7 @@ RED.view = (function() {
// " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+
// (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+
// d.x2+" "+d.y2;
var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1);
var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1, !!(d.source.status || d.target.status));
if (/NaN/.test(path)) {
path = ""
}

View File

@@ -304,7 +304,11 @@ g.red-ui-flow-node-selected {
stroke: var(--red-ui-node-status-colors-#{"" + $current-color});
}
}
.red-ui-flow-node-status-background {
stroke: none;
fill: var(--red-ui-view-background);
fill-opacity: 0.9;
}
.red-ui-flow-node-status-label {
@include disable-selection;
stroke-width: 0;

View File

@@ -281,4 +281,21 @@ declare class env {
* ```const flowName = env.get("NR_FLOW_NAME");```
*/
static get(name:string) :any;
/**
* Get an environment variable value (asynchronous).
*
* Predefined node-red variables...
* * `NR_NODE_ID` - the ID of the node
* * `NR_NODE_NAME` - the Name of the node
* * `NR_NODE_PATH` - the Path of the node
* * `NR_GROUP_ID` - the ID of the containing group
* * `NR_GROUP_NAME` - the Name of the containing group
* * `NR_FLOW_ID` - the ID of the flow the node is on
* * `NR_FLOW_NAME` - the Name of the flow the node is on
* @param name Name of the environment variable to get
* @param callback Callback function (`(err,value) => {}`)
* @example
* ```const flowName = env.get("NR_FLOW_NAME");```
*/
static get(name:string, callback: Function) :void;
}

View File

@@ -82,6 +82,11 @@
<input id="node-input-outputs" style="width: 60px;" value="1">
</div>
<div class="form-row">
<label for="node-input-timeout"><i class="fa fa-clock"></i> <span data-i18n="function.label.timeout"></span></label>
<input id="node-input-timeout" style="width: 60px;" data-i18n="[placeholder]join.seconds">
</div>
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
<label><i class="fa fa-cubes"></i> <span data-i18n="function.label.modules"></span></label>
</div>
@@ -360,6 +365,7 @@
name: {value:"_DEFAULT_"},
func: {value:"\nreturn msg;"},
outputs: {value:1},
timeout:{value:0},
noerr: {value:0,required:true,
validate: function(v, opt) {
if (!v) {
@@ -464,6 +470,26 @@
}
});
// 4294967 is max in node.js timeout.
$( "#node-input-timeout" ).spinner({
min: 0,
max: 4294967,
change: function(event, ui) {
var value = this.value;
if(value == ""){
value = 0;
}
else
{
value = parseInt(value);
}
value = isNaN(value) ? 1 : value;
value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
if (value !== this.value) { $(this).spinner("value", value); }
}
});
var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs, offset) {
var editor = RED.editor.createEditor({
id: id,
@@ -503,7 +529,7 @@
editor:this.editor, // the field name the main text body goes to
mode:"ace/mode/nrjavascript",
fields:[
'name', 'outputs',
'name', 'outputs', 'timeout',
{
name: 'initialize',
get: function() {

View File

@@ -96,6 +96,13 @@ module.exports = function(RED) {
node.name = n.name;
node.func = n.func;
node.outputs = n.outputs;
node.timeout = n.timeout*1000;
if(node.timeout>0){
node.timeoutOptions = {
timeout:node.timeout,
breakOnSigint:true
}
}
node.ini = n.initialize ? n.initialize.trim() : "";
node.fin = n.finalize ? n.finalize.trim() : "";
node.libs = n.libs || [];
@@ -235,8 +242,8 @@ module.exports = function(RED) {
}
},
env: {
get: function(envVar) {
return RED.util.getSetting(node, envVar);
get: function(envVar, callback) {
return RED.util.getSetting(node, envVar, node._flow, callback);
}
},
setTimeout: function () {
@@ -362,6 +369,10 @@ module.exports = function(RED) {
})(__initSend__);`;
iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt);
if(node.timeout>0){
iniOpt.timeout = node.timeout;
iniOpt.breakOnSigint = true;
}
}
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
@@ -385,6 +396,10 @@ module.exports = function(RED) {
})();`;
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
if(node.timeout>0){
finOpt.timeout = node.timeout;
finOpt.breakOnSigint = true;
}
}
var promise = Promise.resolve();
if (iniScript) {
@@ -396,9 +411,12 @@ module.exports = function(RED) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
node.script.runInContext(context);
context.__done__ = done;
var opts = {};
if (node.timeout>0){
opts = node.timeoutOptions;
}
node.script.runInContext(context,opts);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {

View File

@@ -23,8 +23,6 @@ module.exports = async function(RED) {
const { v4: uuid } = require('uuid');
const crypto = require('crypto');
const URL = require("url").URL
const http = require("http")
const https = require("https")
var mustache = require("mustache");
var querystring = require("querystring");
var cookie = require("cookie");
@@ -67,27 +65,16 @@ in your Node-RED user directory (${RED.settings.userDir}).
function HTTPRequest(n) {
RED.nodes.createNode(this,n);
checkNodeAgentPatch();
const node = this;
const nodeUrl = n.url;
const isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
const nodeMethod = n.method || "GET";
let paytoqs = false;
let paytobody = false;
let redirectList = [];
const sendErrorsToCatch = n.senderr;
var node = this;
var nodeUrl = n.url;
var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
var nodeMethod = n.method || "GET";
var paytoqs = false;
var paytobody = false;
var redirectList = [];
var sendErrorsToCatch = n.senderr;
node.headers = n.headers || [];
const useKeepAlive = n["persist"];
let agents = null
if (useKeepAlive) {
agents = {
http: new http.Agent({ keepAlive: true }),
https: new https.Agent({ keepAlive: true })
}
node.on('close', function () {
agents.http.destroy()
agents.https.destroy()
})
}
var nodeHTTPPersistent = n["persist"];
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
}
@@ -573,14 +560,12 @@ in your Node-RED user directory (${RED.settings.userDir}).
opts.agent = {
http: new HttpProxyAgent(proxyOptions),
https: new HttpsProxyAgent(proxyOptions)
}
};
} else {
node.warn("Bad proxy url: "+ prox);
}
}
if (useKeepAlive && !opts.agent) {
opts.agent = agents
}
if (tlsNode) {
opts.https = {};
tlsNode.addTLSOptions(opts.https);

View File

@@ -216,7 +216,8 @@
"initialize": "Start",
"finalize": "Stopp",
"outputs": "Ausgänge",
"modules": "Module"
"modules": "Module",
"timeout": "Timeout"
},
"text": {
"initialize": "// Der Code hier wird ausgeführt,\n// wenn der Node gestartet wird\n",

View File

@@ -252,7 +252,8 @@
"initialize": "On Start",
"finalize": "On Stop",
"outputs": "Outputs",
"modules": "Modules"
"modules": "Modules",
"timeout": "Timeout"
},
"text": {
"initialize": "// Code added here will be run once\n// whenever the node is started.\n",

View File

@@ -212,7 +212,8 @@
"function": "Функция",
"initialize": "Настройка",
"finalize": "Закрытие",
"outputs": "Выходы"
"outputs": "Выходы",
"timeout":"Время ожидания"
},
"text": {
"initialize": "// Добавленный здесь код будет исполняться\n// однократно при развертывании узла.\n",

View File

@@ -416,23 +416,50 @@ class Flow {
return this.activeNodes;
}
/*!
* Get value of environment variable defined in group node.
* @param {String} group - group node
* @param {String} name - name of variable
* @return {Object} object containing the value in val property or null if not defined
/**
* Group callback signature
*
* @callback GroupEnvCallback
* @param {Error} err The error object (or null)
* @param {[result: {val:Any}, name: String]} result The result of the callback
* @returns {void}
*/
getGroupEnvSetting(node, group, name) {
/**
* @function getGroupEnvSetting
* Get a group setting value synchronously.
* This currently automatically defers to the parent
* @overload
* @param {Object} node
* @param {Object} group
* @param {String} name
* @returns {Any}
*
* Get a group setting value asynchronously.
* @overload
* @param {Object} node
* @param {Object} group
* @param {String} name
* @param {GroupEnvCallback} callback
* @returns {void}
*/
getGroupEnvSetting(node, group, name, callback) {
/** @type {GroupEnvCallback} */
const returnOrCallback = (err, [result, newName]) => {
if (callback) {
callback(err, [result, newName]);
return
}
return [result, newName];
}
if (group) {
if (name === "NR_GROUP_NAME") {
return [{
val: group.name
}, null];
return returnOrCallback(null, [{ val: group.name }, null]);
}
if (name === "NR_GROUP_ID") {
return [{
val: group.id
}, null];
return returnOrCallback(null, [{ val: group.id }, null]);
}
if (group.credentials === undefined) {
@@ -457,33 +484,32 @@ class Flow {
if (env) {
let value = env.value;
const type = env.type;
if ((type !== "env") ||
(value !== name)) {
if ((type !== "env") || (value !== name)) {
if (type === "env") {
value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}");
}
if (type === "bool") {
const val
= ((value === "true") ||
(value === true));
return [{
val: val
}, null];
} else if (type === "bool") {
const val = ((value === "true") || (value === true));
return returnOrCallback(null, [{ val: val }, null])
}
if (type === "cred") {
return [{
val: value
}, null];
return returnOrCallback(null, [{ val: value }, null])
}
try {
var val = redUtil.evaluateNodeProperty(value, type, node, null, null);
return [{
val: val
}, null];
if (!callback) {
var val = redUtil.evaluateNodeProperty(value, type, node, null, null);
return [{ val: val }, null];
} else {
redUtil.evaluateNodeProperty(value, type, node, null, (err, value) => {
return returnOrCallback(err, [{ val: value }, null])
});
return
}
}
catch (e) {
this.error(e);
return [null, null];
if (!callback) {
this.error(e);
}
return returnOrCallback(e, null);
}
}
}
@@ -494,27 +520,47 @@ class Flow {
}
if (group.g) {
const parent = this.getGroupNode(group.g);
return this.getGroupEnvSetting(node, parent, name);
const gVal = this.getGroupEnvSetting(node, parent, name, callback);
if (callback) {
return;
}
return gVal;
}
}
return [null, name];
return returnOrCallback(null, [null, name]);
}
/**
* Settings callback signature
*
* @callback SettingsCallback
* @param {Error} err The error object (or null)
* @param {Any} result The result of the callback
* @returns {void}
*/
/**
* Get a flow setting value. This currently automatically defers to the parent
* flow which, as defined in ./index.js returns `process.env[key]`.
* This lays the groundwork for Subflow to have instance-specific settings
* @param {[type]} key [description]
* @return {[type]} [description]
* @param {String} key The settings key
* @param {SettingsCallback} callback Optional callback function
* @return {Any}
*/
getSetting(key) {
getSetting(key, callback) {
/** @type {SettingsCallback} */
const returnOrCallback = (err, result) => {
if (callback) {
callback(err, result);
return
}
return result;
}
const flow = this.flow;
if (key === "NR_FLOW_NAME") {
return flow.label;
return returnOrCallback(null, flow.label);
}
if (key === "NR_FLOW_ID") {
return flow.id;
return returnOrCallback(null, flow.id);
}
if (flow.credentials === undefined) {
flow.credentials = credentials.get(flow.id) || {};
@@ -544,15 +590,14 @@ class Flow {
}
try {
if (type === "bool") {
const val = ((value === "true") ||
(value === true));
return val;
const val = ((value === "true") || (value === true));
return returnOrCallback(null, val);
}
if (type === "cred") {
return value;
return returnOrCallback(null, value);
}
var val = redUtil.evaluateNodeProperty(value, type, null, null, null);
return val;
return returnOrCallback(null, val);
}
catch (e) {
this.error(e);
@@ -564,7 +609,11 @@ class Flow {
key = key.substring(8);
}
}
return this.parent.getSetting(key);
const pVal = this.parent.getSetting(key, callback);
if (callback) {
return;
}
return pVal;
}
/**

View File

@@ -780,7 +780,7 @@ const flowAPI = {
getNode: getNode,
handleError: () => false,
handleStatus: () => false,
getSetting: k => flowUtil.getEnvVar(k),
getSetting: (k, callback) => flowUtil.getEnvVar(k, callback),
log: m => log.log(m)
}

View File

@@ -308,16 +308,34 @@ module.exports = {
runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true);
}
},
getEnvVar: function(k) {
if (!envVarExcludes[k]) {
const item = getGlobalEnv(k);
/**
* Get the value of an environment variable
* Call with a callback to get the value asynchronously
* or without to get the value synchronously
* @param {String} key The name of the environment variable
* @param {(err: Error, val: Any)} [callback] Optional callback for asynchronous call
* @returns {Any | void} The value of the environment variable or undefined if not found
*/
getEnvVar: function(key, callback) {
const returnOrCallback = function(err, val) {
if (callback) {
callback(err, val);
return;
}
return val;
}
if (!envVarExcludes[key]) {
const item = getGlobalEnv(key);
if (item) {
const val = redUtil.evaluateNodeProperty(item.value, item.type, null, null, null);
const val = redUtil.evaluateNodeProperty(item.value, item.type, null, null, callback);
if (callback) {
return;
}
return val;
}
return process.env[k];
return returnOrCallback(null, process.env[key]);
}
return undefined;
return returnOrCallback(undefined);
},
diffNodes: diffNodes,
mapEnvVarProperties: mapEnvVarProperties,

View File

@@ -18,6 +18,7 @@
/**
* @mixin @node-red/util_util
*/
/** @typedef {import('../../runtime/lib/flows/Flow.js').Flow} RuntimeLibFlowsFlow */
const clonedeep = require("lodash.clonedeep");
const jsonata = require("jsonata");
@@ -526,37 +527,68 @@ function setObjectProperty(msg,prop,value,createMissing) {
return true;
}
/*!
/**
* Get value of environment variable.
* @param {Node} node - accessing node
* @param {String} name - name of variable
* @param {RuntimeLibFlowsFlow} flow_ - (optional) flow to check for setting
* @param {(err: Error, result: Any) => void} callback - (optional) called when the property is evaluated
* @return {String} value of env var
*/
function getSetting(node, name, flow_) {
function getSetting(node, name, flow_, callback) {
const returnOrCallback = (err, result) => {
if (callback) {
callback(err, result);
return
}
return result;
}
if (node) {
if (name === "NR_NODE_NAME") {
return node.name;
return returnOrCallback(null, node.name);
}
if (name === "NR_NODE_ID") {
return node.id;
return returnOrCallback(null, node.id);
}
if (name === "NR_NODE_PATH") {
return node._path;
return returnOrCallback(null, node._path);
}
}
/** @type {RuntimeLibFlowsFlow} */
var flow = (flow_ ? flow_ : (node ? node._flow : null));
if (flow) {
if (node && node.g) {
const group = flow.getGroupNode(node.g);
const [result, newName] = flow.getGroupEnvSetting(node, group, name);
if (result) {
return result.val;
if (callback) {
flow.getGroupEnvSetting(node, group, name, (e, [result, newName]) => {
if (e) {
callback(e);
return
}
if (result) {
callback(null, result.val);
return
}
name = newName;
flow.getSetting(name, callback);
});
return
} else {
const [result, newName] = flow.getGroupEnvSetting(node, group, name);
if (result) {
return result.val;
}
name = newName;
}
name = newName;
}
return flow.getSetting(name);
const fVal = flow.getSetting(name, callback)
if (callback) {
return
}
return fVal;
}
return process.env[name];
return returnOrCallback(null, process.env[name]);
}
@@ -568,19 +600,34 @@ function getSetting(node, name, flow_) {
* will return `Hello Joe!`.
* @param {String} value - the string to parse
* @param {Node} node - the node evaluating the property
* @param {(err: Error, result: Any) => void} callback - (optional) called when the property is evaluated
* @return {String} The parsed string
* @memberof @node-red/util_util
*/
function evaluateEnvProperty(value, node) {
function evaluateEnvProperty(value, node, callback) {
const returnOrCallback = (err, result) => {
if (callback) {
callback(err, result);
return
}
return result;
}
/** @type {RuntimeLibFlowsFlow} */
var flow = (node && hasOwnProperty.call(node, "_flow")) ? node._flow : null;
var result;
if (/^\${[^}]+}$/.test(value)) {
// ${ENV_VAR}
var name = value.substring(2,value.length-1);
result = getSetting(node, name, flow);
result = getSetting(node, name, flow, callback);
if (callback) {
return
}
} else if (!/\${\S+}/.test(value)) {
// ENV_VAR
result = getSetting(node, value, flow);
result = getSetting(node, value, flow, callback);
if (callback) {
return
}
} else {
// FOO${ENV_VAR}BAR
return value.replace(/\${([^}]+)}/g, function(match, name) {
@@ -588,8 +635,7 @@ function evaluateEnvProperty(value, node) {
return (val === undefined)?"":val;
});
}
return (result === undefined)?"":result;
return returnOrCallback(null, (result === undefined)?"":result);
}
@@ -677,7 +723,10 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
return
}
} else if (type === 'env') {
result = evaluateEnvProperty(value, node);
result = evaluateEnvProperty(value, node, callback);
if (callback) {
return
}
}
if (callback) {
callback(null,result);

View File

@@ -302,7 +302,7 @@ httpsPromise.then(function(startupHttps) {
settings.httpNodeAuth = settings.httpNodeAuth || settings.httpAuth;
}
if(settings.httpStatic) {
if (settings.httpStatic) {
settings.httpStaticRoot = formatRoot(settings.httpStaticRoot || "/");
const statics = Array.isArray(settings.httpStatic) ? settings.httpStatic : [settings.httpStatic];
const sanitised = [];
@@ -414,13 +414,7 @@ httpsPromise.then(function(startupHttps) {
if (settings.httpNodeRoot !== false) {
app.use(settings.httpNodeRoot,RED.httpNode);
}
// if (settings.httpStatic) {
// settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth;
// if (settings.httpStaticAuth) {
// app.use("/",basicAuthMiddleware(settings.httpStaticAuth.user,settings.httpStaticAuth.pass));
// }
// app.use("/",express.static(settings.httpStatic));
// }
if (settings.httpStatic) {
let appUseMem = {};
for (let si = 0; si < settings.httpStatic.length; si++) {
@@ -428,6 +422,7 @@ httpsPromise.then(function(startupHttps) {
const filePath = sp.path;
const thisRoot = sp.root || "/";
const options = sp.options;
const middleware = sp.middleware;
if(appUseMem[filePath + "::" + thisRoot]) {
continue;// this path and root already registered!
}
@@ -435,6 +430,9 @@ httpsPromise.then(function(startupHttps) {
if (settings.httpStaticAuth) {
app.use(thisRoot, basicAuthMiddleware(settings.httpStaticAuth.user, settings.httpStaticAuth.pass));
}
if (middleware) {
app.use(thisRoot, middleware)
}
app.use(thisRoot, express.static(filePath, options));
}
}

View File

@@ -44,4 +44,30 @@ describe('unknown Node', function() {
});
});
it('should evaluate a global environment variable that is a JSONata value', function (done) {
const flow = [{
id: "n1", type: "global-config", name: "XYZ",
env: [
{ name: "now-var", type: "jsonata", value: "$millis()" }
]
},
{ id: "n2", type: "inject", topic: "t1", payload: "now-var", payloadType: "env", wires: [["n3"]], z: "flow" },
{ id: "n3", type: "helper" }
];
helper.load([config, inject], flow, function () {
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
n3.on("input", (msg) => {
try {
const now = Date.now();
msg.should.have.property("payload").and.be.approximately(now, 1000);
done();
} catch (err) {
done(err);
}
});
n2.receive({});
});
});
});

View File

@@ -1424,7 +1424,30 @@ describe('function node', function() {
});
});
it('should timeout if timeout is set', function(done) {
var flow = [{id:"n1",type:"function",wires:[["n2"]],timeout:"0.010",func:"while(1==1){};\nreturn msg;"}];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
n1.receive({payload:"foo",topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "function";
});
logEvents.should.have.length(1);
var msg = logEvents[0][0];
msg.should.have.property('level', helper.log().ERROR);
msg.should.have.property('id', 'n1');
msg.should.have.property('type', 'function');
should.equal(msg.msg.message, 'Script execution timed out after 10ms');
done();
} catch(err) {
done(err);
}
},50);
});
});
describe("finalize function", function() {

View File

@@ -686,7 +686,7 @@ describe('Flow', function() {
},50);
});
it.only("passes a status event to the group scoped status node",function(done) {
it("passes a status event to the group scoped status node",function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id: "g1", type: "group", g: "g3" },
@@ -1311,33 +1311,42 @@ describe('Flow', function() {
})
process.env.V0 = "gv0";
process.env.V1 = "gv1";
process.env.V3 = "gv3";
var config = flowUtils.parseConfig([
{id:"t1",type:"tab",env:[
{"name": "V0", value: "v0", type: "str"}
{"name": "V0", value: "t1v0", type: "str"},
{"name": "V2", value: "t1v2", type: "str"}
]},
{id:"g1",type:"group",z:"t1",env:[
{"name": "V0", value: "v1", type: "str"},
{"name": "V1", value: "v2", type: "str"}
{"name": "V0", value: "g1v0", type: "str"},
{"name": "V1", value: "g1v1", type: "str"}
]},
{id:"g2",type:"group",z:"t1",g:"g1",env:[
{"name": "V1", value: "v3", type: "str"}
{"name": "V1", value: "g2v1", type: "str"}
]},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"$(V0)",wires:[]},
{id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]},
{id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]},
{id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"$(V1)",wires:[]},
{id:"5",x:10,y:10,z:"t1",type:"test",foo:"$(V1)",wires:[]},
{id:"t1__V0",x:10,y:10,z:"t1",type:"test",foo:"${V0}",wires:[]}, // V0 will come from tab env V0
{id:"t1g1V0",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${V0}",wires:[]}, // V0 will come from group 1 env V0
{id:"t1g1V1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${V1}",wires:[]}, // V1 will come from group 1 env V1
{id:"t1g2V0",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V0}",wires:[]}, // V0 will come from group 1 env V0
{id:"t1g2V1",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V1}",wires:[]}, // V1 will come from group 2 env V1
{id:"t1g2V2",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V2}",wires:[]}, // V2 will come from tab 1 env V2
{id:"t1g2V3",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V3}",wires:[]}, // V3 will come from process env V3
{id:"t1__V1",x:10,y:10,z:"t1",type:"test",foo:"${V1}",wires:[]},
]);
var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
activeNodes["1"].foo.should.equal("v0");
activeNodes["2"].foo.should.equal("v1");
activeNodes["3"].foo.should.equal("v2");
activeNodes["4"].foo.should.equal("v3");
activeNodes["5"].foo.should.equal("gv1");
activeNodes.t1__V0.foo.should.equal("t1v0"); // node in tab 1, get tab 1 env V0
activeNodes.t1__V1.foo.should.equal("gv1"); // node in tab 1, get V1, (tab 1 no V1) --> parent (global has V1)
activeNodes.t1g1V0.foo.should.equal("g1v0"); // node in group 1, get V0, (group 1 has V0)
activeNodes.t1g1V1.foo.should.equal("g1v1"); // node in group 1, get V1, (group 1 has V1)
activeNodes.t1g2V0.foo.should.equal("g1v0"); // node in group 2, get V0, (group 2 no V0) --> parent (group 1 has V0)
activeNodes.t1g2V1.foo.should.equal("g2v1"); // node in group 2, get V1, (group 2 has V1)
activeNodes.t1g2V2.foo.should.equal("t1v2"); // node in group 2, get V2, (group 2 no V2) --> parent (tab 1 has V2)
activeNodes.t1g2V3.foo.should.equal("gv3"); // node in group 2, get V3, (group 2 no V3) --> parent (tab 1 no V2) --> parent (global has V3)
flow.stop().then(function() {
done();
@@ -1347,7 +1356,6 @@ describe('Flow', function() {
console.log(e.stack);
done(e);
}
});
it("can access environment variable property using $parent", function (done) {
try {
@@ -1393,7 +1401,6 @@ describe('Flow', function() {
console.log(e.stack);
done(e);
}
});
it("can define environment variable using JSONata", function (done) {
@@ -1427,9 +1434,40 @@ describe('Flow', function() {
console.log(e.stack);
done(e);
}
});
it("can access global environment variables defined as JSONata values", function (done) {
try {
after(function() {
delete process.env.V0;
})
var config = flowUtils.parseConfig([
{id:"t1",type:"tab",env:[
{"name": "V0", value: "1+2", type: "jsonata"}
]},
{id:"g1",type:"group",z:"t1",env:[
{"name": "V1", value: "2+3", type: "jsonata"},
]},
{id:"1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]},
{id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]},
]);
var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
activeNodes["1"].foo.should.equal(3);
activeNodes["2"].foo.should.equal(5);
flow.stop().then(function() {
done();
});
}
catch (e) {
console.log(e.stack);
done(e);
}
});
});
});

View File

@@ -489,7 +489,7 @@ describe('storage/localfilesystem', function() {
var rootdir = path.win32.resolve(userDir+'/some');
// make it into a local UNC path
flowFile = flowFile.replace('C:\\', '\\\\localhost\\c$\\');
localfilesystem.init({userDir:userDir, flowFile:flowFile}, mockRuntime).then(function() {
localfilesystem.init({userDir:userDir, flowFile:flowFile, getUserSettings: () => {{}}}, mockRuntime).then(function() {
fs.existsSync(flowFile).should.be.false();
localfilesystem.saveFlows(testFlow).then(function() {
fs.existsSync(flowFile).should.be.true();