From 51a0b68d8e26cc629c5b05e6ea283253e42c5015 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 22 Jun 2023 10:17:48 +0100 Subject: [PATCH 1/8] Revert "Add callback to getSetting to support async jsonata access" --- .../src/types/node-red/func.d.ts | 17 --- .../nodes/core/function/10-function.js | 4 +- .../@node-red/runtime/lib/flows/Flow.js | 139 ++++++------------ .../@node-red/runtime/lib/flows/index.js | 2 +- .../@node-red/runtime/lib/flows/util.js | 30 +--- .../node_modules/@node-red/util/lib/util.js | 83 +++-------- .../core/common/91-global-config_spec.js | 26 ---- .../@node-red/runtime/lib/flows/Flow_spec.js | 74 +++------- .../lib/storage/localfilesystem/index_spec.js | 2 +- 9 files changed, 90 insertions(+), 287 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts b/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts index 8544439d9..59f8a8bd7 100644 --- a/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts +++ b/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts @@ -281,21 +281,4 @@ 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; } diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index a6ede375f..662b7fa47 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -242,8 +242,8 @@ module.exports = function(RED) { } }, env: { - get: function(envVar, callback) { - return RED.util.getSetting(node, envVar, node._flow, callback); + get: function(envVar) { + return RED.util.getSetting(node, envVar); } }, setTimeout: function () { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 2e1e97c4b..489ec42cb 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -416,50 +416,23 @@ class Flow { return this.activeNodes; } - - /** - * 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} + /*! + * 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 */ - - /** - * @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]; - } + getGroupEnvSetting(node, group, name) { if (group) { if (name === "NR_GROUP_NAME") { - return returnOrCallback(null, [{ val: group.name }, null]); + return [{ + val: group.name + }, null]; } if (name === "NR_GROUP_ID") { - return returnOrCallback(null, [{ val: group.id }, null]); + return [{ + val: group.id + }, null]; } if (group.credentials === undefined) { @@ -484,32 +457,33 @@ 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+"}"); - } else if (type === "bool") { - const val = ((value === "true") || (value === true)); - return returnOrCallback(null, [{ val: val }, null]) + } + if (type === "bool") { + const val + = ((value === "true") || + (value === true)); + return [{ + val: val + }, null]; } if (type === "cred") { - return returnOrCallback(null, [{ val: value }, null]) + return [{ + val: value + }, null]; } try { - 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 - } + var val = redUtil.evaluateNodeProperty(value, type, node, null, null); + return [{ + val: val + }, null]; } catch (e) { - if (!callback) { - this.error(e); - } - return returnOrCallback(e, null); + this.error(e); + return [null, null]; } } } @@ -520,47 +494,27 @@ class Flow { } if (group.g) { const parent = this.getGroupNode(group.g); - const gVal = this.getGroupEnvSetting(node, parent, name, callback); - if (callback) { - return; - } - return gVal; + return this.getGroupEnvSetting(node, parent, name); } } - return returnOrCallback(null, [null, name]); + return [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 {String} key The settings key - * @param {SettingsCallback} callback Optional callback function - * @return {Any} + * @param {[type]} key [description] + * @return {[type]} [description] */ - getSetting(key, callback) { - /** @type {SettingsCallback} */ - const returnOrCallback = (err, result) => { - if (callback) { - callback(err, result); - return - } - return result; - } + getSetting(key) { const flow = this.flow; if (key === "NR_FLOW_NAME") { - return returnOrCallback(null, flow.label); + return flow.label; } if (key === "NR_FLOW_ID") { - return returnOrCallback(null, flow.id); + return flow.id; } if (flow.credentials === undefined) { flow.credentials = credentials.get(flow.id) || {}; @@ -590,14 +544,15 @@ class Flow { } try { if (type === "bool") { - const val = ((value === "true") || (value === true)); - return returnOrCallback(null, val); + const val = ((value === "true") || + (value === true)); + return val; } if (type === "cred") { - return returnOrCallback(null, value); + return value; } var val = redUtil.evaluateNodeProperty(value, type, null, null, null); - return returnOrCallback(null, val); + return val; } catch (e) { this.error(e); @@ -609,11 +564,7 @@ class Flow { key = key.substring(8); } } - const pVal = this.parent.getSetting(key, callback); - if (callback) { - return; - } - return pVal; + return this.parent.getSetting(key); } /** diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index 9c6133ea5..e18861f17 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -780,7 +780,7 @@ const flowAPI = { getNode: getNode, handleError: () => false, handleStatus: () => false, - getSetting: (k, callback) => flowUtil.getEnvVar(k, callback), + getSetting: k => flowUtil.getEnvVar(k), log: m => log.log(m) } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index 39ff820b4..868657c7b 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -308,34 +308,16 @@ module.exports = { runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true); } }, - /** - * 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); + getEnvVar: function(k) { + if (!envVarExcludes[k]) { + const item = getGlobalEnv(k); if (item) { - const val = redUtil.evaluateNodeProperty(item.value, item.type, null, null, callback); - if (callback) { - return; - } + const val = redUtil.evaluateNodeProperty(item.value, item.type, null, null, null); return val; } - return returnOrCallback(null, process.env[key]); + return process.env[k]; } - return returnOrCallback(undefined); + return undefined; }, diffNodes: diffNodes, mapEnvVarProperties: mapEnvVarProperties, diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 9f711f4fa..ad05d45f1 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -18,7 +18,6 @@ /** * @mixin @node-red/util_util */ -/** @typedef {import('../../runtime/lib/flows/Flow.js').Flow} RuntimeLibFlowsFlow */ const clonedeep = require("lodash.clonedeep"); const jsonata = require("jsonata"); @@ -527,68 +526,37 @@ 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_, callback) { - const returnOrCallback = (err, result) => { - if (callback) { - callback(err, result); - return - } - return result; - } +function getSetting(node, name, flow_) { if (node) { if (name === "NR_NODE_NAME") { - return returnOrCallback(null, node.name); + return node.name; } if (name === "NR_NODE_ID") { - return returnOrCallback(null, node.id); + return node.id; } if (name === "NR_NODE_PATH") { - return returnOrCallback(null, node._path); + return node._path; } } - - /** @type {RuntimeLibFlowsFlow} */ var flow = (flow_ ? flow_ : (node ? node._flow : null)); if (flow) { if (node && node.g) { const group = flow.getGroupNode(node.g); - 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; + const [result, newName] = flow.getGroupEnvSetting(node, group, name); + if (result) { + return result.val; } + name = newName; } - const fVal = flow.getSetting(name, callback) - if (callback) { - return - } - return fVal; + return flow.getSetting(name); } - return returnOrCallback(null, process.env[name]); + return process.env[name]; } @@ -600,34 +568,19 @@ function getSetting(node, name, flow_, callback) { * 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, callback) { - const returnOrCallback = (err, result) => { - if (callback) { - callback(err, result); - return - } - return result; - } - /** @type {RuntimeLibFlowsFlow} */ +function evaluateEnvProperty(value, node) { 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, callback); - if (callback) { - return - } + result = getSetting(node, name, flow); } else if (!/\${\S+}/.test(value)) { // ENV_VAR - result = getSetting(node, value, flow, callback); - if (callback) { - return - } + result = getSetting(node, value, flow); } else { // FOO${ENV_VAR}BAR return value.replace(/\${([^}]+)}/g, function(match, name) { @@ -635,7 +588,8 @@ function evaluateEnvProperty(value, node, callback) { return (val === undefined)?"":val; }); } - return returnOrCallback(null, (result === undefined)?"":result); + return (result === undefined)?"":result; + } @@ -723,10 +677,7 @@ function evaluateNodeProperty(value, type, node, msg, callback) { return } } else if (type === 'env') { - result = evaluateEnvProperty(value, node, callback); - if (callback) { - return - } + result = evaluateEnvProperty(value, node); } if (callback) { callback(null,result); diff --git a/test/nodes/core/common/91-global-config_spec.js b/test/nodes/core/common/91-global-config_spec.js index 29dd905ae..8cc5658cf 100644 --- a/test/nodes/core/common/91-global-config_spec.js +++ b/test/nodes/core/common/91-global-config_spec.js @@ -44,30 +44,4 @@ 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({}); - }); - }); - }); diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index 9f99830b0..6d53747ef 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -686,7 +686,7 @@ describe('Flow', function() { },50); }); - it("passes a status event to the group scoped status node",function(done) { + it.only("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,42 +1311,33 @@ 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: "t1v0", type: "str"}, - {"name": "V2", value: "t1v2", type: "str"} + {"name": "V0", value: "v0", type: "str"} ]}, {id:"g1",type:"group",z:"t1",env:[ - {"name": "V0", value: "g1v0", type: "str"}, - {"name": "V1", value: "g1v1", type: "str"} + {"name": "V0", value: "v1", type: "str"}, + {"name": "V1", value: "v2", type: "str"} ]}, {id:"g2",type:"group",z:"t1",g:"g1",env:[ - {"name": "V1", value: "g2v1", type: "str"} + {"name": "V1", value: "v3", type: "str"} ]}, - {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:[]}, + {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:[]}, ]); var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); flow.start(); var activeNodes = flow.getActiveNodes(); - 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) + 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"); flow.stop().then(function() { done(); @@ -1356,6 +1347,7 @@ describe('Flow', function() { console.log(e.stack); done(e); } + }); it("can access environment variable property using $parent", function (done) { try { @@ -1401,6 +1393,7 @@ describe('Flow', function() { console.log(e.stack); done(e); } + }); it("can define environment variable using JSONata", function (done) { @@ -1434,40 +1427,9 @@ 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); - } - }); }); }); diff --git a/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js b/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js index 00fab11cf..65826c9f4 100644 --- a/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js +++ b/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js @@ -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, getUserSettings: () => {{}}}, mockRuntime).then(function() { + localfilesystem.init({userDir:userDir, flowFile:flowFile}, mockRuntime).then(function() { fs.existsSync(flowFile).should.be.false(); localfilesystem.saveFlows(testFlow).then(function() { fs.existsSync(flowFile).should.be.true(); From 8db2972288d5967903c7c1d9315497bcbab6811e Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 22 Jun 2023 10:24:29 +0100 Subject: [PATCH 2/8] Restore expended env var tests --- .../core/common/91-global-config_spec.js | 28 ++++++- .../@node-red/runtime/lib/flows/Flow_spec.js | 74 ++++++++++++++----- .../lib/storage/localfilesystem/index_spec.js | 2 +- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/test/nodes/core/common/91-global-config_spec.js b/test/nodes/core/common/91-global-config_spec.js index 8cc5658cf..e6f5393e1 100644 --- a/test/nodes/core/common/91-global-config_spec.js +++ b/test/nodes/core/common/91-global-config_spec.js @@ -3,7 +3,7 @@ var config = require("nr-test-utils").require("@node-red/nodes/core/common/91-gl var inject = require("nr-test-utils").require("@node-red/nodes/core/common/20-inject.js"); var helper = require("node-red-node-test-helper"); -describe('unknown Node', function() { +describe('Global Config Node', function() { afterEach(function() { helper.unload(); @@ -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({}); + }); + }); + }); diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index 6d53747ef..9f99830b0 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -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); + } + }); }); }); diff --git a/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js b/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js index 65826c9f4..00fab11cf 100644 --- a/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js +++ b/test/unit/@node-red/runtime/lib/storage/localfilesystem/index_spec.js @@ -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(); From 1c5fdb6ab6d625af398e365cf8e036cf6bf2cbb1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 23 Jun 2023 02:11:57 +0100 Subject: [PATCH 3/8] Evaluate all env vars as part of async flow start --- package.json | 2 +- .../@node-red/runtime/lib/flows/Flow.js | 214 +--- .../@node-red/runtime/lib/flows/Group.js | 55 + .../@node-red/runtime/lib/flows/Subflow.js | 71 +- .../@node-red/runtime/lib/flows/index.js | 2 +- .../@node-red/runtime/lib/flows/util.js | 285 +++-- .../node_modules/@node-red/util/lib/util.js | 6 +- test/node_modules/nr-test-utils/index.js | 6 +- test/nodes/core/common/20-inject_spec.js | 67 +- test/nodes/core/function/15-change_spec.js | 16 +- test/nodes/subflow/subflow_spec.js | 53 +- .../@node-red/runtime/lib/flows/Flow_spec.js | 1109 ++++++++--------- .../@node-red/runtime/lib/flows/Group_spec.js | 48 + .../runtime/lib/flows/Subflow_spec.js | 461 +++---- .../@node-red/runtime/lib/flows/index_spec.js | 22 +- .../@node-red/runtime/lib/flows/util_spec.js | 14 +- 16 files changed, 1141 insertions(+), 1290 deletions(-) create mode 100644 packages/node_modules/@node-red/runtime/lib/flows/Group.js create mode 100644 test/unit/@node-red/runtime/lib/flows/Group_spec.js diff --git a/package.json b/package.json index 986c88375..8f367b842 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "mermaid": "^9.4.3", "minami": "1.2.3", "mocha": "9.2.2", - "node-red-node-test-helper": "^0.3.1", + "node-red-node-test-helper": "^0.3.2", "nodemon": "2.0.20", "proxy": "^1.0.2", "sass": "1.62.1", diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 489ec42cb..2e03f5165 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -14,19 +14,20 @@ * limitations under the License. **/ -var clone = require("clone"); -var redUtil = require("@node-red/util").util; +const clone = require("clone"); +const redUtil = require("@node-red/util").util; const events = require("@node-red/util").events; -var flowUtil = require("./util"); +const flowUtil = require("./util"); const context = require('../nodes/context'); const hooks = require("@node-red/util").hooks; const credentials = require("../nodes/credentials"); -var Subflow; -var Log; +let Subflow; +let Log; +let Group; -var nodeCloseTimeout = 15000; -var asyncMessageDelivery = true; +let nodeCloseTimeout = 15000; +let asyncMessageDelivery = true; /** * This class represents a flow within the runtime. It is responsible for @@ -52,6 +53,27 @@ class Flow { this.isGlobalFlow = false; } this.id = this.flow.id || "global"; + + // Initialise the group objects. These must be done in the right order + // starting from outer-most to inner-most so that the parent hierarchy + // is maintained. + this.groups = {} + this.groupOrder = [] + const groupIds = Object.keys(this.flow.groups || {}) + while (groupIds.length > 0) { + const id = groupIds.shift() + const groupDef = this.flow.groups[id] + if (!groupDef.g || this.groups[groupDef.g]) { + // The parent of this group is available - either another group + // or the top-level flow (this) + const parent = this.groups[groupDef.g] || this + this.groups[groupDef.id] = new Group(parent, groupDef) + this.groupOrder.push(groupDef.id) + } else { + // Try again once we've processed the other groups + groupIds.push(id) + } + } this.activeNodes = {}; this.subflowInstanceNodes = {}; this.catchNodes = []; @@ -59,6 +81,11 @@ class Flow { this.path = this.id; // Ensure a context exists for this flow this.context = context.getFlowContext(this.id,this.parent.id); + + // env is an array of env definitions + // _env is an object for direct lookup of env name -> value + this.env = this.flow.env + this._env = {} } /** @@ -136,7 +163,7 @@ class Flow { * @param {[type]} msg [description] * @return {[type]} [description] */ - start(diff) { + async start(diff) { this.trace("start "+this.TYPE+" ["+this.path+"]"); var node; var newNode; @@ -145,6 +172,18 @@ class Flow { this.statusNodes = []; this.completeNodeMap = {}; + if (this.env) { + this._env = await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) + // console.log('env', this.env) + // console.log('_env', this._env) + } + for (let i = 0; i < this.groupOrder.length; i++) { + // Start the groups in the right order so they + // can setup their env vars knowning their parent + // will have been started + await this.groups[this.groupOrder[i]].start() + } + var configNodes = Object.keys(this.flow.configs); var configNodeAttempts = {}; while (configNodes.length > 0) { @@ -177,7 +216,7 @@ class Flow { } } if (readyToCreate) { - newNode = flowUtil.createNode(this,node); + newNode = await flowUtil.createNode(this,node); if (newNode) { this.activeNodes[id] = newNode; } @@ -203,7 +242,7 @@ class Flow { if (node.d !== true) { if (!node.subflow) { if (!this.activeNodes[id]) { - newNode = flowUtil.createNode(this,node); + newNode = await flowUtil.createNode(this,node); if (newNode) { this.activeNodes[id] = newNode; } @@ -221,7 +260,7 @@ class Flow { node ); this.subflowInstanceNodes[id] = subflow; - subflow.start(); + await subflow.start(); this.activeNodes[id] = subflow.node; // this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id}); @@ -404,8 +443,7 @@ class Flow { * @return {Node} group node */ getGroupNode(id) { - const groups = this.global.groups; - return groups[id]; + return this.groups[id]; } /** @@ -416,95 +454,8 @@ 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 - */ - getGroupEnvSetting(node, group, name) { - if (group) { - if (name === "NR_GROUP_NAME") { - return [{ - val: group.name - }, null]; - } - if (name === "NR_GROUP_ID") { - return [{ - val: group.id - }, null]; - } - - if (group.credentials === undefined) { - group.credentials = credentials.get(group.id) || {}; - } - if (!name.startsWith("$parent.")) { - if (group.env) { - if (!group._env) { - const envs = group.env; - const entries = envs.map((env) => { - if (env.type === "cred") { - const cred = group.credentials; - if (cred.hasOwnProperty(env.name)) { - env.value = cred[env.name]; - } - } - return [env.name, env]; - }); - group._env = Object.fromEntries(entries); - } - const env = group._env[name]; - if (env) { - let value = env.value; - const type = env.type; - 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]; - } - if (type === "cred") { - return [{ - val: value - }, null]; - } - try { - var val = redUtil.evaluateNodeProperty(value, type, node, null, null); - return [{ - val: val - }, null]; - } - catch (e) { - this.error(e); - return [null, null]; - } - } - } - } - } - else { - name = name.substring(8); - } - if (group.g) { - const parent = this.getGroupNode(group.g); - return this.getGroupEnvSetting(node, parent, name); - } - } - return [null, name]; - } - - /** - * 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 + * Get a flow setting value. * @param {[type]} key [description] * @return {[type]} [description] */ @@ -516,54 +467,14 @@ class Flow { if (key === "NR_FLOW_ID") { return flow.id; } - if (flow.credentials === undefined) { - flow.credentials = credentials.get(flow.id) || {}; - } - if (flow.env) { - if (!key.startsWith("$parent.")) { - if (!flow._env) { - const envs = flow.env; - const entries = envs.map((env) => { - if (env.type === "cred") { - const cred = flow.credentials; - if (cred.hasOwnProperty(env.name)) { - env.value = cred[env.name]; - } - } - return [env.name, env] - }); - flow._env = Object.fromEntries(entries); - } - const env = flow._env[key]; - if (env) { - let value = env.value; - const type = env.type; - if ((type !== "env") || (value !== key)) { - if (type === "env") { - value = value.replace(new RegExp("\\${"+key+"}","g"),"${$parent."+key+"}"); - } - try { - if (type === "bool") { - const val = ((value === "true") || - (value === true)); - return val; - } - if (type === "cred") { - return value; - } - var val = redUtil.evaluateNodeProperty(value, type, null, null, null); - return val; - } - catch (e) { - this.error(e); - } - } - } + if (!key.startsWith("$parent.")) { + if (this._env.hasOwnProperty(key)) { + return this._env[key] } - else { + } else { key = key.substring(8); - } } + // Delegate to the parent flow. return this.parent.getSetting(key); } @@ -618,10 +529,10 @@ class Flow { let distance = 0 if (reportingNode.g) { // Reporting node inside a group. Calculate the distance between it and the status node - let containingGroup = this.global.groups[reportingNode.g] + let containingGroup = this.groups[reportingNode.g] while (containingGroup && containingGroup.id !== targetStatusNode.g) { distance++ - containingGroup = this.global.groups[containingGroup.g] + containingGroup = this.groups[containingGroup.g] } if (!containingGroup && targetStatusNode.g && targetStatusNode.scope === 'group') { // This status node is in a group, but not in the same hierachy @@ -706,10 +617,10 @@ class Flow { let distance = 0 if (reportingNode.g) { // Reporting node inside a group. Calculate the distance between it and the catch node - let containingGroup = this.global.groups[reportingNode.g] + let containingGroup = this.groups[reportingNode.g] while (containingGroup && containingGroup.id !== targetCatchNode.g) { distance++ - containingGroup = this.global.groups[containingGroup.g] + containingGroup = this.groups[containingGroup.g] } if (!containingGroup && targetCatchNode.g && targetCatchNode.scope === 'group') { // This catch node is in a group, but not in the same hierachy @@ -909,9 +820,10 @@ module.exports = { asyncMessageDelivery = !runtime.settings.runtimeSyncDelivery Log = runtime.log; Subflow = require("./Subflow"); + Group = require("./Group").Group }, create: function(parent,global,conf) { - return new Flow(parent,global,conf); + return new Flow(parent,global,conf) }, Flow: Flow } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Group.js b/packages/node_modules/@node-red/runtime/lib/flows/Group.js new file mode 100644 index 000000000..dc05211a1 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/flows/Group.js @@ -0,0 +1,55 @@ +const flowUtil = require("./util"); +const credentials = require("../nodes/credentials"); + +/** + * This class represents a group within the runtime. + */ +class Group { + + /** + * Create a Group object. + * @param {[type]} parent The parent flow/group + * @param {[type]} groupDef This group's definition + */ + constructor(parent, groupDef) { + this.TYPE = 'group' + this.name = groupDef.name + this.parent = parent + this.group = groupDef + this.id = this.group.id + this.g = this.group.g + this.env = this.group.env + this._env = {} + } + + async start() { + if (this.env) { + this._env = await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) + } + } + /** + * Get a group setting value. + * @param {[type]} key [description] + * @return {[type]} [description] + */ + getSetting(key) { + if (key === "NR_GROUP_NAME") { + return this.name; + } + if (key === "NR_GROUP_ID") { + return this.id; + } + if (!key.startsWith("$parent.")) { + if (this._env.hasOwnProperty(key)) { + return this._env[key] + } + } else { + key = key.substring(8); + } + return this.parent.getSetting(key); + } +} + +module.exports = { + Group +} diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js index 78f0c7550..4588e3d9b 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js @@ -119,7 +119,7 @@ class Subflow extends Flow { this.templateCredentials = credentials.get(subflowDef.id) || {}; this.instanceCredentials = credentials.get(id) || {}; - var env = []; + var env = {}; if (this.subflowDef.env) { this.subflowDef.env.forEach(e => { env[e.name] = e; @@ -145,7 +145,7 @@ class Subflow extends Flow { } }); } - this.env = env; + this.env = Object.values(env); } /** @@ -156,7 +156,7 @@ class Subflow extends Flow { * @param {[type]} diff [description] * @return {[type]} [description] */ - start(diff) { + async start(diff) { var self = this; // Create a subflow node to accept inbound messages and route appropriately var Node = require("../nodes/Node"); @@ -310,7 +310,7 @@ class Subflow extends Flow { } } } - super.start(diff); + return super.start(diff); } /** @@ -335,68 +335,35 @@ class Subflow extends Flow { } /** * Get environment variable of subflow - * @param {String} name name of env var + * @param {String} key name of env var * @return {Object} val value of env var */ - getSetting(name) { - if (!/^\$parent\./.test(name)) { - var env = this.env; - if (env && env.hasOwnProperty(name)) { - var val = env[name]; - // If this is an env type property we need to be careful not - // to get into lookup loops. - // 1. if the value to lookup is the same as this one, go straight to parent - // 2. otherwise, check if it is a compound env var ("foo $(bar)") - // and if so, substitute any instances of `name` with $parent.name - // See https://github.com/node-red/node-red/issues/2099 - if (val.type !== 'env' || val.value !== name) { - let value = val.value; - var type = val.type; - if (type === 'env') { - value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); - } - try { - return evaluateInputValue(value, type, this.node); - } - catch (e) { - this.error(e); - return undefined; - } - } else { - // This _is_ an env property pointing at itself - go to parent - } - } - } else { - // name starts $parent. ... so delegate to parent automatically - name = name.substring(8); - } + getSetting(key) { const node = this.subflowInstance; if (node) { - if (name === "NR_NODE_NAME") { + if (key === "NR_NODE_NAME") { return node.name; } - if (name === "NR_NODE_ID") { + if (key === "NR_NODE_ID") { return node.id; } - if (name === "NR_NODE_PATH") { + if (key === "NR_NODE_PATH") { return node._path; } } - if (node.g) { - const group = this.getGroupNode(node.g); - const [result, newName] = this.getGroupEnvSetting(node, group, name); - if (result) { - return result.val; + if (!key.startsWith("$parent.")) { + if (this._env.hasOwnProperty(key)) { + return this._env[key] } - name = newName; + } else { + key = key.substring(8); } - - var parent = this.parent; - if (parent) { - var val = parent.getSetting(name); - return val; + // Push the request up to the parent. + // Unlike a Flow, the parent of a Subflow could be a Group + if (node.g) { + return this.parent.getGroupNode(node.g).getSetting(key) } - return undefined; + return this.parent.getSetting(key) } /** diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index e18861f17..c121e469c 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -391,7 +391,7 @@ async function start(type,diff,muteLog,isDeploy) { for (id in activeFlows) { if (activeFlows.hasOwnProperty(id)) { try { - activeFlows[id].start(diff); + await activeFlows[id].start(diff); // Create a map of node id to flow id and also a subflowInstance lookup map var activeNodes = activeFlows[id].getActiveNodes(); Object.keys(activeNodes).forEach(function(nid) { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index 868657c7b..85c1381b3 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -70,8 +70,58 @@ function mapEnvVarProperties(obj,prop,flow,config) { } } +async function evaluateEnvProperties(flow, env, credentials) { + const pendingEvaluations = [] + const evaluatedEnv = {} + const envTypes = [] + for (let i = 0; i < env.length; i++) { + let { name, value, type } = env[i] + if (type === "env") { + // Do env types last as they may include references to other env vars + // at this level which need to be resolved before they can be looked-up + envTypes.push(env[i]) + } else if (type === "bool") { + value = (value === "true") || (value === true); + } else if (type === "cred") { + if (credentials.hasOwnProperty(name)) { + value = credentials[name]; + } + } else if (type ==='jsonata') { + pendingEvaluations.push(new Promise((resolve, _) => { + redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => { + if (!err) { + evaluatedEnv[name] = result + } + resolve() + }); + })) + } else { + value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); + } + evaluatedEnv[name] = value + } + if (pendingEvaluations.length > 0) { + await Promise.all(pendingEvaluations) + } + for (let i = 0; i < envTypes.length; i++) { + let { name, value, type } = envTypes[i] + // If an env-var wants to lookup itself, delegate straight to the parent + // https://github.com/node-red/node-red/issues/2099 + if (value === name) { + value = `$parent.${name}` + } + if (evaluatedEnv.hasOwnProperty(value)) { + value = evaluatedEnv[value] + } else { + value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); + } + evaluatedEnv[name] = value + } -function createNode(flow,config) { + return evaluatedEnv +} + +async function createNode(flow,config) { var newNode = null; var type = config.type; try { @@ -140,7 +190,7 @@ function createNode(flow,config) { // This allows nodes inside the subflow to get ahold of each other // such as a node accessing its config node flow.subflowInstanceNodes[config.id] = subflow - subflow.start(); + await subflow.start(); return subflow.node; } } catch(err) { @@ -150,123 +200,122 @@ function createNode(flow,config) { } function parseConfig(config) { - var flow = {}; - flow.allNodes = {}; - flow.subflows = {}; - flow.configs = {}; - flow.flows = {}; - flow.groups = {}; - flow.missingTypes = []; + var flow = {}; + flow.allNodes = {}; + flow.subflows = {}; + flow.configs = {}; + flow.flows = {}; + flow.missingTypes = []; - config.forEach(function(n) { - flow.allNodes[n.id] = clone(n); - if (n.type === 'tab') { - flow.flows[n.id] = n; - flow.flows[n.id].subflows = {}; - flow.flows[n.id].configs = {}; - flow.flows[n.id].nodes = {}; - } - if (n.type === 'group') { - flow.groups[n.id] = n; - } - }); + config.forEach(function (n) { + flow.allNodes[n.id] = clone(n); + if (n.type === 'tab') { + flow.flows[n.id] = n; + flow.flows[n.id].subflows = {}; + flow.flows[n.id].configs = {}; + flow.flows[n.id].nodes = {}; + flow.flows[n.id].groups = {}; + } else if (n.type === 'subflow') { + flow.subflows[n.id] = n; + flow.subflows[n.id].configs = {}; + flow.subflows[n.id].nodes = {}; + flow.subflows[n.id].groups = {}; + flow.subflows[n.id].instances = []; + } + }); - // TODO: why a separate forEach? this can be merged with above - config.forEach(function(n) { - if (n.type === 'subflow') { - flow.subflows[n.id] = n; - flow.subflows[n.id].configs = {}; - flow.subflows[n.id].nodes = {}; - flow.subflows[n.id].instances = []; - } - }); - var linkWires = {}; - var linkOutNodes = []; - config.forEach(function(n) { - if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { - var subflowDetails = subflowInstanceRE.exec(n.type); + var linkWires = {}; + var linkOutNodes = []; + config.forEach(function (n) { + if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { + var subflowDetails = subflowInstanceRE.exec(n.type); - if ( (subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type)) ) { - if (flow.missingTypes.indexOf(n.type) === -1) { - flow.missingTypes.push(n.type); - } - } - var container = null; - if (flow.flows[n.z]) { - container = flow.flows[n.z]; - } else if (flow.subflows[n.z]) { - container = flow.subflows[n.z]; - } - if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { - if (subflowDetails) { - var subflowType = subflowDetails[1] - n.subflow = subflowType; - if (flow.subflows[subflowType]) { - flow.subflows[subflowType].instances.push(n) - } - } - if (container) { - container.nodes[n.id] = n; - } - } else { - if (container) { - container.configs[n.id] = n; - } else { - flow.configs[n.id] = n; - flow.configs[n.id]._users = []; - } - } - if (n.type === 'link in' && n.links) { - // Ensure wires are present in corresponding link out nodes - n.links.forEach(function(id) { - linkWires[id] = linkWires[id]||{}; - linkWires[id][n.id] = true; - }) - } else if (n.type === 'link out' && n.links) { - linkWires[n.id] = linkWires[n.id]||{}; - n.links.forEach(function(id) { - linkWires[n.id][id] = true; - }) - linkOutNodes.push(n); - } - } - }); - linkOutNodes.forEach(function(n) { - var links = linkWires[n.id]; - var targets = Object.keys(links); - n.wires = [targets]; - }); + if ((subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type))) { + if (flow.missingTypes.indexOf(n.type) === -1) { + flow.missingTypes.push(n.type); + } + } + var container = null; + if (flow.flows[n.z]) { + container = flow.flows[n.z]; + } else if (flow.subflows[n.z]) { + container = flow.subflows[n.z]; + } + if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { + if (subflowDetails) { + var subflowType = subflowDetails[1] + n.subflow = subflowType; + if (flow.subflows[subflowType]) { + flow.subflows[subflowType].instances.push(n) + } + } + if (container) { + container.nodes[n.id] = n; + } + } else { + if (container) { + container.configs[n.id] = n; + } else { + flow.configs[n.id] = n; + flow.configs[n.id]._users = []; + } + } + if (n.type === 'link in' && n.links) { + // Ensure wires are present in corresponding link out nodes + n.links.forEach(function (id) { + linkWires[id] = linkWires[id] || {}; + linkWires[id][n.id] = true; + }) + } else if (n.type === 'link out' && n.links) { + linkWires[n.id] = linkWires[n.id] || {}; + n.links.forEach(function (id) { + linkWires[n.id][id] = true; + }) + linkOutNodes.push(n); + } + } else if (n.type === 'group') { + const parentContainer = flow.flows[n.z] || flow.subflows[n.z] + if (parentContainer) { + parentContainer.groups[n.id] = n + } + } + }); + linkOutNodes.forEach(function (n) { + var links = linkWires[n.id]; + var targets = Object.keys(links); + n.wires = [targets]; + }); - var addedTabs = {}; - config.forEach(function(n) { - if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { - for (var prop in n) { - if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) { - // This property references a global config node - flow.configs[n[prop]]._users.push(n.id) - } - } - if (n.z && !flow.subflows[n.z]) { + var addedTabs = {}; + config.forEach(function (n) { + if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { + for (var prop in n) { + if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) { + // This property references a global config node + flow.configs[n[prop]]._users.push(n.id) + } + } + if (n.z && !flow.subflows[n.z]) { - if (!flow.flows[n.z]) { - flow.flows[n.z] = {type:'tab',id:n.z}; - flow.flows[n.z].subflows = {}; - flow.flows[n.z].configs = {}; - flow.flows[n.z].nodes = {}; - addedTabs[n.z] = flow.flows[n.z]; - } - if (addedTabs[n.z]) { - if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { - addedTabs[n.z].nodes[n.id] = n; - } else { - addedTabs[n.z].configs[n.id] = n; - } - } - } - } - }); - return flow; + if (!flow.flows[n.z]) { + flow.flows[n.z] = { type: 'tab', id: n.z }; + flow.flows[n.z].subflows = {}; + flow.flows[n.z].configs = {}; + flow.flows[n.z].nodes = {}; + addedTabs[n.z] = flow.flows[n.z]; + } + if (addedTabs[n.z]) { + if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { + addedTabs[n.z].nodes[n.id] = n; + } else { + addedTabs[n.z].configs[n.id] = n; + } + } + } + } + }); + return flow; } function getGlobalEnv(name) { @@ -319,10 +368,11 @@ module.exports = { } return undefined; }, - diffNodes: diffNodes, - mapEnvVarProperties: mapEnvVarProperties, - parseConfig: parseConfig, + diffNodes, + mapEnvVarProperties, + evaluateEnvProperties, + parseConfig, diffConfigs: function(oldConfig, newConfig) { var id; @@ -615,5 +665,6 @@ module.exports = { * @param {object} config The node configuration object * @return {Node} The instance of the node */ - createNode: createNode + createNode: createNode, + evaluateEnvProperties } diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index ad05d45f1..4ae0032b0 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -548,11 +548,9 @@ function getSetting(node, name, flow_) { 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 (group) { + return group.getSetting(name) } - name = newName; } return flow.getSetting(name); } diff --git a/test/node_modules/nr-test-utils/index.js b/test/node_modules/nr-test-utils/index.js index 56bc369e6..08a0b3d89 100644 --- a/test/node_modules/nr-test-utils/index.js +++ b/test/node_modules/nr-test-utils/index.js @@ -16,7 +16,6 @@ const path = require("path"); -const fs = require("fs"); const PACKAGE_ROOT = "../../../packages/node_modules"; @@ -27,5 +26,10 @@ module.exports = { }, resolve: function(file) { return path.resolve(path.join(__dirname,PACKAGE_ROOT,file)); + }, + sleep: async (time) => { + return new Promise(resolve => { + setTimeout(resolve, time) + }) } } diff --git a/test/nodes/core/common/20-inject_spec.js b/test/nodes/core/common/20-inject_spec.js index 3e9a35391..056ca9693 100644 --- a/test/nodes/core/common/20-inject_spec.js +++ b/test/nodes/core/common/20-inject_spec.js @@ -22,7 +22,9 @@ var helper = require("node-red-node-test-helper"); describe('inject node', function() { beforeEach(function(done) { - helper.startServer(done); + helper.startServer(() => { + done() + }); }); function initContext(done) { @@ -41,7 +43,7 @@ describe('inject node', function() { }); } - afterEach(function(done) { + afterEach(async function() { helper.unload().then(function () { return Context.clean({allNodes: {}}); }).then(function () { @@ -53,8 +55,11 @@ describe('inject node', function() { function basicTest(type, val, rval) { it('inject value ('+type+')', function (done) { - var flow = [{id: "n1", type: "inject", topic: "t1", payload: val, payloadType: type, wires: [["n2"]], z: "flow"}, - {id: "n2", type: "helper"}]; + var flow = [ + {id:'flow', type:'tab'}, + {id: "n1", type: "inject", topic: "t1", payload: val, payloadType: type, wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper", z:'flow'} + ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -93,6 +98,7 @@ describe('inject node', function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function (msg) { + delete process.env.NR_TEST try { msg.should.have.property("topic", "t1"); msg.should.have.property("payload", "foo"); @@ -101,13 +107,13 @@ describe('inject node', function() { done(err); } }); - process.env.NR_TEST = 'foo'; + process.env.NR_TEST = 'foo'; n1.receive({}); }); }); it('inject name of node as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_NODE_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -125,7 +131,7 @@ describe('inject node', function() { }); it('inject id of node as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_NODE_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -143,7 +149,7 @@ describe('inject node', function() { }); it('inject path of node as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_PATH", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_NODE_PATH", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -162,7 +168,7 @@ describe('inject node', function() { it('inject name of flow as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_FLOW_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_FLOW_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", label: "FLOW" }, ]; @@ -182,7 +188,7 @@ describe('inject node', function() { }); it('inject id of flow as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_FLOW_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_FLOW_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", name: "FLOW" }, ]; @@ -202,9 +208,10 @@ describe('inject node', function() { }); it('inject name of group as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_GROUP_NAME", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_GROUP_NAME", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -222,9 +229,10 @@ describe('inject node', function() { }); it('inject id of group as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_GROUP_ID", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_GROUP_ID", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -243,8 +251,9 @@ describe('inject node', function() { it('inject name of node as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, - {id: "n2", type: "helper"}]; + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_NODE_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper", z: "flow"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -261,7 +270,7 @@ describe('inject node', function() { }); it('inject id of node as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_NODE_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -279,7 +288,7 @@ describe('inject node', function() { }); it('inject path of node as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_PATH}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_NODE_PATH}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -298,7 +307,7 @@ describe('inject node', function() { it('inject name of flow as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_FLOW_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_FLOW_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", label: "FLOW" }, ]; @@ -318,7 +327,7 @@ describe('inject node', function() { }); it('inject id of flow as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_FLOW_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_FLOW_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", name: "FLOW" }, ]; @@ -338,9 +347,10 @@ describe('inject node', function() { }); it('inject name of group as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_GROUP_NAME}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_GROUP_NAME}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -358,9 +368,10 @@ describe('inject node', function() { }); it('inject id of group as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_GROUP_ID}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_GROUP_ID}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); diff --git a/test/nodes/core/function/15-change_spec.js b/test/nodes/core/function/15-change_spec.js index 66f02d6d1..b23c1994a 100644 --- a/test/nodes/core/function/15-change_spec.js +++ b/test/nodes/core/function/15-change_spec.js @@ -568,11 +568,12 @@ describe('change Node', function() { it('sets the value using env property from group', function(done) { var flow = [ + {"id": "flow", type:"tab"}, {"id":"group1","type":"group","env":[ {"name":"NR_TEST_A", "value":"bar", "type": "str"} - ]}, - {"id":"changeNode1","type":"change","g":"group1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, - {id:"helperNode1", type:"helper", wires:[]} + ], z: "flow"}, + {"id":"changeNode1","type":"change","g":"group1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]], z: "flow"}, + {id:"helperNode1", type:"helper", wires:[], z: "flow"} ]; helper.load(changeNode, flow, function() { var changeNode1 = helper.getNode("changeNode1"); @@ -591,12 +592,13 @@ describe('change Node', function() { it('sets the value using env property from nested group', function(done) { var flow = [ + {"id": "flow", type:"tab"}, {"id":"group1","type":"group","env":[ {"name":"NR_TEST_A", "value":"bar", "type": "str"} - ]}, - {"id":"group2","type":"group","g":"group1","env":[]}, - {"id":"changeNode1","type":"change","g":"group2",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, - {id:"helperNode1", type:"helper", wires:[]} + ], z: "flow"}, + {"id":"group2","type":"group","g":"group1","env":[], z: "flow"}, + {"id":"changeNode1","type":"change","g":"group2",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]], z: "flow"}, + {id:"helperNode1", type:"helper", wires:[], z: "flow"} ]; helper.load(changeNode, flow, function() { var changeNode1 = helper.getNode("changeNode1"); diff --git a/test/nodes/subflow/subflow_spec.js b/test/nodes/subflow/subflow_spec.js index f4b8257d8..e25b45102 100644 --- a/test/nodes/subflow/subflow_spec.js +++ b/test/nodes/subflow/subflow_spec.js @@ -253,35 +253,32 @@ describe('subflow', function() { it('should access typed value of env var', function(done) { var flow = [ - {id:"t0", type:"tab", label:"", disabled:false, info:""}, - {id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", - env: [ - {name: "KN", type: "num", value: "100"}, - {name: "KB", type: "bool", value: "true"}, - {name: "KJ", type: "json", value: "[1,2,3]"}, - {name: "Kb", type: "bin", value: "[65,65]"}, - {name: "Ke", type: "env", value: "KS"}, - {name: "Kj", type: "jsonata", value: "1+2"}, - ], - wires:[["n2"]]}, - {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, - // Subflow - {id:"s1", type:"subflow", name:"Subflow", info:"", - in:[{ - x:10, y:10, - wires:[ {id:"s1-n1"} ] - }], - out:[{ - x:10, y:10, - wires:[ {id:"s1-n1", port:0} ] - }], - env: [ - {name: "KS", type: "str", value: "STR"} - ] + { id: "t0", type: "tab", label: "", disabled: false, info: "" }, + { + id: "n1", x: 10, y: 10, z: "t0", type: "subflow:s1", + env: [ + { name: "KN", type: "num", value: "100" }, + { name: "KB", type: "bool", value: "true" }, + { name: "KJ", type: "json", value: "[1,2,3]" }, + { name: "Kb", type: "bin", value: "[65,65]" }, + { name: "Ke", type: "env", value: "KS" }, + { name: "Kj", type: "jsonata", value: "1+2" }, + ], + wires: [["n2"]] }, - {id:"s1-n1", x:10, y:10, z:"s1", type:"function", - func:"msg.VE = env.get('Ke'); msg.VS = env.get('KS'); msg.VN = env.get('KN'); msg.VB = env.get('KB'); msg.VJ = env.get('KJ'); msg.Vb = env.get('Kb'); msg.Vj = env.get('Kj'); return msg;", - wires:[]} + { id: "n2", x: 10, y: 10, z: "t0", type: "helper", wires: [] }, + // Subflow + { + id: "s1", type: "subflow", name: "Subflow", info: "", + in: [{ x: 10, y: 10, wires: [{ id: "s1-n1" }] }], + out: [{ x: 10, y: 10, wires: [{ id: "s1-n1", port: 0 }] }], + env: [{ name: "KS", type: "str", value: "STR" }] + }, + { + id: "s1-n1", x: 10, y: 10, z: "s1", type: "function", + func: "msg.VE = env.get('Ke'); msg.VS = env.get('KS'); msg.VN = env.get('KN'); msg.VB = env.get('KB'); msg.VJ = env.get('KJ'); msg.Vb = env.get('Kb'); msg.Vj = env.get('Kj'); return msg;", + wires: [] + } ]; helper.load(functionNode, flow, function() { var n1 = helper.getNode("n1"); diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index 9f99830b0..9f115c058 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -29,7 +29,6 @@ var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); var hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks"); var typeRegistry = NR_TEST_UTILS.require("@node-red/registry"); - describe('Flow', function() { var getType; @@ -200,7 +199,7 @@ describe('Flow', function() { }); describe('#start',function() { - it("instantiates an initial configuration and stops it",function(done) { + it("instantiates an initial configuration and stops it", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -209,49 +208,50 @@ describe('Flow', function() { {id:"4",z:"t1",type:"test",foo:"a"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); + return new Promise((done) => { + Object.keys(flow.getActiveNodes()).should.have.length(4); - Object.keys(flow.getActiveNodes()).should.have.length(4); + flow.getNode('1').should.have.a.property('id','1'); + flow.getNode('2').should.have.a.property('id','2'); + flow.getNode('3').should.have.a.property('id','3'); + flow.getNode('4').should.have.a.property('id','4'); - flow.getNode('1').should.have.a.property('id','1'); - flow.getNode('2').should.have.a.property('id','2'); - flow.getNode('3').should.have.a.property('id','3'); - flow.getNode('4').should.have.a.property('id','4'); + currentNodes.should.have.a.property("1"); + currentNodes.should.have.a.property("2"); + currentNodes.should.have.a.property("3"); + currentNodes.should.have.a.property("4"); - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - currentNodes.should.have.a.property("4"); + currentNodes["1"].should.have.a.property("handled",0); + currentNodes["2"].should.have.a.property("handled",0); + currentNodes["3"].should.have.a.property("handled",0); - currentNodes["1"].should.have.a.property("handled",0); - currentNodes["2"].should.have.a.property("handled",0); - currentNodes["3"].should.have.a.property("handled",0); + currentNodes["3"].on("input", function() { + currentNodes["1"].should.have.a.property("handled",1); + currentNodes["2"].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); - currentNodes["3"].on("input", function() { - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - - flow.stop().then(function() { - try { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - currentNodes.should.not.have.a.property("4"); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - stoppedNodes.should.have.a.property("4"); - done(); - } catch(err) { - done(err); - } + flow.stop().then(function() { + try { + currentNodes.should.not.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + currentNodes.should.not.have.a.property("4"); + stoppedNodes.should.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); + stoppedNodes.should.have.a.property("4"); + done(); + } catch(err) { + done(err); + } + }); }); - }); - currentNodes["1"].receive({payload:"test"}); + currentNodes["1"].receive({payload:"test"}); + }) }); - it("instantiates config nodes in the right order",function(done) { + it("instantiates config nodes in the right order",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -261,11 +261,8 @@ describe('Flow', function() { {id:"5",z:"t1",type:"test"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(5); - - currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); @@ -278,7 +275,7 @@ describe('Flow', function() { currentNodes["4"].should.have.a.property("_index",1); currentNodes["5"].should.have.a.property("_index",0); - flow.stop().then(function() { + return flow.stop().then(function() { currentNodes.should.not.have.a.property("1"); currentNodes.should.not.have.a.property("2"); currentNodes.should.not.have.a.property("3"); @@ -289,12 +286,11 @@ describe('Flow', function() { stoppedNodes.should.have.a.property("3"); stoppedNodes.should.have.a.property("4"); stoppedNodes.should.have.a.property("5"); - done(); }); }); - it("detects dependency loops in config nodes",function() { + it("detects dependency loops in config nodes",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"node1",z:"t1",type:"test",foo:"node2"}, // This node depends on #5 @@ -302,12 +298,12 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); /*jshint immed: false */ - (function(){ - flow.start(); - }).should.throw("Circular config node dependency detected: node1"); + return flow.start().catch(err => { + err.toString().should.equal("Error: Circular config node dependency detected: node1") + }) }); - it("rewires nodes specified by diff",function(done) { + it("rewires nodes specified by diff", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -317,16 +313,15 @@ describe('Flow', function() { var flow = Flow.create({},config,config.flows["t1"]); createCount.should.equal(0); - flow.start(); + await flow.start(); //TODO: use update to pass in new wiring and verify the change createCount.should.equal(3); flow.start({rewired:["2"]}); createCount.should.equal(3); rewiredNodes.should.have.a.property("2"); - done(); }); - it("instantiates a node with environment variable property values",function(done) { + it("instantiates a node with environment variable property values", async function() { after(function() { delete process.env.NODE_RED_TEST_VALUE; }) @@ -341,7 +336,7 @@ describe('Flow', function() { {id:"6",x:10,y:10,z:"t1",type:"test",foo:["$(NODE_RED_TEST_VALUE)"],wires:[]} ]); var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); @@ -352,12 +347,10 @@ describe('Flow', function() { activeNodes["5"].foo.should.equal("$(NODE_RED_TEST_VALUE_NONE)"); activeNodes["6"].foo[0].should.equal("a-value"); - flow.stop().then(function() { - done(); - }); + await flow.stop() }); - it("ignores disabled nodes",function(done) { + it("ignores disabled nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -368,7 +361,7 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(3); @@ -387,28 +380,20 @@ describe('Flow', function() { currentNodes["3"].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); + await NR_TEST_UTILS.sleep(50) + currentNodes["1"].should.have.a.property("handled",1); + // Message doesn't reach 3 as 2 is disabled + currentNodes["3"].should.have.a.property("handled",0); - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - // Message doesn't reach 3 as 2 is disabled - currentNodes["3"].should.have.a.property("handled",0); - - flow.stop().then(function() { - try { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - currentNodes.should.not.have.a.property("4"); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.not.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - stoppedNodes.should.have.a.property("4"); - done(); - } catch(err) { - done(err); - } - }); - },50); + await flow.stop() + currentNodes.should.not.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + currentNodes.should.not.have.a.property("4"); + stoppedNodes.should.have.a.property("1"); + stoppedNodes.should.not.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); + stoppedNodes.should.have.a.property("4"); }); }); @@ -416,7 +401,7 @@ describe('Flow', function() { describe('#stop', function() { - it("stops all nodes",function(done) { + it("stops all nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -424,25 +409,21 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"asyncTest",foo:"a",wires:[]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - - + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - flow.stop().then(function() { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - done(); - }).catch(done); + await flow.stop() + currentNodes.should.not.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + stoppedNodes.should.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); }); - it("stops specified nodes",function(done) { + it("stops specified nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -450,24 +431,21 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - flow.stop(["2"]).then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.have.a.property("3"); - stoppedNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.not.have.a.property("3"); - done(); - }); + await flow.stop(["2"]) + currentNodes.should.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.have.a.property("3"); + stoppedNodes.should.not.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.not.have.a.property("3"); }); - it("stops config nodes last",function(done) { + it("stops config nodes last", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -478,7 +456,7 @@ describe('Flow', function() { {id:"c3",z:"t1",type:"test"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); @@ -488,14 +466,12 @@ describe('Flow', function() { currentNodes.should.have.a.property("c3"); stoppedOrder.should.have.a.length(0); - flow.stop().then(function() { - stoppedOrder.should.eql([ '1', '2', '3', 'c1', 'c2', 'c3' ]); - done(); - }).catch(done); + await flow.stop() + stoppedOrder.should.eql([ '1', '2', '3', 'c1', 'c2', 'c3' ]); }); - it("Times out a node that fails to close", function(done) { + it("Times out a node that fails to close", async function() { Flow.init({settings:{nodeCloseTimeout:50},log:{ log: sinon.stub(), debug: sinon.stub(), @@ -512,31 +488,28 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - flow.stop().then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - stoppedNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - setTimeout(function() { - currentNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("1"); - done(); - },40) - }); + await flow.stop() + currentNodes.should.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + stoppedNodes.should.not.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); + await NR_TEST_UTILS.sleep(40) + currentNodes.should.not.have.a.property("1"); + stoppedNodes.should.have.a.property("1"); }); }); describe('#getNode',function() { - it("gets a node known to the flow",function(done) { + it("gets a node known to the flow", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -545,16 +518,13 @@ describe('Flow', function() { {id:"4",z:"t1",type:"test",foo:"a"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(4); - flow.getNode('1').should.have.a.property('id','1'); - - flow.stop().then(() => { done() }); + await flow.stop(); }); - it("passes to parent if node not known locally",function(done) { + it("passes to parent if node not known locally", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -565,19 +535,14 @@ describe('Flow', function() { var flow = Flow.create({ getNode: id => { return {id:id}} },config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(4); - flow.getNode('1').should.have.a.property('id','1'); - flow.getNode('parentNode').should.have.a.property('id','parentNode'); - - - flow.stop().then(() => { done() }); + await flow.stop() }); - it("does not pass to parent if cancelBubble set",function(done) { + it("does not pass to parent if cancelBubble set", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -588,19 +553,16 @@ describe('Flow', function() { var flow = Flow.create({ getNode: id => { return {id:id}} },config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(4); - flow.getNode('1').should.have.a.property('id','1'); - should.not.exist(flow.getNode('parentNode',true)); - flow.stop().then(() => { done() }); + await flow.stop() }); }); describe("#handleStatus",function() { - it("passes a status event to the adjacent status node",function(done) { + it("passes a status event to the adjacent status node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -611,44 +573,34 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(5); flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status",random:"otherProperty"}); - - setTimeout(function() { - - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","1"); - statusMessage.status.source.should.have.a.property("type","test"); - statusMessage.status.source.should.have.a.property("name","a"); - - currentNodes["sn2"].should.have.a.property("handled",1); - statusMessage = currentNodes["sn2"].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("random","otherProperty"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","1"); - statusMessage.status.source.should.have.a.property("type","test"); - statusMessage.status.source.should.have.a.property("name","a"); - - - flow.stop().then(function() { - done(); - }); - },50) + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + currentNodes["sn2"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("random","otherProperty"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + await flow.stop() }); - it("passes a status event to the adjacent scoped status node ",function(done) { + it("passes a status event to the adjacent scoped status node ", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -659,39 +611,32 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(5); - flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",0); - currentNodes["sn2"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn2"].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","1"); - statusMessage.status.source.should.have.a.property("type","test"); - statusMessage.status.source.should.have.a.property("name","a"); - - - flow.stop().then(function() { - done(); - }); - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + await flow.stop() }); - it("passes a status event to the group scoped status node",function(done) { + it("passes a status event to the group scoped status node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, - {id: "g1", type: "group", g: "g3" }, - {id: "g2", type: "group" }, - {id: "g3", type: "group" }, + {id: "g1", type: "group", g: "g3", z:"t1" }, + {id: "g2", type: "group", z:"t1" }, + {id: "g3", type: "group", z:"t1" }, {id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]}, // sn - in the same group as source node {id:"sn",x:10,y:10,z:"t1",g:"g1", type:"status",scope:"group",wires:[]}, @@ -705,29 +650,21 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"}); - setTimeout(function() { - try { - currentNodes["sn"].should.have.a.property("handled",1); - currentNodes["sn2"].should.have.a.property("handled",0); - currentNodes["sn3"].should.have.a.property("handled",1); - currentNodes["sn3"].should.have.a.property("handled",1); - done() - } catch(err) { - done(err) - } - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + currentNodes["sn2"].should.have.a.property("handled",0); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + await flow.stop() }); - - - }); describe("#handleError",function() { - it("passes an error event to the adjacent catch node",function(done) { + it("passes an error event to the adjacent catch node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -739,45 +676,38 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(6); - - flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); + currentNodes["sn2"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn2"].messages[0]; - currentNodes["sn2"].should.have.a.property("handled",1); - statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); - - // Node sn3 has uncaught:true - so should not get called - currentNodes["sn3"].should.have.a.property("handled",0); - - - flow.stop().then(function() { - done(); - }); - },50); + // Node sn3 has uncaught:true - so should not get called + currentNodes["sn3"].should.have.a.property("handled",0); + await flow.stop() }); - it("passes an error event to the adjacent scoped catch node ",function(done) { + + it("passes an error event to the adjacent scoped catch node ", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -790,56 +720,50 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(7); flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn2"].messages[0]; - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",0); - currentNodes["sn2"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); + // Node sn3/4 have uncaught:true - so should not get called + currentNodes["sn3"].should.have.a.property("handled",0); + currentNodes["sn4"].should.have.a.property("handled",0); - // Node sn3/4 have uncaught:true - so should not get called - currentNodes["sn3"].should.have.a.property("handled",0); - currentNodes["sn4"].should.have.a.property("handled",0); + // Inject error that sn1/2 will ignore - so should get picked up by sn3 + flow.handleError(config.flows["t1"].nodes["3"],"my-error-2",{a:"foo-2"}); - // Inject error that sn1/2 will ignore - so should get picked up by sn3 - flow.handleError(config.flows["t1"].nodes["3"],"my-error-2",{a:"foo-2"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",0); - currentNodes["sn2"].should.have.a.property("handled",1); - currentNodes["sn3"].should.have.a.property("handled",1); - currentNodes["sn4"].should.have.a.property("handled",1); - - statusMessage = currentNodes["sn3"].messages[0]; - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error-2"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","3"); - statusMessage.error.source.should.have.a.property("type","test"); - - flow.stop().then(function() { - done(); - }); - },50); - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn4"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn3"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error-2"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","3"); + statusMessage.error.source.should.have.a.property("type","test"); + await flow.stop() }); - it("passes an error event to the group scoped catch node",function(done) { + it("passes an error event to the group scoped catch node",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, - {id: "g1", type: "group", g: "g3" }, - {id: "g2", type: "group" }, - {id: "g3", type: "group" }, + {id: "g1", type: "group", g: "g3", z:"t1" }, + {id: "g2", type: "group", z:"t1" }, + {id: "g3", type: "group", z:"t1" }, {id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]}, // sn - in the same group as source node {id:"sn",x:10,y:10,z:"t1",g:"g1", type:"catch",scope:"group",wires:[]}, @@ -853,24 +777,20 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); - setTimeout(function() { - try { - currentNodes["sn"].should.have.a.property("handled",1); - currentNodes["sn2"].should.have.a.property("handled",0); - currentNodes["sn3"].should.have.a.property("handled",1); - currentNodes["sn3"].should.have.a.property("handled",1); - done() - } catch(err) { - done(err) - } - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + currentNodes["sn2"].should.have.a.property("handled",0); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + await flow.stop() }); - it("moves any existing error object sideways",function(done){ + + it("moves any existing error object sideways", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -880,33 +800,30 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo",error:"existing"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("_error","existing"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); + statusMessage.should.have.a.property("_error","existing"); + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - flow.stop().then(function() { - done(); - }); - },50); + await flow.stop() }); it("prevents an error looping more than 10 times",function(){}); }); describe("#handleComplete",function() { - it("passes a complete event to the adjacent Complete node",function(done) { + it("passes a complete event to the adjacent Complete node",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"testDone",name:"a",wires:["2"]}, @@ -916,143 +833,154 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(4); var msg = {payload: "hello world"} var n1 = currentNodes["1"].receive(msg); - setTimeout(function() { - currentNodes["cn"].should.have.a.property("handled",2); - currentNodes["cn"].messages[0].should.have.a.property("handled",1); - currentNodes["cn"].messages[1].should.have.a.property("handled",2); - flow.stop().then(function() { - done(); - }); - },50); + await NR_TEST_UTILS.sleep(50) + + currentNodes["cn"].should.have.a.property("handled",2); + currentNodes["cn"].messages[0].should.have.a.property("handled",1); + currentNodes["cn"].messages[1].should.have.a.property("handled",2); + await flow.stop() }); }); describe("#send", function() { - it("sends a message - no cloning", function(done) { - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } + it("sends a message - no cloning", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(2); var n1 = flow.getNode('1'); var n2 = flow.getNode('2'); var messageReceived = false; - n2.receive = function(msg) { - messageReceived = true; - try { - msg.should.be.exactly(message); - shutdownTest(); - } catch(err) { - shutdownTest(err); + + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } } - } - - var message = {payload:"hello"} - flow.send([{ - msg: message, - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined }, - cloneMessage: false - }]) - messageReceived.should.be.false() - }) - it("sends a message - cloning", function(done) { - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} - ]); - var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - - Object.keys(flow.getActiveNodes()).should.have.length(2); - - var n1 = flow.getNode('1'); - var n2 = flow.getNode('2'); - - n2.receive = function(msg) { - try { - // Message should be cloned - msg.should.be.eql(message); - msg.should.not.be.exactly(message); - shutdownTest(); - } catch(err) { - shutdownTest(err); - } - } - - var message = {payload:"hello"} - flow.send([{ - msg: message, - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined }, - cloneMessage: true - }]) - }) - it("sends multiple messages", function(done) { - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} - ]); - var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - - Object.keys(flow.getActiveNodes()).should.have.length(2); - - var n1 = flow.getNode('1'); - var n2 = flow.getNode('2'); - - var messageCount = 0; - n2.receive = function(msg) { - try { - msg.should.be.exactly(messages[messageCount++]); - if (messageCount === 2) { + n2.receive = function(msg) { + messageReceived = true; + try { + msg.should.be.exactly(message); shutdownTest(); + } catch(err) { + shutdownTest(err); } - } catch(err) { - shutdownTest(err); } - } - var messages = [{payload:"hello"},{payload:"world"}]; - - flow.send([{ - msg: messages[0], - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined } - },{ - msg: messages[1], - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined } - }]) + var message = {payload:"hello"} + flow.send([{ + msg: message, + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined }, + cloneMessage: false + }]) + messageReceived.should.be.false() + }) }) - it("sends a message - triggers hooks", function(done) { + it("sends a message - cloning", async function() { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} + ]); + var flow = Flow.create({},config,config.flows["t1"]); + await flow.start(); + + Object.keys(flow.getActiveNodes()).should.have.length(2); + + var n1 = flow.getNode('1'); + var n2 = flow.getNode('2'); + + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } + } + n2.receive = function(msg) { + try { + // Message should be cloned + msg.should.be.eql(message); + msg.should.not.be.exactly(message); + shutdownTest(); + } catch(err) { + shutdownTest(err); + } + } + + var message = {payload:"hello"} + flow.send([{ + msg: message, + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined }, + cloneMessage: true + }]) + }) + }) + it("sends multiple messages", async function() { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} + ]); + var flow = Flow.create({},config,config.flows["t1"]); + await flow.start(); + + Object.keys(flow.getActiveNodes()).should.have.length(2); + + var n1 = flow.getNode('1'); + var n2 = flow.getNode('2'); + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } + } + var messageCount = 0; + n2.receive = function(msg) { + try { + msg.should.be.exactly(messages[messageCount++]); + if (messageCount === 2) { + shutdownTest(); + } + } catch(err) { + shutdownTest(err); + } + } + + var messages = [{payload:"hello"},{payload:"world"}]; + + flow.send([{ + msg: messages[0], + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined } + },{ + msg: messages[1], + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined } + }]) + }) + }) + it("sends a message - triggers hooks", async function() { + const message = {payload:"hello"} var hookErrors = []; var messageReceived = false; var hooksCalled = []; @@ -1100,45 +1028,49 @@ describe('Flow', function() { } }) - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(2); var n1 = flow.getNode('1'); var n2 = flow.getNode('2'); - n2.receive = function(msg) { - messageReceived = true; - try { - msg.should.be.eql(message); - msg.should.not.be.exactly(message); - hooksCalled.should.eql(["onSend","preRoute","preDeliver","postDeliver"]) - if (hookErrors.length > 0) { - shutdownTest(hookErrors[0]) - } else { - shutdownTest(); - } - } catch(err) { - shutdownTest(err); + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } + } + n2.receive = function(msg) { + messageReceived = true; + try { + msg.should.be.eql(message); + msg.should.not.be.exactly(message); + hooksCalled.should.eql(["onSend","preRoute","preDeliver","postDeliver"]) + if (hookErrors.length > 0) { + shutdownTest(hookErrors[0]) + } else { + shutdownTest(); + } + } catch(err) { + shutdownTest(err); + } } - } - var message = {payload:"hello"} - flow.send([{ - msg: message, - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined }, - cloneMessage: true - }]) + + flow.send([{ + msg: message, + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined }, + cloneMessage: true + }]) + }) }) describe("errors thrown by hooks are reported to the sending node", function() { @@ -1146,7 +1078,7 @@ describe('Flow', function() { var n1,n2; var messageReceived = false; var errorReceived = null; - before(function() { + before(async function() { hooks.add("onSend", function(sendEvents) { if (sendEvents[0].msg.payload === "trigger-onSend") { throw new Error("onSend Error"); @@ -1173,7 +1105,7 @@ describe('Flow', function() { {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); n1 = flow.getNode('1'); n2 = flow.getNode('2'); n2.receive = function(msg) { @@ -1184,9 +1116,9 @@ describe('Flow', function() { } }) - after(function(done) { + after(async function() { hooks.clear(); - flow.stop().then(() => { done() }); + await flow.stop() }) beforeEach(function() { messageReceived = false; @@ -1223,7 +1155,7 @@ describe('Flow', function() { var n1,n2; var messageReceived = false; var errorReceived = false; - before(function() { + before(async function() { hooks.add("onSend", function(sendEvents) { if (sendEvents[0].msg.payload === "trigger-onSend") { return false @@ -1250,7 +1182,7 @@ describe('Flow', function() { {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); n1 = flow.getNode('1'); n2 = flow.getNode('2'); n2.receive = function(msg) { @@ -1261,9 +1193,9 @@ describe('Flow', function() { } }) - after(function(done) { + after(async function() { hooks.clear(); - flow.stop().then(() => { done() }); + await flow.stop() }) function testSend(payload,messageReceivedExpected,errorReceivedExpected,done) { messageReceived = false; @@ -1303,170 +1235,139 @@ describe('Flow', function() { }) describe("#env", function () { - it("can instantiate a node with environment variable property values of group and tab", function (done) { - try { - after(function() { - delete process.env.V0; - delete process.env.V1; - }) - process.env.V0 = "gv0"; - process.env.V1 = "gv1"; - process.env.V3 = "gv3"; - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab",env:[ - {"name": "V0", value: "t1v0", type: "str"}, - {"name": "V2", value: "t1v2", type: "str"} - ]}, - {id:"g1",type:"group",z:"t1",env:[ - {"name": "V0", value: "g1v0", type: "str"}, - {"name": "V1", value: "g1v1", type: "str"} - ]}, - {id:"g2",type:"group",z:"t1",g:"g1",env:[ - {"name": "V1", value: "g2v1", type: "str"} - ]}, - {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 + it("can instantiate a node with environment variable property values of group and tab", async function () { + after(function() { + delete process.env.V0; + delete process.env.V1; + }) + process.env.V0 = "gv0"; + process.env.V1 = "gv1"; + process.env.V3 = "gv3"; + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab",env:[ + {"name": "V0", value: "t1v0", type: "str"}, + {"name": "V2", value: "t1v2", type: "str"} + ]}, + {id:"g1",type:"group",z:"t1",env:[ + {"name": "V0", value: "g1v0", type: "str"}, + {"name": "V1", value: "g1v1", type: "str"} + ]}, + {id:"g2",type:"group",z:"t1",g:"g1",env:[ + {"name": "V1", value: "g2v1", type: "str"} + ]}, + {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(); + {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"]); + await flow.start(); - var activeNodes = flow.getActiveNodes(); + var activeNodes = flow.getActiveNodes(); - 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) + 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(); - }); - } - catch (e) { - console.log(e.stack); - done(e); - } - }); - it("can access environment variable property using $parent", function (done) { - try { - after(function() { - delete process.env.V0; - delete process.env.V1; - }) - process.env.V0 = "gv0"; - process.env.V1 = "gv1"; - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab",env:[ - {"name": "V0", value: "v0", type: "str"} - ]}, - {id:"g1",type:"group",z:"t1",env:[ - {"name": "V0", value: "v1", type: "str"}, - {"name": "V1", value: "v2", type: "str"} - ]}, - {id:"g2",type:"group",z:"t1",g:"g1",env:[ - {"name": "V1", value: "v3", type: "str"} - ]}, - {id:"1",x:10,y:10,z:"t1",type:"test",foo:"${$parent.V0}",wires:[]}, - {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V0}",wires:[]}, - {id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V1}",wires:[]}, - {id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${$parent.V1}",wires:[]}, - {id:"5",x:10,y:10,z:"t1",type:"test",foo:"${$parent.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("gv0"); - activeNodes["2"].foo.should.equal("v0"); - activeNodes["3"].foo.should.equal("gv1"); - activeNodes["4"].foo.should.equal("v2"); - activeNodes["5"].foo.should.equal("gv1"); - - flow.stop().then(function() { - done(); - }); - } - catch (e) { - console.log(e.stack); - done(e); - } + await flow.stop() }); - it("can define environment variable using JSONata", 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(); + it("can access environment variable property using $parent", async function () { + after(function() { + delete process.env.V0; + delete process.env.V1; + }) + process.env.V0 = "gv0"; + process.env.V1 = "gv1"; + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab",env:[ + {"name": "V0", value: "v0", type: "str"} + ]}, + {id:"g1",type:"group",z:"t1",env:[ + {"name": "V0", value: "v1", type: "str"}, + {"name": "V1", value: "v2", type: "str"} + ]}, + {id:"g2",type:"group",z:"t1",g:"g1",env:[ + {"name": "V1", value: "v3", type: "str"} + ]}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"${$parent.V0}",wires:[]}, + {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V0}",wires:[]}, + {id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V1}",wires:[]}, + {id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${$parent.V1}",wires:[]}, + {id:"5",x:10,y:10,z:"t1",type:"test",foo:"${$parent.V1}",wires:[]}, + ]); + var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); + await flow.start(); - var activeNodes = flow.getActiveNodes(); + var activeNodes = flow.getActiveNodes(); - activeNodes["1"].foo.should.equal(3); - activeNodes["2"].foo.should.equal(5); + activeNodes["1"].foo.should.equal("gv0"); + activeNodes["2"].foo.should.equal("v0"); + activeNodes["3"].foo.should.equal("gv1"); + activeNodes["4"].foo.should.equal("v2"); + activeNodes["5"].foo.should.equal("gv1"); - flow.stop().then(function() { - done(); - }); - } - catch (e) { - console.log(e.stack); - done(e); - } + await flow.stop() }); - 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(); + it("can define environment variable using JSONata", async function () { + 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"]); + await flow.start(); - var activeNodes = flow.getActiveNodes(); + var activeNodes = flow.getActiveNodes(); - activeNodes["1"].foo.should.equal(3); - activeNodes["2"].foo.should.equal(5); + 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); - } + await flow.stop() + }); + + it("can access global environment variables defined as JSONata values", async function () { + 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"]); + await flow.start(); + + var activeNodes = flow.getActiveNodes(); + + activeNodes["1"].foo.should.equal(3); + activeNodes["2"].foo.should.equal(5); + + await flow.stop() }); }); diff --git a/test/unit/@node-red/runtime/lib/flows/Group_spec.js b/test/unit/@node-red/runtime/lib/flows/Group_spec.js new file mode 100644 index 000000000..b547b1c77 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/flows/Group_spec.js @@ -0,0 +1,48 @@ +const should = require("should"); +const NR_TEST_UTILS = require("nr-test-utils"); +const { Group } = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Group"); + +describe('Group', function () { + describe('getSetting', function () { + it("returns group name/id", async function () { + const group = new Group({ + getSetting: v => v+v + }, { + name: "g1", + id: "group1" + }) + await group.start() + + group.getSetting("NR_GROUP_NAME").should.equal("g1") + group.getSetting("NR_GROUP_ID").should.equal("group1") + }) + it("delegates to parent if not found", async function () { + const group = new Group({ + getSetting: v => v+v + }, { + name: "g1", + id: "group1" + }) + await group.start() + + group.getSetting("123").should.equal("123123") + }) + it("delegates to parent if explicit requested", async function () { + const parentGroup = new Group({ + getSetting: v => v+v + }, { + name: "g0", + id: "group0" + }) + const group = new Group(parentGroup, { + name: "g1", + id: "group1" + }) + await parentGroup.start() + await group.start() + + group.getSetting("$parent.NR_GROUP_NAME").should.equal("g0") + group.getSetting("$parent.NR_GROUP_ID").should.equal("group0") + }) + }) +}) diff --git a/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js b/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js index c2e251447..618205f6d 100644 --- a/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js @@ -68,11 +68,13 @@ describe('Subflow', function() { this.handled = 0; this.stopped = false; this.received = null; + this.receivedEnv = null; currentNodes[node.id] = node; this.on('input',function(msg) { // console.log(this.id,msg.payload); node.handled++; node.received = msg.payload; + node.receivedEnv = msg.receivedEnv; node.send(msg); }); this.on('close',function() { @@ -185,7 +187,15 @@ describe('Subflow', function() { var flow = node._flow; var val = flow.getSetting("__KEY__"); node.received = val; - node.send({payload: val}); + const receivedEnv = {} + try { + ['__KEY__','__KEY1__','__KEY2__','__KEY3__','__KEY4__'].forEach(k => { + receivedEnv[k] = flow.getSetting(k) + }) + } catch (err) { + console.log(err) + } + node.send({payload: val, receivedEnv}); }); this.on('close',function() { node.stopped = true; @@ -282,7 +292,7 @@ describe('Subflow', function() { getType.restore(); }); describe('#start',function() { - it("instantiates a subflow and stops it",function(done) { + it("instantiates a subflow and stops it", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -297,7 +307,7 @@ describe('Subflow', function() { ]); var flow = Flow.create({handleError: (a,b,c) => { console.log(a,b,c); }},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(4); @@ -332,37 +342,21 @@ describe('Subflow', function() { // currentNodes[sfInstanceId2].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); + + await NR_TEST_UTILS.sleep(150) - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - // currentNodes[sfInstanceId].should.have.a.property("handled",1); - // currentNodes[sfInstanceId2].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",1); + currentNodes["1"].should.have.a.property("handled",1); + // currentNodes[sfInstanceId].should.have.a.property("handled",1); + // currentNodes[sfInstanceId2].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",1); - - - flow.stop().then(function() { - Object.keys(currentNodes).should.have.length(0); - Object.keys(stoppedNodes).should.have.length(6); - - // currentNodes.should.not.have.a.property("1"); - // currentNodes.should.not.have.a.property("3"); - // currentNodes.should.not.have.a.property("4"); - // // currentNodes.should.not.have.a.property(sfInstanceId); - // // currentNodes.should.not.have.a.property(sfInstanceId2); - // // currentNodes.should.not.have.a.property(sfConfigId); - // stoppedNodes.should.have.a.property("1"); - // stoppedNodes.should.have.a.property("3"); - // stoppedNodes.should.have.a.property("4"); - // // stoppedNodes.should.have.a.property(sfInstanceId); - // // stoppedNodes.should.have.a.property(sfInstanceId2); - // // stoppedNodes.should.have.a.property(sfConfigId); - done(); - }); - },150); + await flow.stop() + Object.keys(currentNodes).should.have.length(0); + Object.keys(stoppedNodes).should.have.length(6); }); - it("instantiates a subflow inside a subflow and stops it",function(done) { + + it("instantiates a subflow inside a subflow and stops it", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -379,24 +373,20 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes["1"].should.have.a.property("handled",0); currentNodes["3"].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); - - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - flow.stop().then(function() { - Object.keys(currentNodes).should.have.length(0); - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["1"].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + await flow.stop() + Object.keys(currentNodes).should.have.length(0); }); - it("rewires a subflow node on update/start",function(done){ + it("rewires a subflow node on update/start", async function(){ var rawConfig = [ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -417,7 +407,7 @@ describe('Subflow', function() { var diff = flowUtils.diffConfigs(config,newConfig); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(4); @@ -429,36 +419,28 @@ describe('Subflow', function() { currentNodes["4"].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); + await NR_TEST_UTILS.sleep(150) + currentNodes["1"].should.have.a.property("handled",1); + // currentNodes[sfInstanceId].should.have.a.property("handled",1); + // currentNodes[sfInstanceId2].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",0); - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - // currentNodes[sfInstanceId].should.have.a.property("handled",1); - // currentNodes[sfInstanceId2].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",0); + flow.update(newConfig,newConfig.flows["t1"]); + await flow.start(diff) + currentNodes["1"].receive({payload:"test2"}); + await NR_TEST_UTILS.sleep(150) + currentNodes["1"].should.have.a.property("handled",2); + // currentNodes[sfInstanceId].should.have.a.property("handled",2); + // currentNodes[sfInstanceId2].should.have.a.property("handled",2); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",1); - flow.update(newConfig,newConfig.flows["t1"]); - flow.start(diff) - - currentNodes["1"].receive({payload:"test2"}); - setTimeout(function() { - - currentNodes["1"].should.have.a.property("handled",2); - // currentNodes[sfInstanceId].should.have.a.property("handled",2); - // currentNodes[sfInstanceId2].should.have.a.property("handled",2); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",1); - - - flow.stop().then(function() { - done(); - }); - },150); - },150); + await flow.stop() }); }); describe('#stop', function() { - it("stops subflow instance nodes",function(done) { + it("stops subflow instance nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -470,20 +452,18 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(3); Object.keys(stoppedNodes).should.have.length(0); - flow.stop(["2"]).then(function() { - Object.keys(currentNodes).should.have.length(2); - Object.keys(stoppedNodes).should.have.length(1); - done(); - }).catch(done); + await flow.stop(["2"]) + Object.keys(currentNodes).should.have.length(2); + Object.keys(stoppedNodes).should.have.length(1); }); }); describe("#handleStatus",function() { - it("passes a status event to the subflow's parent tab status node - all scope",function(done) { + it("passes a status event to the subflow's parent tab status node - all scope", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -496,27 +476,24 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(150) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","test status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("type","testStatus"); - statusMessage.status.source.should.have.a.property("name","test-status-node"); + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","test status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("type","testStatus"); + statusMessage.status.source.should.have.a.property("name","test-status-node"); - flow.stop().then(function() { - done(); - }); - },150); + await flow.stop() }); - it("passes a status event to the subflow's parent tab status node - targetted scope",function(done) { + it("passes a status event to the subflow's parent tab status node - targetted scope", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -531,34 +508,30 @@ describe('Subflow', function() { var flow = Flow.create({handleStatus:() => { parentFlowStatusCalled = true} },config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); - setTimeout(function() { - parentFlowStatusCalled.should.be.false(); + await NR_TEST_UTILS.sleep(150) + parentFlowStatusCalled.should.be.false(); - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","test status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("type","testStatus"); - statusMessage.status.source.should.have.a.property("name","test-status-node"); + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","test status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("type","testStatus"); + statusMessage.status.source.should.have.a.property("name","test-status-node"); - flow.stop().then(function() { - - done(); - }); - },150); + await flow.stop() }); }); describe("status node", function() { - it("emits a status event when a message is passed to a subflow-status node - msg.payload as string", function(done) { + it("emits a status event when a message is passed to a subflow-status node - msg.payload as string", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -578,29 +551,24 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test-payload"}); + await NR_TEST_UTILS.sleep(150) - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","test-payload"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","2"); - statusMessage.status.source.should.have.a.property("type","subflow:sf1"); - - flow.stop().then(function() { - - done(); - }); - },150); + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","test-payload"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","2"); + statusMessage.status.source.should.have.a.property("type","subflow:sf1"); + await flow.stop() }); - it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", function(done) { + it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -620,29 +588,26 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:{text:"payload-obj"}}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(150) - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","payload-obj"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","2"); - statusMessage.status.source.should.have.a.property("type","subflow:sf1"); + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - flow.stop().then(function() { + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","payload-obj"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","2"); + statusMessage.status.source.should.have.a.property("type","subflow:sf1"); - done(); - }); - },150); + await flow.stop() }); - it("emits a status event when a message is passed to a subflow-status node - msg.status", function(done) { + it("emits a status event when a message is passed to a subflow-status node - msg.status", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -662,29 +627,26 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({status:{text:"status-obj"}}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(150) - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","status-obj"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","2"); - statusMessage.status.source.should.have.a.property("type","subflow:sf1"); + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - flow.stop().then(function() { + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","status-obj"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","2"); + statusMessage.status.source.should.have.a.property("type","subflow:sf1"); - done(); - }); - },150); + flow.stop() }); - it("does not emit a regular status event if it contains a subflow-status node", function(done) { + it("does not emit a regular status event if it contains a subflow-status node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -704,7 +666,7 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); @@ -712,15 +674,12 @@ describe('Subflow', function() { currentNodes["sn"].should.have.a.property("handled",0); - flow.stop().then(function() { - - done(); - }); + await flow.stop() }); }) describe("#handleError",function() { - it("passes an error event to the subflow's parent tab catch node - all scope",function(done) { + it("passes an error event to the subflow's parent tab catch node - all scope",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -733,28 +692,26 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); + + await NR_TEST_UTILS.sleep(150) - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","test error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("type","testError"); - statusMessage.error.source.should.have.a.property("name","test-error-node"); + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","test error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("type","testError"); + statusMessage.error.source.should.have.a.property("name","test-error-node"); - flow.stop().then(function() { - done(); - }); - },150); + await flow.stop() }); - it("passes an error event to the subflow's parent tab catch node - targetted scope",function(done) { + it("passes an error event to the subflow's parent tab catch node - targetted scope", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -768,50 +725,31 @@ describe('Subflow', function() { var parentFlowErrorCalled = false; var flow = Flow.create({handleError:() => { parentFlowErrorCalled = true} },config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); - setTimeout(function() { - parentFlowErrorCalled.should.be.false(); + await NR_TEST_UTILS.sleep(150) + + parentFlowErrorCalled.should.be.false(); - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","test error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("type","testError"); - statusMessage.error.source.should.have.a.property("name","test-error-node"); - - flow.stop().then(function() { - done(); - }); - },150); + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","test error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("type","testError"); + statusMessage.error.source.should.have.a.property("name","test-error-node"); + await flow.stop() }); }); describe("#env var", function() { - // should be changed according to internal env var representation - function setEnv(node, key, val) { - var flow = node._flow; - if (flow) { - var env = flow.env; - if (!env) { - env = flow.env = {}; - } - env[key] = { - name: key, - type: "str", - value: val - }; - } - } - - it("can access process env var", function(done) { + it("can access process env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, @@ -828,29 +766,25 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); process.env["__KEY__"] = "__VAL__"; currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL__"); - - flow.stop().then(function() { - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("received", "__VAL__"); + await flow.stop() }); - it("can access subflow env var", function(done) { + it("can access subflow env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, {id:"3",x:10,y:10,z:"t1",type:"test",foo:"t1.3",wires:[]}, - {id:"sf1",type:"subflow",name:"Subflow 2",info:"", - "in":[ {wires:[{id:"sf1-1"}]} ], - "out":[ {wires:[{id:"sf1-2",port:0}]} ]}, + {id:"sf1",type:"subflow",name:"Subflow 2",info:"",env: [{name: '__KEY__', value: '__VAL1__', type: 'str'}], + "in":[ {wires:[{id:"sf1-1"}]} ], + "out":[ {wires:[{id:"sf1-2",port:0}]} ]}, {id:"sf1-1",type:"test",z:"sf1",foo:"sf1.1",x:166,y:99,wires:[["sf1-2"]]}, {id:"sf1-2",type:"testEnv",z:"sf1",foo:"sf1.2",x:166,y:99,wires:[[]]} ]); @@ -859,7 +793,7 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); var testenv_node = null; for (var n in currentNodes) { @@ -870,32 +804,30 @@ describe('Subflow', function() { } } process.env["__KEY__"] = "__VAL0__"; - setEnv(testenv_node, "__KEY__", "__VAL1__"); currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL1__"); + await NR_TEST_UTILS.sleep(150) - flow.stop().then(function() { - done(); - }); - },150); + currentNodes["3"].should.have.a.property("received", "__VAL1__"); + await flow.stop() }); - it("can access nested subflow env var", function(done) { + it("can access nested subflow env var", async function() { var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, + {id:"t1",type:"tab", env: [{name: '__KEY1__', value: 't1', type: 'str'}]}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, {id:"3",x:10,y:10,z:"t1",type:"test",foo:"t1.3",wires:[]}, {id:"sf1",type:"subflow",name:"Subflow 1",info:"", - in:[{wires:[{id:"sf1-1"}]}], - out:[{wires:[{id:"sf1-2",port:0}]}]}, + env: [{name: '__KEY2__', value: 'sf1', type: 'str'}], + in:[{wires:[{id:"sf1-1"}]}], + out:[{wires:[{id:"sf1-2",port:0}]}]}, {id:"sf2",type:"subflow",name:"Subflow 2",info:"", - in:[{wires:[{id:"sf2-1"}]}], - out:[{wires:[{id:"sf2-2",port:0}]}]}, + env: [{name: '__KEY3__', value: 'sf2', type: 'str'}], + in:[{wires:[{id:"sf2-1"}]}], + out:[{wires:[{id:"sf2-2",port:0}]}]}, {id:"sf1-1",type:"test",z:"sf1",foo:"sf1.1",x:166,y:99,wires:[["sf1-2"]]}, - {id:"sf1-2",type:"subflow:sf2",z:"sf1",x:166,y:99,wires:[[]]}, + {id:"sf1-2",type:"subflow:sf2",z:"sf1",x:166,y:99,wires:[[]], env: [{name: '__KEY4__', value: 'sf1-2', type: 'str'}] }, {id:"sf2-1",type:"test",z:"sf2",foo:"sf2.1",x:166,y:99,wires:[["sf2-2"]]}, {id:"sf2-2",type:"testEnv",z:"sf2",foo:"sf2.2",x:166,y:99,wires:[[]]}, ]); @@ -904,45 +836,22 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); - - var node_sf1_1 = null; - var node_sf2_1 = null; - var testenv_node = null; - for (var n in currentNodes) { - var node = currentNodes[n]; - if (node.foo === "sf1.1") { - node_sf1_1 = node; - } - if (node.foo === "sf2.1") { - node_sf2_1 = node; - } - } + await flow.start(); process.env["__KEY__"] = "__VAL0__"; currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL0__"); - - setEnv(node_sf1_1, "__KEY__", "__VAL1__"); - currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL1__"); - - setEnv(node_sf2_1, "__KEY__", "__VAL2__"); - currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL2__"); - - flow.stop().then(function() { - done(); - }); - },150); - },150); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("receivedEnv"); + currentNodes["3"].receivedEnv.should.have.a.property('__KEY__', '__VAL0__') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY1__', 't1') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY2__', 'sf1') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY3__', 'sf2') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY4__', 'sf1-2') + + await flow.stop() }); - it("can access name of subflow as env var", function(done) { + it("can access name of subflow as env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, @@ -959,19 +868,15 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "SFN"); - - flow.stop().then(function() { - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("received", "SFN"); + await flow.stop() }); - it("can access id of subflow as env var", function(done) { + it("can access id of subflow as env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, @@ -988,19 +893,13 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "2"); - - flow.stop().then(function() { - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("received", "2"); + await flow.stop() }); - - }); }); diff --git a/test/unit/@node-red/runtime/lib/flows/index_spec.js b/test/unit/@node-red/runtime/lib/flows/index_spec.js index 1a0f2a73c..a07ab7b42 100644 --- a/test/unit/@node-red/runtime/lib/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/index_spec.js @@ -93,7 +93,7 @@ describe('flows/index', function() { flowCreate.flows[id] = { flow: flow, global: global, - start: sinon.spy(), + start: sinon.spy(async() => {}), update: sinon.spy(), stop: sinon.spy(), getActiveNodes: function() { @@ -221,13 +221,18 @@ describe('flows/index', function() { return Promise.resolve({flows:originalConfig}); } events.once('flows:started',function() { - flows.setFlows(newConfig,"nodes").then(function() { - flows.getFlows().flows.should.eql(newConfig); - flowCreate.flows['t1'].update.called.should.be.true(); - flowCreate.flows['t2'].start.called.should.be.true(); - flowCreate.flows['_GLOBAL_'].update.called.should.be.true(); - done(); + events.once('flows:started', function() { + try { + flows.getFlows().flows.should.eql(newConfig); + flowCreate.flows['t1'].update.called.should.be.true(); + flowCreate.flows['t2'].start.called.should.be.true(); + flowCreate.flows['_GLOBAL_'].update.called.should.be.true(); + done(); + } catch(err) { + done(err) + } }) + flows.setFlows(newConfig,"nodes") }); flows.init({log:mockLog, settings:{},storage:storage}); @@ -250,13 +255,14 @@ describe('flows/index', function() { } events.once('flows:started',function() { - flows.setFlows(newConfig,"nodes").then(function() { + events.once('flows:started',function() { flows.getFlows().flows.should.eql(newConfig); flowCreate.flows['t1'].update.called.should.be.true(); flowCreate.flows['t2'].start.called.should.be.true(); flowCreate.flows['_GLOBAL_'].update.called.should.be.true(); flows.stopFlows().then(done); }) + flows.setFlows(newConfig,"nodes") }); flows.init({log:mockLog, settings:{},storage:storage}); diff --git a/test/unit/@node-red/runtime/lib/flows/util_spec.js b/test/unit/@node-red/runtime/lib/flows/util_spec.js index 6a4571e87..99747f6b4 100644 --- a/test/unit/@node-red/runtime/lib/flows/util_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/util_spec.js @@ -149,7 +149,7 @@ describe('flows/util', function() { {id:"t1",type:"tab"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -160,7 +160,7 @@ describe('flows/util', function() { {id:"t1",type:"tab"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -172,7 +172,7 @@ describe('flows/util', function() { {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -184,7 +184,7 @@ describe('flows/util', function() { {id:"sf1-1",x:10,y:10,z:"sf1",type:"test",wires:[]} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"groups":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -196,7 +196,7 @@ describe('flows/util', function() { ]; var parsedConfig = flowUtil.parseConfig(originalConfig); parsedConfig.missingTypes.should.eql(['missing']); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},'t1-2': { id: 't1-2', x: 10, y: 10, z: 't1', type: 'missing', wires: [] }}}},"groups":{},"missingTypes":["missing"]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},'t1-2': { id: 't1-2', x: 10, y: 10, z: 't1', type: 'missing', wires: [] }}}},"missingTypes":["missing"]}; redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true(); }); @@ -206,7 +206,7 @@ describe('flows/util', function() { {id:"cn",type:"test"}, ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -217,7 +217,7 @@ describe('flows/util', function() { {id:"g1",type:"group",z:"t1"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"g1":{"id":"g1","type":"group","z":"t1"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"groups":{"g1":{"id":"g1","type":"group","z":"t1"}},"missingTypes":[]} + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"g1":{"id":"g1","type":"group","z":"t1"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{"g1":{"id":"g1","type":"group","z":"t1"}},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"missingTypes":[]} parsedConfig.should.eql(expectedConfig); }); From f196493402574b0a6d638d14aa2b367a80202264 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 23 Jun 2023 09:35:00 +0100 Subject: [PATCH 4/8] Evaluate global-config env on startup --- .../@node-red/runtime/lib/flows/Flow.js | 21 ++++++++--- .../@node-red/runtime/lib/flows/index.js | 20 ++--------- .../@node-red/runtime/lib/flows/util.js | 36 ------------------- .../@node-red/runtime/lib/nodes/index.js | 1 - .../@node-red/runtime/lib/flows/Flow_spec.js | 25 +++++++++++++ 5 files changed, 45 insertions(+), 58 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 2e03f5165..e7ca490d2 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -172,11 +172,24 @@ class Flow { this.statusNodes = []; this.completeNodeMap = {}; - if (this.env) { - this._env = await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) - // console.log('env', this.env) - // console.log('_env', this._env) + + if (this.isGlobalFlow) { + // This is the global flow. It needs to go find the `global-config` + // node and extract any env properties from it + const configNodes = Object.keys(this.flow.configs); + for (let i = 0; i < configNodes.length; i++) { + const node = this.flow.configs[configNodes[i]] + if (node.type === 'global-config' && node.env) { + const nodeEnv = await flowUtil.evaluateEnvProperties(this, node.env, credentials.get(node.id)) + this._env = { ...this._env, ...nodeEnv } + } + } } + + if (this.env) { + this._env = { ...this._env, ...await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) } + } + for (let i = 0; i < this.groupOrder.length; i++) { // Start the groups in the right order so they // can setup their env vars knowning their parent diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index c121e469c..16bd8a867 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -359,7 +359,7 @@ async function start(type,diff,muteLog,isDeploy) { if (activeFlowConfig.flows.hasOwnProperty(id)) { if (!activeFlowConfig.flows[id].disabled && !activeFlows[id]) { // This flow is not disabled, nor is it currently active, so create it - activeFlows[id] = Flow.create(flowAPI,activeFlowConfig,activeFlowConfig.flows[id]); + activeFlows[id] = Flow.create(activeFlows['global'],activeFlowConfig,activeFlowConfig.flows[id]); log.debug("red/nodes/flows.start : starting flow : "+id); } else { log.debug("red/nodes/flows.start : not starting disabled flow : "+id); @@ -379,7 +379,7 @@ async function start(type,diff,muteLog,isDeploy) { activeFlows[id].update(activeFlowConfig,activeFlowConfig.flows[id]); } else { // This flow didn't previously exist, so create it - activeFlows[id] = Flow.create(flowAPI,activeFlowConfig,activeFlowConfig.flows[id]); + activeFlows[id] = Flow.create(activeFlows['global'],activeFlowConfig,activeFlowConfig.flows[id]); log.debug("red/nodes/flows.start : starting flow : "+id); } } else { @@ -784,17 +784,6 @@ const flowAPI = { log: m => log.log(m) } - -function getGlobalConfig() { - let gconf = null; - eachNode((n) => { - if (n.type === "global-config") { - gconf = n; - } - }); - return gconf; -} - module.exports = { init: init, @@ -807,10 +796,7 @@ module.exports = { get:getNode, eachNode: eachNode, - - - getGlobalConfig: getGlobalConfig, - + /** * Gets the current flow configuration */ diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index 85c1381b3..ed9a3a1b9 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -318,37 +318,6 @@ function parseConfig(config) { return flow; } -function getGlobalEnv(name) { - const nodes = _runtime.nodes; - if (!nodes) { - return null; - } - const gconf = nodes.getGlobalConfig(); - const env = gconf ? gconf.env : null; - - if (env) { - const cred = (gconf ? credentials.get(gconf.id) : null) || { - map: {} - }; - const map = cred.map; - - for (let i = 0; i < env.length; i++) { - const item = env[i]; - if (item.name === name) { - if (item.type === "cred") { - return { - name: name, - value: map[name], - type: "cred" - }; - } - return item; - } - } - } - return null; -} - module.exports = { init: function(runtime) { _runtime = runtime; @@ -359,11 +328,6 @@ module.exports = { }, getEnvVar: function(k) { if (!envVarExcludes[k]) { - const item = getGlobalEnv(k); - if (item) { - const val = redUtil.evaluateNodeProperty(item.value, item.type, null, null, null); - return val; - } return process.env[k]; } return undefined; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/index.js index fd01ebdb0..5b859a5f8 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -205,7 +205,6 @@ module.exports = { getNode: flows.get, eachNode: flows.eachNode, getContext: context.get, - getGlobalConfig: flows.getGlobalConfig, clearContext: context.clear, diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index 9f115c058..167691074 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -1369,6 +1369,31 @@ describe('Flow', function() { await flow.stop() }); + it("global flow can access global-config defined environment variables", async function () { + after(function() { + delete process.env.V0; + }) + const config = flowUtils.parseConfig([ + {id:"gc", type:"global-config", env:[ + {"name": "GC0", value: "3+4", type: "jsonata"} + ]}, + {id:"t1",type:"tab" }, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"${GC0}",wires:[]}, + ]); + // Two-arg call - makes this the global flow that handles global-config nodes + const globalFlow = Flow.create({getSetting:v=>process.env[v]},config); + await globalFlow.start(); + + // Pass the globalFlow in as the parent flow to allow global-config lookup + const flow = Flow.create(globalFlow,config,config.flows["t1"]); + await flow.start(); + + var activeNodes = flow.getActiveNodes(); + activeNodes["1"].foo.should.equal(7); + + await flow.stop() + await globalFlow.stop() + }); }); }); From 3209777aba13761a0eb413065f2fc96c9a80f56d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 23 Jun 2023 15:48:06 +0100 Subject: [PATCH 5/8] Tidy up flow/util --- .../@node-red/runtime/lib/flows/util.js | 536 +++++++++--------- 1 file changed, 267 insertions(+), 269 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index ed9a3a1b9..e619e2b2a 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -13,16 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -var clone = require("clone"); -var redUtil = require("@node-red/util").util; -var Log = require("@node-red/util").log; -var subflowInstanceRE = /^subflow:(.+)$/; -var typeRegistry = require("@node-red/registry"); -const credentials = require("../nodes/credentials"); +const clone = require("clone"); +const redUtil = require("@node-red/util").util; +const Log = require("@node-red/util").log; +const typeRegistry = require("@node-red/registry"); +const subflowInstanceRE = /^subflow:(.+)$/; let _runtime = null; +let envVarExcludes = {}; -var envVarExcludes = {}; +function init(runtime) { + _runtime = runtime; + envVarExcludes = {}; + if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) { + runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true); + } +} function diffNodes(oldNode,newNode) { if (oldNode == null) { @@ -121,6 +127,12 @@ async function evaluateEnvProperties(flow, env, credentials) { return evaluatedEnv } +/** + * Create a new instance of a node + * @param {Flow} flow The containing flow + * @param {object} config The node configuration object + * @return {Node} The instance of the node + */ async function createNode(flow,config) { var newNode = null; var type = config.type; @@ -317,259 +329,207 @@ function parseConfig(config) { }); return flow; } +function getEnvVar(k) { + if (!envVarExcludes[k]) { + return process.env[k]; + } + return undefined; +} +function diffConfigs(oldConfig, newConfig) { + var id; + var node; + var nn; + var wires; + var j,k; -module.exports = { - init: function(runtime) { - _runtime = runtime; - envVarExcludes = {}; - if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) { - runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true); + if (!oldConfig) { + oldConfig = { + flows:{}, + allNodes:{} } - }, - getEnvVar: function(k) { - if (!envVarExcludes[k]) { - return process.env[k]; + } + var changedSubflows = {}; + + var added = {}; + var removed = {}; + var changed = {}; + var wiringChanged = {}; + + var linkMap = {}; + + var changedTabs = {}; + + // Look for tabs that have been removed + for (id in oldConfig.flows) { + if (oldConfig.flows.hasOwnProperty(id) && (!newConfig.flows.hasOwnProperty(id))) { + removed[id] = oldConfig.allNodes[id]; } - return undefined; - }, + } - diffNodes, - mapEnvVarProperties, - evaluateEnvProperties, - parseConfig, - - diffConfigs: function(oldConfig, newConfig) { - var id; - var node; - var nn; - var wires; - var j,k; - - if (!oldConfig) { - oldConfig = { - flows:{}, - allNodes:{} - } - } - var changedSubflows = {}; - - var added = {}; - var removed = {}; - var changed = {}; - var wiringChanged = {}; - - var linkMap = {}; - - var changedTabs = {}; - - // Look for tabs that have been removed - for (id in oldConfig.flows) { - if (oldConfig.flows.hasOwnProperty(id) && (!newConfig.flows.hasOwnProperty(id))) { - removed[id] = oldConfig.allNodes[id]; - } - } - - // Look for tabs that have been disabled - for (id in oldConfig.flows) { - if (oldConfig.flows.hasOwnProperty(id) && newConfig.flows.hasOwnProperty(id)) { - var originalState = oldConfig.flows[id].disabled||false; - var newState = newConfig.flows[id].disabled||false; - if (originalState !== newState) { - changedTabs[id] = true; - if (originalState) { - added[id] = oldConfig.allNodes[id]; - } else { - removed[id] = oldConfig.allNodes[id]; - } + // Look for tabs that have been disabled + for (id in oldConfig.flows) { + if (oldConfig.flows.hasOwnProperty(id) && newConfig.flows.hasOwnProperty(id)) { + var originalState = oldConfig.flows[id].disabled||false; + var newState = newConfig.flows[id].disabled||false; + if (originalState !== newState) { + changedTabs[id] = true; + if (originalState) { + added[id] = oldConfig.allNodes[id]; + } else { + removed[id] = oldConfig.allNodes[id]; } } } + } - for (id in oldConfig.allNodes) { - if (oldConfig.allNodes.hasOwnProperty(id)) { - node = oldConfig.allNodes[id]; - if (node.type !== 'tab') { - // build the map of what this node was previously wired to - if (node.wires) { - linkMap[node.id] = linkMap[node.id] || []; - for (j=0;j 0) { - var subflowId = changedSubflowStack.pop(); - for (id in newConfig.allNodes) { - if (newConfig.allNodes.hasOwnProperty(id)) { - node = newConfig.allNodes[id]; - if (node.type === 'subflow:'+subflowId) { - if (!changed[node.id]) { - changed[node.id] = node; - if (!changed[changed[node.id].z] && newConfig.allNodes[changed[node.id].z]) { - changed[changed[node.id].z] = newConfig.allNodes[changed[node.id].z]; - if (newConfig.allNodes[changed[node.id].z].type === "subflow") { - // This subflow instance is inside a subflow. Add the - // containing subflow to the stack to mark - changedSubflowStack.push(changed[node.id].z); - delete changed[node.id]; + var changeOrigin = changed[node[prop]]; + if (changeOrigin || removed[node[prop]]) { + if (!changed[node.id]) { + if (changeOrigin && + (prop === "g") && + (changeOrigin.type === "group")) { + var oldNode = oldConfig.allNodes[node.id]; + // ignore change of group node + // if group of this node not changed + if (oldNode && + (node.g === oldNode.g)) { + continue; + } + } + madeChange = true; + changed[node.id] = node; + // This node exists within subflow template + // Mark the template as having changed + if (newConfig.allNodes[node.z]) { + changed[node.z] = newConfig.allNodes[node.z]; + if (changed[node.z].type === "subflow") { + changedSubflows[node.z] = changed[node.z]; + } } } } @@ -577,58 +537,96 @@ module.exports = { } } } + } while (madeChange===true) - var diff = { - added:Object.keys(added), - changed:Object.keys(changed), - removed:Object.keys(removed), - rewired:Object.keys(wiringChanged), - linked:[] + // Find any nodes that exist on a subflow template and remove from changed + // list as the parent subflow will now be marked as containing a change + for (id in newConfig.allNodes) { + if (newConfig.allNodes.hasOwnProperty(id)) { + node = newConfig.allNodes[id]; + if (newConfig.allNodes[node.z] && newConfig.allNodes[node.z].type === "subflow") { + delete changed[node.id]; + } } + } - // Traverse the links of all modified nodes to mark the connected nodes - var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired); - var visited = {}; - while (modifiedNodes.length > 0) { - node = modifiedNodes.pop(); - if (!visited[node]) { - visited[node] = true; - if (linkMap[node]) { - if (!changed[node] && !added[node] && !removed[node] && !wiringChanged[node]) { - diff.linked.push(node); + // Recursively mark all instances of changed subflows as changed + var changedSubflowStack = Object.keys(changedSubflows); + while (changedSubflowStack.length > 0) { + var subflowId = changedSubflowStack.pop(); + for (id in newConfig.allNodes) { + if (newConfig.allNodes.hasOwnProperty(id)) { + node = newConfig.allNodes[id]; + if (node.type === 'subflow:'+subflowId) { + if (!changed[node.id]) { + changed[node.id] = node; + if (!changed[changed[node.id].z] && newConfig.allNodes[changed[node.id].z]) { + changed[changed[node.id].z] = newConfig.allNodes[changed[node.id].z]; + if (newConfig.allNodes[changed[node.id].z].type === "subflow") { + // This subflow instance is inside a subflow. Add the + // containing subflow to the stack to mark + changedSubflowStack.push(changed[node.id].z); + delete changed[node.id]; + } + } } - modifiedNodes = modifiedNodes.concat(linkMap[node]); } } } - // console.log(diff); - // for (id in newConfig.allNodes) { - // console.log( - // (added[id]?"a":(changed[id]?"c":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"l":" "), - // newConfig.allNodes[id].type.padEnd(10), - // id.padEnd(16), - // (newConfig.allNodes[id].z||"").padEnd(16), - // newConfig.allNodes[id].name||newConfig.allNodes[id].label||"" - // ); - // } - // for (id in removed) { - // console.log( - // "- "+(diff.linked.indexOf(id)!==-1?"~":" "), - // id, - // oldConfig.allNodes[id].type, - // oldConfig.allNodes[id].name||oldConfig.allNodes[id].label||"" - // ); - // } + } - return diff; - }, + var diff = { + added:Object.keys(added), + changed:Object.keys(changed), + removed:Object.keys(removed), + rewired:Object.keys(wiringChanged), + linked:[] + } - /** - * Create a new instance of a node - * @param {Flow} flow The containing flow - * @param {object} config The node configuration object - * @return {Node} The instance of the node - */ - createNode: createNode, + // Traverse the links of all modified nodes to mark the connected nodes + var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired); + var visited = {}; + while (modifiedNodes.length > 0) { + node = modifiedNodes.pop(); + if (!visited[node]) { + visited[node] = true; + if (linkMap[node]) { + if (!changed[node] && !added[node] && !removed[node] && !wiringChanged[node]) { + diff.linked.push(node); + } + modifiedNodes = modifiedNodes.concat(linkMap[node]); + } + } + } + // console.log(diff); + // for (id in newConfig.allNodes) { + // console.log( + // (added[id]?"a":(changed[id]?"c":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"l":" "), + // newConfig.allNodes[id].type.padEnd(10), + // id.padEnd(16), + // (newConfig.allNodes[id].z||"").padEnd(16), + // newConfig.allNodes[id].name||newConfig.allNodes[id].label||"" + // ); + // } + // for (id in removed) { + // console.log( + // "- "+(diff.linked.indexOf(id)!==-1?"~":" "), + // id, + // oldConfig.allNodes[id].type, + // oldConfig.allNodes[id].name||oldConfig.allNodes[id].label||"" + // ); + // } + + return diff; +} + +module.exports = { + init, + createNode, + parseConfig, + diffConfigs, + diffNodes, + getEnvVar, + mapEnvVarProperties, evaluateEnvProperties } From 7481b78b16acb848c3d1a47a4507ed45e21892af Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 10 Jul 2023 12:04:52 +0100 Subject: [PATCH 6/8] Force reeval of env vars if group/flow/global envs change --- .../@node-red/runtime/lib/flows/Flow.js | 40 +++++----- .../@node-red/runtime/lib/flows/index.js | 9 ++- .../@node-red/runtime/lib/flows/util.js | 75 +++++++++++++++---- 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index e7ca490d2..ddea3f07f 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -53,27 +53,8 @@ class Flow { this.isGlobalFlow = false; } this.id = this.flow.id || "global"; - - // Initialise the group objects. These must be done in the right order - // starting from outer-most to inner-most so that the parent hierarchy - // is maintained. this.groups = {} this.groupOrder = [] - const groupIds = Object.keys(this.flow.groups || {}) - while (groupIds.length > 0) { - const id = groupIds.shift() - const groupDef = this.flow.groups[id] - if (!groupDef.g || this.groups[groupDef.g]) { - // The parent of this group is available - either another group - // or the top-level flow (this) - const parent = this.groups[groupDef.g] || this - this.groups[groupDef.id] = new Group(parent, groupDef) - this.groupOrder.push(groupDef.id) - } else { - // Try again once we've processed the other groups - groupIds.push(id) - } - } this.activeNodes = {}; this.subflowInstanceNodes = {}; this.catchNodes = []; @@ -190,6 +171,27 @@ class Flow { this._env = { ...this._env, ...await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) } } + // Initialise the group objects. These must be done in the right order + // starting from outer-most to inner-most so that the parent hierarchy + // is maintained. + this.groups = {} + this.groupOrder = [] + const groupIds = Object.keys(this.flow.groups || {}) + while (groupIds.length > 0) { + const id = groupIds.shift() + const groupDef = this.flow.groups[id] + if (!groupDef.g || this.groups[groupDef.g]) { + // The parent of this group is available - either another group + // or the top-level flow (this) + const parent = this.groups[groupDef.g] || this + this.groups[groupDef.id] = new Group(parent, groupDef) + this.groupOrder.push(groupDef.id) + } else { + // Try again once we've processed the other groups + groupIds.push(id) + } + } + for (let i = 0; i < this.groupOrder.length; i++) { // Start the groups in the right order so they // can setup their env vars knowning their parent diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index 16bd8a867..89ba9d822 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -271,6 +271,10 @@ function getFlows() { async function start(type,diff,muteLog,isDeploy) { type = type || "full"; + if (diff && diff.globalConfigChanged) { + type = 'full' + } + started = true; state = 'start' var i; @@ -441,6 +445,9 @@ function stop(type,diff,muteLog,isDeploy) { log.info(log._("nodes.flows.stopping-flows")); } } + if (diff.globalConfigChanged) { + type = 'full' + } started = false; state = 'stop' var promises = []; @@ -464,7 +471,7 @@ function stop(type,diff,muteLog,isDeploy) { activeFlowIds.forEach(id => { if (activeFlows.hasOwnProperty(id)) { - var flowStateChanged = diff && (diff.added.indexOf(id) !== -1 || diff.removed.indexOf(id) !== -1); + var flowStateChanged = diff && (diff.changed.indexOf(id) !== -1 || diff.added.indexOf(id) !== -1 || diff.removed.indexOf(id) !== -1); log.debug("red/nodes/flows.stop : stopping flow : "+id); promises.push(activeFlows[id].stop(flowStateChanged?null:stopList,removedList)); if (type === "full" || flowStateChanged || diff.removed.indexOf(id)!==-1) { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index e619e2b2a..ed30a3816 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -34,8 +34,11 @@ function diffNodes(oldNode,newNode) { if (oldNode == null) { return true; } - var oldKeys = Object.keys(oldNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" }); - var newKeys = Object.keys(newNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" }); + const keyFilter = p => p != 'x' && p != 'y' && p != 'wires' + const groupKeyFilter = p => keyFilter(p) && p != 'nodes' && p != 'style' && p != 'w' && p != 'h' + var oldKeys = Object.keys(oldNode).filter(oldNode.type === 'group' ? groupKeyFilter : keyFilter); + var newKeys = Object.keys(newNode).filter(newNode.type === 'group' ? groupKeyFilter : keyFilter); + if (oldKeys.length != newKeys.length) { return true; } @@ -354,10 +357,9 @@ function diffConfigs(oldConfig, newConfig) { var removed = {}; var changed = {}; var wiringChanged = {}; - + var globalConfigChanged = false; var linkMap = {}; - - var changedTabs = {}; + var allNestedGroups = [] // Look for tabs that have been removed for (id in oldConfig.flows) { @@ -372,7 +374,6 @@ function diffConfigs(oldConfig, newConfig) { var originalState = oldConfig.flows[id].disabled||false; var newState = newConfig.flows[id].disabled||false; if (originalState !== newState) { - changedTabs[id] = true; if (originalState) { added[id] = oldConfig.allNodes[id]; } else { @@ -435,6 +436,9 @@ function diffConfigs(oldConfig, newConfig) { delete changed[id]; } } + if (newConfig.allNodes[id].type === 'global-config') { + globalConfigChanged = true + } } // This node's wiring has changed if (!redUtil.compareObjects(node.wires,newConfig.allNodes[id].wires)) { @@ -450,6 +454,10 @@ function diffConfigs(oldConfig, newConfig) { } } } + } else { + if (JSON.stringify(node.env) !== JSON.stringify(newConfig.allNodes[id].env)) { + changed[id] = newConfig.allNodes[id]; + } } } } @@ -457,6 +465,20 @@ function diffConfigs(oldConfig, newConfig) { for (id in newConfig.allNodes) { if (newConfig.allNodes.hasOwnProperty(id)) { node = newConfig.allNodes[id]; + if (node.type === 'group') { + if (node.g) { + allNestedGroups.push(node) + } + if (changed[node.id]) { + if (node.nodes) { + node.nodes.forEach(nid => { + if (!changed[nid]) { + changed[nid] = true + } + }) + } + } + } // build the map of what this node is now wired to if (node.wires) { linkMap[node.id] = linkMap[node.id] || []; @@ -550,6 +572,26 @@ function diffConfigs(oldConfig, newConfig) { } } + // Recursively mark all children of changed groups as changed + do { + madeChange = false + for (let i = 0; i < allNestedGroups.length; i++) { + const group = allNestedGroups[i] + if (!changed[group.id] && group.g && changed[group.g]) { + changed[group.id] = true + madeChange = true + } + if (changed[group.id] && group.nodes) { + group.nodes.forEach(nid => { + if (!changed[nid]) { + changed[nid] = true + madeChange = true + } + }) + } + } + } while(madeChange) + // Recursively mark all instances of changed subflows as changed var changedSubflowStack = Object.keys(changedSubflows); while (changedSubflowStack.length > 0) { @@ -575,12 +617,15 @@ function diffConfigs(oldConfig, newConfig) { } } + + var diff = { added:Object.keys(added), changed:Object.keys(changed), removed:Object.keys(removed), rewired:Object.keys(wiringChanged), - linked:[] + linked:[], + globalConfigChanged } // Traverse the links of all modified nodes to mark the connected nodes @@ -600,13 +645,15 @@ function diffConfigs(oldConfig, newConfig) { } // console.log(diff); // for (id in newConfig.allNodes) { - // console.log( - // (added[id]?"a":(changed[id]?"c":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"l":" "), - // newConfig.allNodes[id].type.padEnd(10), - // id.padEnd(16), - // (newConfig.allNodes[id].z||"").padEnd(16), - // newConfig.allNodes[id].name||newConfig.allNodes[id].label||"" - // ); + // if (added[id] || changed[id] || wiringChanged[id] || diff.linked.indexOf(id)!==-1) { + // console.log( + // (added[id]?"a":(changed[id]?"c":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"l":" "), + // newConfig.allNodes[id].type.padEnd(10), + // id.padEnd(16), + // (newConfig.allNodes[id].z||"").padEnd(16), + // newConfig.allNodes[id].name||newConfig.allNodes[id].label||"" + // ); + // } // } // for (id in removed) { // console.log( From a5066d529f986b5330d96e8a30894cf10b8d5451 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 10 Jul 2023 12:30:36 +0100 Subject: [PATCH 7/8] Use flowChanged in diff to mark flows to restart --- packages/node_modules/@node-red/runtime/lib/flows/index.js | 5 +++-- packages/node_modules/@node-red/runtime/lib/flows/util.js | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index 89ba9d822..fa6649263 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -436,7 +436,8 @@ function stop(type,diff,muteLog,isDeploy) { changed:[], removed:[], rewired:[], - linked:[] + linked:[], + flowChanged:[] }; if (!muteLog) { if (type !== "full") { @@ -471,7 +472,7 @@ function stop(type,diff,muteLog,isDeploy) { activeFlowIds.forEach(id => { if (activeFlows.hasOwnProperty(id)) { - var flowStateChanged = diff && (diff.changed.indexOf(id) !== -1 || diff.added.indexOf(id) !== -1 || diff.removed.indexOf(id) !== -1); + var flowStateChanged = diff && (diff.flowChanged.indexOf(id) !== -1 || diff.added.indexOf(id) !== -1 || diff.removed.indexOf(id) !== -1); log.debug("red/nodes/flows.stop : stopping flow : "+id); promises.push(activeFlows[id].stop(flowStateChanged?null:stopList,removedList)); if (type === "full" || flowStateChanged || diff.removed.indexOf(id)!==-1) { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index ed30a3816..7eefe86c4 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -356,6 +356,7 @@ function diffConfigs(oldConfig, newConfig) { var added = {}; var removed = {}; var changed = {}; + var flowChanged = {}; var wiringChanged = {}; var globalConfigChanged = false; var linkMap = {}; @@ -454,9 +455,9 @@ function diffConfigs(oldConfig, newConfig) { } } } - } else { + } else if (!removed[id]) { if (JSON.stringify(node.env) !== JSON.stringify(newConfig.allNodes[id].env)) { - changed[id] = newConfig.allNodes[id]; + flowChanged[id] = newConfig.allNodes[id]; } } } @@ -625,6 +626,7 @@ function diffConfigs(oldConfig, newConfig) { removed:Object.keys(removed), rewired:Object.keys(wiringChanged), linked:[], + flowChanged: Object.keys(flowChanged), globalConfigChanged } From a3e41d4f3598ae93fa5d2ec26108c498477e9e78 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 11 Jul 2023 21:11:26 +0100 Subject: [PATCH 8/8] Update packages/node_modules/@node-red/util/lib/util.js Co-authored-by: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> --- packages/node_modules/@node-red/util/lib/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 4ae0032b0..ea5ffbbe3 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -526,7 +526,7 @@ function setObjectProperty(msg,prop,value,createMissing) { return true; } -/*! +/** * Get value of environment variable. * @param {Node} node - accessing node * @param {String} name - name of variable