This commit is contained in:
gongwenxin
2025-08-28 16:20:24 +08:00
parent 61e533cddf
commit 73e887ebf4
1539 changed files with 360926 additions and 0 deletions
@@ -0,0 +1,857 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const clone = require("clone");
const redUtil = require("@node-red/util").util;
const events = require("@node-red/util").events;
const flowUtil = require("./util");
const context = require('../nodes/context');
const hooks = require("@node-red/util").hooks;
const credentials = require("../nodes/credentials");
let Subflow;
let Log;
let Group;
let nodeCloseTimeout = 15000;
let asyncMessageDelivery = true;
/**
* This class represents a flow within the runtime. It is responsible for
* creating, starting and stopping all nodes within the flow.
*/
class Flow {
/**
* Create a Flow object.
* @param {[type]} parent The parent flow
* @param {[type]} globalFlow The global flow definition
* @param {[type]} flow This flow's definition
*/
constructor(parent,globalFlow,flow) {
this.TYPE = 'flow';
this.parent = parent;
this.global = globalFlow;
if (typeof flow === 'undefined') {
this.flow = globalFlow;
this.isGlobalFlow = true;
} else {
this.flow = flow;
this.isGlobalFlow = false;
}
this.id = this.flow.id || "global";
this.groups = {}
this.groupOrder = []
this.activeNodes = {};
this.subflowInstanceNodes = {};
this.catchNodes = [];
this.statusNodes = [];
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 = {}
}
/**
* Log a debug-level message from this flow
* @param {[type]} msg [description]
* @return {[type]} [description]
*/
debug(msg) {
Log.log({
id: this.id||"global",
level: Log.DEBUG,
type:this.TYPE,
msg:msg
})
}
/**
* Log an error-level message from this flow
* @param {[type]} msg [description]
* @return {[type]} [description]
*/
error(msg) {
Log.log({
id: this.id||"global",
level: Log.ERROR,
type:this.TYPE,
msg:msg
})
}
/**
* Log a info-level message from this flow
* @param {[type]} msg [description]
* @return {[type]} [description]
*/
info(msg) {
Log.log({
id: this.id||"global",
level: Log.INFO,
type:this.TYPE,
msg:msg
})
}
/**
* Log a trace-level message from this flow
* @param {[type]} msg [description]
* @return {[type]} [description]
*/
trace(msg) {
Log.log({
id: this.id||"global",
level: Log.TRACE,
type:this.TYPE,
msg:msg
})
}
/**
* [log description]
* @param {[type]} msg [description]
* @return {[type]} [description]
*/
log(msg) {
if (!msg.path) {
msg.path = this.path;
}
this.parent.log(msg);
}
/**
* Start this flow.
* The `diff` argument helps define what needs to be started in the case
* of a modified-nodes/flows type deploy.
* @param {[type]} msg [description]
* @return {[type]} [description]
*/
async start(diff) {
this.trace("start "+this.TYPE+" ["+this.path+"]");
var node;
var newNode;
var id;
this.catchNodes = [];
this.statusNodes = [];
this.completeNodeMap = {};
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 globalCreds = credentials.get(node.id)?.map || {}
const nodeEnv = await flowUtil.evaluateEnvProperties(this, node.env, globalCreds)
this._env = { ...this._env, ...nodeEnv }
}
}
}
if (this.env) {
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
// will have been started
await this.groups[this.groupOrder[i]].start()
}
var configNodes = Object.keys(this.flow.configs);
var configNodeAttempts = {};
while (configNodes.length > 0) {
id = configNodes.shift();
node = this.flow.configs[id];
if (!this.activeNodes[id]) {
if (node.d !== true) {
var readyToCreate = true;
// This node doesn't exist.
// Check it doesn't reference another non-existent config node
for (var prop in node) {
if (node.hasOwnProperty(prop) &&
prop !== 'id' &&
prop !== 'wires' &&
prop !== '_users' &&
this.flow.configs[node[prop]] &&
this.flow.configs[node[prop]].d !== true
) {
if (!this.activeNodes[node[prop]]) {
// References a non-existent config node
// Add it to the back of the list to try again later
configNodes.push(id);
configNodeAttempts[id] = (configNodeAttempts[id]||0)+1;
if (configNodeAttempts[id] === 100) {
throw new Error("Circular config node dependency detected: "+id);
}
readyToCreate = false;
break;
}
}
}
if (readyToCreate) {
newNode = await flowUtil.createNode(this,node);
if (newNode) {
this.activeNodes[id] = newNode;
}
}
} else {
this.debug("not starting disabled config node : "+id);
}
}
}
if (diff && diff.rewired) {
for (var j=0;j<diff.rewired.length;j++) {
var rewireNode = this.activeNodes[diff.rewired[j]];
if (rewireNode) {
rewireNode.updateWires(this.flow.nodes[rewireNode.id].wires);
}
}
}
for (id in this.flow.nodes) {
if (this.flow.nodes.hasOwnProperty(id)) {
node = this.flow.nodes[id];
if (node.d !== true) {
if (!node.subflow) {
if (!this.activeNodes[id]) {
newNode = await flowUtil.createNode(this,node);
if (newNode) {
this.activeNodes[id] = newNode;
}
}
} else {
if (!this.subflowInstanceNodes[id]) {
try {
var subflowDefinition = this.flow.subflows[node.subflow]||this.global.subflows[node.subflow]
// console.log("NEED TO CREATE A SUBFLOW",id,node.subflow);
this.subflowInstanceNodes[id] = true;
var subflow = Subflow.create(
this,
this.global,
subflowDefinition,
node
);
this.subflowInstanceNodes[id] = subflow;
await subflow.start();
this.activeNodes[id] = subflow.node;
// this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id});
// for (var i=0;i<nodes.length;i++) {
// if (nodes[i]) {
// this.activeNodes[nodes[i].id] = nodes[i];
// }
// }
} catch(err) {
console.log(err.stack)
}
}
}
} else {
this.debug("not starting disabled node : "+id);
}
}
}
var activeCount = Object.keys(this.activeNodes).length;
if (activeCount > 0) {
this.trace("------------------|--------------|-----------------");
this.trace(" id | type | alias");
this.trace("------------------|--------------|-----------------");
}
// Build the map of catch/status/complete nodes.
for (id in this.activeNodes) {
if (this.activeNodes.hasOwnProperty(id)) {
node = this.activeNodes[id];
this.trace(" "+id.padEnd(16)+" | "+node.type.padEnd(12)+" | "+(node._alias||"")+(node._zAlias?" [zAlias:"+node._zAlias+"]":""));
if (node.type === "catch") {
this.catchNodes.push(node);
} else if (node.type === "status") {
this.statusNodes.push(node);
} else if (node.type === "complete") {
if (node.scope) {
node.scope.forEach(id => {
this.completeNodeMap[id] = this.completeNodeMap[id] || [];
this.completeNodeMap[id].push(node);
})
}
}
}
}
this.catchNodes.sort(function(A,B) {
if (A.scope && !B.scope) {
return -1;
} else if (!A.scope && B.scope) {
return 1;
} else if (A.scope && B.scope) {
return 0;
} else if (A.uncaught && !B.uncaught) {
return 1;
} else if (!A.uncaught && B.uncaught) {
return -1;
}
return 0;
});
if (activeCount > 0) {
this.trace("------------------|--------------|-----------------");
}
// this.dump();
}
/**
* Stop this flow.
* The `stopList` argument helps define what needs to be stopped in the case
* of a modified-nodes/flows type deploy.
* @param {[type]} stopList [description]
* @param {[type]} removedList [description]
* @return {[type]} [description]
*/
stop(stopList, removedList) {
this.trace("stop "+this.TYPE);
var i;
if (!stopList) {
stopList = Object.keys(this.activeNodes);
}
// this.trace(" stopList: "+stopList.join(","))
// Convert the list to a map to avoid multiple scans of the list
var removedMap = {};
removedList = removedList || [];
removedList.forEach(function(id) {
removedMap[id] = true;
});
let nodesToStop = [];
let configsToStop = [];
stopList.forEach(id => {
if (this.flow.configs[id]) {
configsToStop.push(id);
} else {
nodesToStop.push(id);
}
});
stopList = nodesToStop.concat(configsToStop);
var promises = [];
for (i=0;i<stopList.length;i++) {
var node = this.activeNodes[stopList[i]];
if (node) {
delete this.activeNodes[stopList[i]];
if (this.subflowInstanceNodes[stopList[i]]) {
delete this.subflowInstanceNodes[stopList[i]];
}
try {
var removed = removedMap[stopList[i]];
promises.push(stopNode(node,removed).catch(()=>{}));
} catch(err) {
node.error(err);
}
if (removedMap[stopList[i]]) {
events.emit("node-status",{
id: node.id
});
}
}
}
return Promise.all(promises);
}
/**
* Update the flow definition. This doesn't change anything that is running.
* This should be called after `stop` and before `start`.
* @param {[type]} _global [description]
* @param {[type]} _flow [description]
* @return {[type]} [description]
*/
update(_global,_flow) {
this.global = _global;
this.flow = _flow;
}
/**
* Get a node instance from this flow. If the node is not known to this
* flow, pass the request up to the parent.
* @param {String} id [description]
* @param {Boolean} cancelBubble if true, prevents the flow from passing the request to the parent
* This stops infinite loops when the parent asked this Flow for the
* node to begin with.
* @return {[type]} [description]
*/
getNode(id, cancelBubble) {
if (!id) {
return undefined;
}
// console.log((new Error().stack).toString().split("\n").slice(1,3).join("\n"))
if ((this.flow.configs && this.flow.configs[id]) || (this.flow.nodes && this.flow.nodes[id] && this.flow.nodes[id].type.substring(0,8) != "subflow:")) {
// This is a node owned by this flow, so return whatever we have got
// During a stop/restart, activeNodes could be null for this id
return this.activeNodes[id];
} else if (this.activeNodes[id]) {
// TEMP: this is a subflow internal node within this flow or subflow instance node
return this.activeNodes[id];
} else if (this.subflowInstanceNodes[id]) {
return this.subflowInstanceNodes[id];
} else if (cancelBubble) {
// The node could be inside one of this flow's subflows
var node;
for (var sfId in this.subflowInstanceNodes) {
if (this.subflowInstanceNodes.hasOwnProperty(sfId)) {
node = this.subflowInstanceNodes[sfId].getNode(id,cancelBubble);
if (node) {
return node;
}
}
}
} else {
// Node not found inside this flow - ask the parent
return this.parent.getNode(id);
}
return undefined;
}
/**
* Get a group node instance
* @param {String} id
* @return {Node} group node
*/
getGroupNode(id) {
return this.groups[id];
}
/**
* Get all of the nodes instantiated within this flow
* @return {[type]} [description]
*/
getActiveNodes() {
return this.activeNodes;
}
/**
* Get a flow setting value.
* @param {[type]} key [description]
* @return {[type]} [description]
*/
getSetting(key) {
const flow = this.flow;
if (key === "NR_FLOW_NAME") {
return flow.label;
}
if (key === "NR_FLOW_ID") {
return flow.id;
}
if (!key.startsWith("$parent.")) {
if (this._env.hasOwnProperty(key)) {
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
}
} else {
key = key.substring(8);
}
// Delegate to the parent flow.
return this.parent.getSetting(key);
}
/**
* Handle a status event from a node within this flow.
* @param {Node} node The original node that triggered the event
* @param {Object} statusMessage The status object
* @param {Node} reportingNode The node emitting the status event.
* This could be a subflow instance node when the status
* is being delegated up.
* @param {boolean} muteStatusEvent Whether to emit the status event
* @return {[type]} [description]
*/
handleStatus(node,statusMessage,reportingNode,muteStatusEvent) {
if (!reportingNode) {
reportingNode = node;
}
if (!muteStatusEvent) {
if (statusMessage.hasOwnProperty("text") && typeof statusMessage.text !== "string") {
try {
statusMessage.text = statusMessage.text.toString();
}
catch(e) {}
}
events.emit("node-status",{
id: node.id,
status:statusMessage
});
}
let handled = false;
if (this.id === 'global' && node.users) {
// This is a global config node
// Delegate status to any nodes using this config node
for (let userNode in node.users) {
if (node.users.hasOwnProperty(userNode)) {
handled = node.users[userNode]._flow.handleStatus(node,statusMessage,node.users[userNode],true) || handled;
}
}
} else {
const candidateNodes = [];
this.statusNodes.forEach(targetStatusNode => {
if (targetStatusNode.g && targetStatusNode.scope === 'group' && !reportingNode.g) {
// Status node inside a group, reporting node not in a group - skip it
return
}
if (Array.isArray(targetStatusNode.scope) && targetStatusNode.scope.indexOf(reportingNode.id) === -1) {
return;
}
let distance = 0
if (reportingNode.g) {
// Reporting node inside a group. Calculate the distance between it and the status node
let containingGroup = this.groups[reportingNode.g]
while (containingGroup && containingGroup.id !== targetStatusNode.g) {
distance++
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
// the reporting node is in
return
}
}
candidateNodes.push({ d: distance, n: targetStatusNode })
})
candidateNodes.sort((A,B) => {
return A.d - B.d
})
candidateNodes.forEach(candidate => {
const targetStatusNode = candidate.n
var message = {
status: clone(statusMessage)
}
if (statusMessage.hasOwnProperty("text")) {
message.status.text = statusMessage.text.toString();
}
message.status.source = {
id: node.id,
type: node.type,
name: node.name
}
targetStatusNode.receive(message);
handled = true;
});
}
return handled;
}
/**
* Handle an error event from a node within this flow. If there are no Catch
* nodes within this flow, pass the event to the parent flow.
* @param {[type]} node [description]
* @param {[type]} logMessage [description]
* @param {[type]} msg [description]
* @param {[type]} reportingNode [description]
* @return {[type]} [description]
*/
handleError(node,logMessage,msg,reportingNode) {
if (!reportingNode) {
reportingNode = node;
}
// console.log("HE",logMessage);
var count = 1;
if (msg && msg.hasOwnProperty("error") && msg.error) {
if (msg.error.hasOwnProperty("source") && msg.error.source) {
if (msg.error.source.id === node.id) {
count = msg.error.source.count+1;
if (count === 10) {
node.warn(Log._("nodes.flow.error-loop"));
return false;
}
}
}
}
let handled = false;
if (this.id === 'global' && node.users) {
// This is a global config node
// Delegate status to any nodes using this config node
for (let userNode in node.users) {
if (node.users.hasOwnProperty(userNode)) {
handled = node.users[userNode]._flow.handleError(node,logMessage,msg,node.users[userNode]) || handled;
}
}
} else {
const candidateNodes = [];
this.catchNodes.forEach(targetCatchNode => {
if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) {
// Catch node inside a group, reporting node not in a group - skip it
return
}
if (Array.isArray(targetCatchNode.scope) && targetCatchNode.scope.indexOf(reportingNode.id) === -1) {
// Catch node has a scope set and it doesn't include the reporting node
return;
}
let distance = 0
if (reportingNode.g) {
// Reporting node inside a group. Calculate the distance between it and the catch node
let containingGroup = this.groups[reportingNode.g]
while (containingGroup && containingGroup.id !== targetCatchNode.g) {
distance++
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
// the reporting node is in
return
}
}
candidateNodes.push({ d: distance, n: targetCatchNode })
})
candidateNodes.sort((A,B) => {
return A.d - B.d
})
let handledByUncaught = false
candidateNodes.forEach(candidate => {
const targetCatchNode = candidate.n
if (targetCatchNode.uncaught && !handledByUncaught) {
// This node only wants errors that haven't already been handled
if (handled) {
return
}
handledByUncaught = true
}
let errorMessage;
if (msg) {
errorMessage = redUtil.cloneMessage(msg);
} else {
errorMessage = {};
}
if (errorMessage.hasOwnProperty("error")) {
errorMessage._error = errorMessage.error;
}
errorMessage.error = {
message: logMessage.toString(),
source: {
id: node.id,
type: node.type,
name: node.name,
count: count
}
};
if (logMessage.hasOwnProperty('code')) {
errorMessage.error.code = logMessage.code;
}
if (logMessage.hasOwnProperty('stack')) {
errorMessage.error.stack = logMessage.stack;
}
if (logMessage.hasOwnProperty('cause')) {
errorMessage.error.cause = logMessage.cause;
}
targetCatchNode.receive(errorMessage);
handled = true;
});
}
return handled;
}
handleComplete(node,msg) {
if (this.completeNodeMap[node.id]) {
let toSend = msg;
this.completeNodeMap[node.id].forEach((completeNode,index) => {
toSend = redUtil.cloneMessage(msg);
completeNode.receive(toSend);
})
}
}
send(sendEvents) {
// onSend - passed an array of SendEvent objects. The messages inside these objects are exactly what the node has passed to node.send - meaning there could be duplicate references to the same message object.
// preRoute - called once for each SendEvent object in turn
// preDeliver - the local router has identified the node it is going to send to. At this point, the message has been cloned if needed.
// postDeliver - the message has been dispatched to be delivered asynchronously (unless the sync delivery flag is set, in which case it would be continue as synchronous delivery)
// onReceive - a node is about to receive a message
// postReceive - the message has been passed to the node's input handler
// onDone, onError - the node has completed with a message or logged an error
handleOnSend(this,sendEvents, (err, eventData) => {
if (err) {
let srcNode;
if (Array.isArray(eventData)) {
srcNode = eventData[0].source.node;
} else {
srcNode = eventData.source.node;
}
srcNode.error(err);
}
});
}
getContext(scope) {
if (scope === 'flow') {
return this.context
} else if (scope === 'global') {
return context.get('global')
}
}
dump() {
console.log("==================")
console.log(this.TYPE, this.id);
for (var id in this.activeNodes) {
if (this.activeNodes.hasOwnProperty(id)) {
var node = this.activeNodes[id];
console.log(" ",id.padEnd(16),node.type)
if (node.wires) {
console.log(" -> ",node.wires)
}
}
}
console.log("==================")
}
}
/**
* Stop an individual node within this flow.
*
* @param {[type]} node [description]
* @param {[type]} removed [description]
* @return {[type]} [description]
*/
function stopNode(node,removed) {
Log.trace("Stopping node "+node.type+":"+node.id+(removed?" removed":""));
const start = Date.now();
const closePromise = node.close(removed);
let closeTimer = null;
const closeTimeout = new Promise((resolve,reject) => {
closeTimer = setTimeout(() => {
reject("Close timed out");
}, nodeCloseTimeout);
});
return Promise.race([closePromise,closeTimeout]).then(() => {
clearTimeout(closeTimer);
var delta = Date.now() - start;
Log.trace("Stopped node "+node.type+":"+node.id+" ("+delta+"ms)" );
}).catch(err => {
clearTimeout(closeTimer);
node.error(Log._("nodes.flows.stopping-error",{message:err}));
Log.debug(err.stack);
})
}
function handleOnSend(flow,sendEvents, reportError) {
// onSend - passed an array of SendEvent objects. The messages inside these objects are exactly what the node has passed to node.send - meaning there could be duplicate references to the same message object.
hooks.trigger("onSend",sendEvents,(err) => {
if (err) {
reportError(err,sendEvents);
return
} else if (err !== false) {
for (var i=0;i<sendEvents.length;i++) {
handlePreRoute(flow,sendEvents[i],reportError)
}
}
});
}
function handlePreRoute(flow, sendEvent, reportError) {
// preRoute - called once for each SendEvent object in turn
hooks.trigger("preRoute",sendEvent,(err) => {
if (err) {
reportError(err,sendEvent);
return;
} else if (err !== false) {
sendEvent.destination.node = flow.getNode(sendEvent.destination.id);
if (sendEvent.destination.node && typeof sendEvent.destination.node === 'object') {
if (sendEvent.cloneMessage) {
sendEvent.msg = redUtil.cloneMessage(sendEvent.msg);
}
handlePreDeliver(flow,sendEvent,reportError);
}
}
})
}
function deliverMessageToDestination(sendEvent) {
if (sendEvent?.destination?.node) {
try {
sendEvent.destination.node.receive(sendEvent.msg);
} catch(err) {
Log.error(`Error delivering message to node:${sendEvent.destination.node._path} [${sendEvent.destination.node.type}]`)
Log.error(err.stack)
}
}
}
function handlePreDeliver(flow,sendEvent, reportError) {
// preDeliver - the local router has identified the node it is going to send to. At this point, the message has been cloned if needed.
hooks.trigger("preDeliver",sendEvent,(err) => {
if (err) {
reportError(err,sendEvent);
return;
} else if (err !== false) {
if (asyncMessageDelivery) {
setImmediate(function() {
deliverMessageToDestination(sendEvent)
})
} else {
deliverMessageToDestination(sendEvent)
}
// postDeliver - the message has been dispatched to be delivered asynchronously (unless the sync delivery flag is set, in which case it would be continue as synchronous delivery)
hooks.trigger("postDeliver", sendEvent, function(err) {
if (err) {
reportError(err,sendEvent);
}
})
}
})
}
module.exports = {
init: function(runtime) {
nodeCloseTimeout = runtime.settings.nodeCloseTimeout || 15000;
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)
},
Flow: Flow
}
@@ -0,0 +1,64 @@
const flowUtil = require("./util");
const credentials = require("../nodes/credentials");
const clone = require("clone");
/**
* 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] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
}
} else {
key = key.substring(8);
}
return this.parent.getSetting(key);
}
error(msg) {
this.parent.error(msg);
}
getContext(scope) {
return this.parent.getContext(scope);
}
}
module.exports = {
Group
}
@@ -0,0 +1,568 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const clone = require("clone");
const jsonClone = require("rfdc")();
const Flow = require('./Flow').Flow;
const context = require('../nodes/context');
const util = require("util");
const redUtil = require("@node-red/util").util;
const events = require("@node-red/util").events;
const flowUtil = require("./util");
const credentials = require("../nodes/credentials");
/**
* Create deep copy of object
*/
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
/**
* Evaluate Input Value
*/
function evaluateInputValue(value, type, node) {
if (type === "bool") {
return (value === "true") || (value === true);
}
if (type === "cred") {
return value;
}
return redUtil.evaluateNodeProperty(value, type, node, null, null);
}
/**
* This class represents a subflow - which is handled as a special type of Flow
*/
class Subflow extends Flow {
/**
* Create a Subflow object.
* This takes a subflow definition and instance node, creates a clone of the
* definition with unique ids applied and passes to the super class.
* @param {[type]} parent [description]
* @param {[type]} globalFlow [description]
* @param {[type]} subflowDef [description]
* @param {[type]} subflowInstance [description]
*/
constructor(parent,globalFlow,subflowDef,subflowInstance) {
// console.log("CREATE SUBFLOW",subflowDef.id,subflowInstance.id,"alias?",subflowInstance._alias);
// console.log("SubflowInstance\n"+JSON.stringify(subflowInstance," ",2));
// console.log("SubflowDef\n"+JSON.stringify(subflowDef," ",2));
var subflows = parent.flow.subflows;
var globalSubflows = parent.global.subflows;
var node_map = {};
var node;
var wires;
var i;
var subflowInternalFlowConfig = {
id: subflowInstance.id,
configs: {},
nodes: {},
groups: {},
subflows: {}
}
if (subflowDef.groups) {
// Clone all of the subflow group definitions and give them new IDs
for (i in subflowDef.groups) {
if (subflowDef.groups.hasOwnProperty(i)) {
node = createNodeInSubflow(subflowInstance.id,subflowDef.groups[i]);
node_map[node._alias] = node;
subflowInternalFlowConfig.groups[node.id] = node;
}
}
}
if (subflowDef.configs) {
// Clone all of the subflow config node definitions and give them new IDs
for (i in subflowDef.configs) {
if (subflowDef.configs.hasOwnProperty(i)) {
node = createNodeInSubflow(subflowInstance.id,subflowDef.configs[i]);
node_map[node._alias] = node;
subflowInternalFlowConfig.configs[node.id] = node;
}
}
}
if (subflowDef.nodes) {
// Clone all of the subflow node definitions and give them new IDs
for (i in subflowDef.nodes) {
if (subflowDef.nodes.hasOwnProperty(i)) {
node = createNodeInSubflow(subflowInstance.id,subflowDef.nodes[i]);
node_map[node._alias] = node;
subflowInternalFlowConfig.nodes[node.id] = node;
}
}
}
subflowInternalFlowConfig.subflows = jsonClone(subflowDef.subflows || {});
remapSubflowNodes(subflowInternalFlowConfig.configs,node_map);
remapSubflowNodes(subflowInternalFlowConfig.nodes,node_map);
remapSubflowNodes(subflowInternalFlowConfig.groups,node_map);
// console.log("Instance config\n",JSON.stringify(subflowInternalFlowConfig,"",2));
super(parent,globalFlow,subflowInternalFlowConfig);
this.TYPE = 'subflow';
this.subflowDef = subflowDef;
this.subflowInstance = subflowInstance;
this.node_map = node_map;
this.path = parent.path+"/"+(subflowInstance._alias||subflowInstance.id);
let id = subflowInstance.id;
if (subflowInstance._alias) {
id = subflowInstance._alias;
}
this.templateCredentials = credentials.get(subflowDef.id) || {};
this.instanceCredentials = credentials.get(id) || {};
var env = {};
if (this.subflowDef.env) {
this.subflowDef.env.forEach(e => {
env[e.name] = e;
if (e.type === "cred") {
e.value = this.templateCredentials[e.name];
}
});
}
if (this.subflowInstance.env) {
this.subflowInstance.env.forEach(e => {
var old = env[e.name];
var ui = old ? old.ui : null;
env[e.name] = e;
if (ui) {
env[e.name].ui = ui;
}
if (e.type === "cred") {
if (!old || this.instanceCredentials.hasOwnProperty(e.name) ) {
e.value = this.instanceCredentials[e.name];
} else if (old) {
e.value = this.templateCredentials[e.name];
}
}
});
}
this.env = Object.values(env);
}
/**
* Start the subflow.
* This creates a subflow instance node to handle the inbound messages. It also
* rewires an subflow internal node that is connected to an output so it is connected
* to the parent flow nodes the subflow instance is wired to.
* @param {[type]} diff [description]
* @return {[type]} [description]
*/
async start(diff) {
var self = this;
// Create a subflow node to accept inbound messages and route appropriately
var Node = require("../nodes/Node");
if (this.subflowDef.status) {
var subflowStatusConfig = {
id: this.subflowInstance.id+":status",
type: "subflow-status",
z: this.subflowInstance.id,
_flow: this.parent
}
this.statusNode = new Node(subflowStatusConfig);
this.statusNode.on("input", function(msg) {
if (msg.payload !== undefined) {
if (typeof msg.payload === "string") {
// if msg.payload is a String, use it as status text
self.node.status({text:msg.payload})
return;
} else if (Object.prototype.toString.call(msg.payload) === "[object Object]") {
if (msg.payload.hasOwnProperty('text') || msg.payload.hasOwnProperty('fill') || msg.payload.hasOwnProperty('shape') || Object.keys(msg.payload).length === 0) {
// msg.payload is an object that looks like a status object
self.node.status(msg.payload);
return;
}
}
// Anything else - inspect it and use as status text
var text = util.inspect(msg.payload);
if (text.length > 32) { text = text.substr(0,32) + "..."; }
self.node.status({text:text});
} else if (msg.status !== undefined) {
// if msg.status exists
if (msg.status.hasOwnProperty("text") && msg.status.text.indexOf("common.") === 0) {
msg.status.text = "node-red:"+msg.status.text;
}
self.node.status(msg.status)
}
})
}
var subflowInstanceConfig = {
id: this.subflowInstance.id,
type: this.subflowInstance.type,
g: this.subflowInstance.g,
z: this.subflowInstance.z,
name: this.subflowInstance.name,
wires: [],
_flow: this
}
if (this.subflowDef.in) {
subflowInstanceConfig.wires = this.subflowDef.in.map(function(n) { return n.wires.map(function(w) { return self.node_map[w.id].id;})})
subflowInstanceConfig._originalWires = jsonClone(subflowInstanceConfig.wires);
}
this.node = new Node(subflowInstanceConfig);
this.node.on("input", function(msg) { this.send(msg);});
// Called when the subflow instance node is being stopped
this.node.on("close", function(done) {
this.status({});
// Stop the complete subflow
self.stop().finally(done)
})
this.node.status = status => this.parent.handleStatus(this.node,status);
// Create a context instance
// console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z)
this._context = context.get(this._alias||this.id,this.z);
this.node._updateWires = this.node.updateWires;
this.node.updateWires = function(newWires) {
// Wire the subflow outputs
if (self.subflowDef.out) {
var node,wires,i,j;
// Restore the original wiring to the internal nodes
subflowInstanceConfig.wires = jsonClone(subflowInstanceConfig._originalWires);
for (i=0;i<self.subflowDef.out.length;i++) {
wires = self.subflowDef.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id != self.subflowDef.id) {
node = self.node_map[wires[j].id];
if (node && node._originalWires) {
node.wires = jsonClone(node._originalWires);
}
}
}
}
var modifiedNodes = {};
var subflowInstanceModified = false;
for (i=0;i<self.subflowDef.out.length;i++) {
wires = self.subflowDef.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id === self.subflowDef.id) {
subflowInstanceConfig.wires[wires[j].port] = subflowInstanceConfig.wires[wires[j].port].concat(newWires[i]);
subflowInstanceModified = true;
} else {
node = self.node_map[wires[j].id];
if (node) {
node.wires[wires[j].port] = node.wires[wires[j].port].concat(newWires[i]);
modifiedNodes[node.id] = node;
}
}
}
}
Object.keys(modifiedNodes).forEach(function(id) {
var node = modifiedNodes[id];
self.activeNodes[id].updateWires(node.wires);
});
if (subflowInstanceModified) {
self.node._updateWires(subflowInstanceConfig.wires);
}
}
};
// Wire the subflow outputs
if (this.subflowDef.out) {
for (var i=0;i<this.subflowDef.out.length;i++) {
// i: the output index
// This is what this Output is wired to
var wires = this.subflowDef.out[i].wires;
for (var j=0;j<wires.length;j++) {
if (wires[j].id === this.subflowDef.id) {
// A subflow input wired straight to a subflow output
subflowInstanceConfig.wires[wires[j].port] = subflowInstanceConfig.wires[wires[j].port].concat(this.subflowInstance.wires[i])
this.node._updateWires(subflowInstanceConfig.wires);
} else {
var node = self.node_map[wires[j].id];
if (node) {
if (!node._originalWires) {
node._originalWires = jsonClone(node.wires);
}
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(this.subflowInstance.wires[i]);
} else {
this.error("Unknown node referenced inside subflow: " + wires[j].id)
}
}
}
}
}
if (this.subflowDef.status) {
var subflowStatusId = this.statusNode.id;
wires = this.subflowDef.status.wires;
for (var j=0;j<wires.length;j++) {
if (wires[j].id === this.subflowDef.id) {
// A subflow input wired straight to a subflow output
subflowInstanceConfig.wires[wires[j].port].push(subflowStatusId);
this.node._updateWires(subflowInstanceConfig.wires);
} else {
var node = self.node_map[wires[j].id];
if (node) {
if (!node._originalWires) {
node._originalWires = jsonClone(node.wires);
}
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]);
node.wires[wires[j].port].push(subflowStatusId);
} else {
this.error("Unknown node referenced inside subflow: " + wires[j].id)
}
}
}
}
return super.start(diff);
}
/**
* Stop this subflow.
* The `stopList` argument helps define what needs to be stopped in the case
* of a modified-nodes/flows type deploy.
* @param {[type]} stopList [description]
* @param {[type]} removedList [description]
* @return {[type]} [description]
*/
stop(stopList, removedList) {
const nodes = Object.keys(this.activeNodes);
return super.stop(stopList, removedList).then(res => {
nodes.forEach(id => {
events.emit("node-status",{
id: id
});
})
return res;
})
}
/**
* Get environment variable of subflow
* @param {String} key name of env var
* @return {Object} val value of env var
*/
getSetting(key) {
const node = this.subflowInstance;
if (node) {
if (key === "NR_NODE_NAME" || key === "NR_SUBFLOW_NAME") {
return node.name;
}
if (key === "NR_NODE_ID" || key === "NR_SUBFLOW_ID") {
return node.id;
}
if (key === "NR_NODE_PATH" || key === "NR_SUBFLOW_PATH") {
return node._path;
}
}
if (!key.startsWith("$parent.")) {
if (this._env.hasOwnProperty(key)) {
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
}
} else {
key = key.substring(8);
}
// 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 this.parent.getSetting(key)
}
/**
* Get a node instance from this subflow.
* If the subflow has a status node, check for that, otherwise use
* the super-class function
* @param {String} id [description]
* @param {Boolean} cancelBubble if true, prevents the flow from passing the request to the parent
* This stops infinite loops when the parent asked this Flow for the
* node to begin with.
* @return {[type]} [description]
*/
getNode(id, cancelBubble) {
if (this.statusNode && this.statusNode.id === id) {
return this.statusNode;
}
return super.getNode(id,cancelBubble);
}
/**
* Handle a status event from a node within this flow.
* @param {Node} node The original node that triggered the event
* @param {Object} statusMessage The status object
* @param {Node} reportingNode The node emitting the status event.
* This could be a subflow instance node when the status
* is being delegated up.
* @param {boolean} muteStatus Whether to emit the status event
* @return {[type]} [description]
*/
handleStatus(node,statusMessage,reportingNode,muteStatus) {
let handled = super.handleStatus(node,statusMessage,reportingNode,muteStatus);
if (!handled) {
if (!this.statusNode || node === this.node) {
// No status node on this subflow caught the status message.
// AND there is no Subflow Status node - so the user isn't
// wanting to manage status messages themselves
// Pass up to the parent with this subflow's instance as the
// reporting node
handled = this.parent.handleStatus(node,statusMessage,this.node,true);
}
}
return handled;
}
/**
* Handle an error event from a node within this flow. If there are no Catch
* nodes within this flow, pass the event to the parent flow.
* @param {[type]} node [description]
* @param {[type]} logMessage [description]
* @param {[type]} msg [description]
* @param {[type]} reportingNode [description]
* @return {[type]} [description]
*/
handleError(node,logMessage,msg,reportingNode) {
let handled = super.handleError(node,logMessage,msg,reportingNode);
if (!handled) {
// No catch node on this subflow caught the error message.
// Pass up to the parent with the subflow's instance as the
// reporting node.
handled = this.parent.handleError(node,logMessage,msg,this.node);
}
return handled;
}
}
/**
* Clone a node definition for use within a subflow instance.
* Give the node a new id and set its _alias property to record
* its association with the original node definition.
* @param {[type]} subflowInstanceId [description]
* @param {[type]} def [description]
* @return {[type]} [description]
*/
function createNodeInSubflow(subflowInstanceId, def) {
let node = jsonClone(def);
let nid = `${subflowInstanceId}-${node.id}` //redUtil.generateId();
// console.log("Create Node In subflow",node._alias, "--->",nid, "(",node.type,")")
// node_map[node.id] = node;
node._alias = node.id;
node.id = nid;
node.z = subflowInstanceId;
return node;
}
/**
* Given an object of {id:nodes} and a map of {old-id:node}, modifiy all
* properties in the nodes object to reference the new node ids.
* This handles:
* - node.wires,
* - node.scope of Complete, Catch and Status nodes,
* - node.XYZ for any property where XYZ is recognised as an old property
* @param {[type]} nodes [description]
* @param {[type]} nodeMap [description]
* @return {[type]} [description]
*/
function remapSubflowNodes(nodes,nodeMap) {
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
var node = nodes[id];
if (node.wires) {
var outputs = node.wires;
for (var j=0;j<outputs.length;j++) {
var wires = outputs[j];
for (var k=0;k<wires.length;k++) {
if (nodeMap[outputs[j][k]]) {
outputs[j][k] = nodeMap[outputs[j][k]].id
} else {
outputs[j][k] = null;
}
}
}
}
if ((node.type === 'complete' || node.type === 'catch' || node.type === 'status') && Array.isArray(node.scope)) {
node.scope = node.scope.map(function(id) {
return nodeMap[id]?nodeMap[id].id:""
})
} else {
for (var prop in node) {
if (node.hasOwnProperty(prop) && prop !== '_alias') {
if (nodeMap[node[prop]]) {
node[prop] = nodeMap[node[prop]].id;
}
}
}
}
}
}
}
class SubflowModule extends Subflow {
/**
* Create a Subflow Module object.
* This is a node that has been published as a subflow.
* @param {[type]} parent [description]
* @param {[type]} globalFlow [description]
* @param {[type]} subflowDef [description]
* @param {[type]} subflowInstance [description]
*/
constructor(type, parent,globalFlow,subflowDef,subflowInstance) {
super(parent,globalFlow,subflowDef,subflowInstance);
this.TYPE = `module:${type}`;
this.subflowType = type;
}
/**
* [log description]
* @param {[type]} msg [description]
* @return {[type]} [description]
*/
log(msg) {
if (msg.id) {
msg.id = this.id
}
if (msg.type) {
msg.type = this.subflowType
}
super.log(msg);
}
}
function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
}
function createModuleInstance(type, parent,globalFlow,subflowDef,subflowInstance) {
return new SubflowModule(type, parent,globalFlow,subflowDef,subflowInstance);
}
module.exports = {
init: function(runtime) {},
create: createSubflow,
createModuleInstance: createModuleInstance
}
@@ -0,0 +1,876 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const jsonClone = require("rfdc")();
var Flow = require('./Flow');
var typeRegistry = require("@node-red/registry");
var deprecated = typeRegistry.deprecated;
var context = require("../nodes/context")
var credentials = require("../nodes/credentials");
var flowUtil = require("./util");
var log;
const events = require("@node-red/util").events;
var redUtil = require("@node-red/util").util;
var storage = null;
var settings = null;
var activeConfig = null;
var activeFlowConfig = null;
var activeFlows = {};
var started = false;
var state = 'stop'
var credentialsPendingReset = false;
var activeNodesToFlow = {};
var typeEventRegistered = false;
function init(runtime) {
if (started) {
throw new Error("Cannot init without a stop");
}
settings = runtime.settings;
storage = runtime.storage;
log = runtime.log;
started = false;
state = 'stop';
if (!typeEventRegistered) {
events.on('type-registered',function(type) {
if (activeFlowConfig && activeFlowConfig.missingTypes.length > 0) {
var i = activeFlowConfig.missingTypes.indexOf(type);
if (i != -1) {
log.info(log._("nodes.flows.registered-missing", {type:type}));
activeFlowConfig.missingTypes.splice(i,1);
if (activeFlowConfig.missingTypes.length === 0 && started) {
events.emit("runtime-event",{id:"runtime-state",retain: true});
start();
}
}
}
});
typeEventRegistered = true;
}
Flow.init(runtime);
flowUtil.init(runtime);
}
function loadFlows() {
var config;
return storage.getFlows().then(function(_config) {
config = _config;
log.debug("loaded flow revision: "+config.rev);
return credentials.load(config.credentials).then(function() {
events.emit("runtime-event",{id:"runtime-state",retain:true});
return config;
});
}).catch(function(err) {
if (err.code === "credentials_load_failed" && !storage.projects) {
// project disabled, credential load failed
credentialsPendingReset = true;
log.warn(log._("nodes.flows.error",{message:err.toString()}));
events.emit("runtime-event",{id:"runtime-state",payload:{type:"warning",error:err.code,text:"notification.warnings.credentials_load_failed_reset"},retain:true});
return config;
} else {
activeConfig = null;
events.emit("runtime-event",{id:"runtime-state",payload:{type:"warning",error:err.code,project:err.project,text:"notification.warnings."+err.code},retain:true});
if (err.code === "project_not_found") {
log.warn(log._("storage.localfilesystem.projects.project-not-found",{project:err.project}));
} else {
log.warn(log._("nodes.flows.error",{message:err.toString()}));
}
throw err;
}
});
}
function load(forceStart) {
if (forceStart && settings.safeMode) {
// This is a force reload from the API - disable safeMode
delete settings.safeMode;
}
return setFlows(null,null,"load",false,forceStart);
}
/*
* _config - new node array configuration
* _credentials - new credentials configuration (optional)
* type - full/nodes/flows/load (default full)
* muteLog - don't emit the standard log messages (used for individual flow api)
*/
function setFlows(_config,_credentials,type,muteLog,forceStart,user) {
if (typeof _credentials === "string") {
type = _credentials;
_credentials = null;
}
type = type||"full";
if (settings.safeMode) {
if (type !== "load") {
// If in safeMode, the flows are stopped. We cannot do a modified nodes/flows
// type deploy as nothing is running. Can only do a "load" or "full" deploy.
// The "load" case is already handled in `load()` to distinguish between
// startup-load and api-request-load.
type = "full";
delete settings.safeMode;
}
}
var configSavePromise = null;
var config = null;
var diff;
var newFlowConfig;
var isLoad = false;
if (type === "load") {
isLoad = true;
configSavePromise = loadFlows().then(function(_config) {
config = jsonClone(_config.flows);
newFlowConfig = flowUtil.parseConfig(jsonClone(config));
type = "full";
return _config.rev;
});
} else {
// Clone the provided config so it can be manipulated
config = jsonClone(_config);
// Parse the configuration
newFlowConfig = flowUtil.parseConfig(jsonClone(config));
// Generate a diff to identify what has changed
diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig);
// Now the flows have been compared, remove any credentials from newFlowConfig
// so they don't cause false-positive diffs the next time a flow is deployed
for (var id in newFlowConfig.allNodes) {
if (newFlowConfig.allNodes.hasOwnProperty(id)) {
delete newFlowConfig.allNodes[id].credentials;
}
}
var credsDirty;
if (_credentials) {
if (_credentials['$']) {
// this is a set of encrypted credentials - pass to load to decrypt
// the complete set
configSavePromise = credentials.load(_credentials);
} else {
credentials.clean(config);
// A full set of credentials have been provided. Use those instead
let credentialSavePromises = [];
for (let id in _credentials) {
if (_credentials.hasOwnProperty(id)) {
credentialSavePromises.push(credentials.add(id,_credentials[id]));
}
}
configSavePromise = Promise.all(credentialSavePromises);
credsDirty = true;
}
} else {
// Allow the credential store to remove anything no longer needed
credentials.clean(config);
// Remember whether credentials need saving or not
var credsDirty = credentials.dirty();
configSavePromise = Promise.resolve();
}
// Get the latest credentials and ask storage to save them (if needed)
// as well as the new flow configuration.
configSavePromise = configSavePromise.then(function() {
return credentials.export()
}).then(function(creds) {
var saveConfig = {
flows: config,
credentialsDirty:credsDirty,
credentials: creds
}
return storage.saveFlows(saveConfig, user);
});
}
return configSavePromise.then(flowRevision => {
if (!isLoad) {
log.debug("saved flow revision: "+flowRevision);
}
activeConfig = {
flows:config,
rev:flowRevision
};
activeFlowConfig = newFlowConfig;
if (forceStart || started) {
// Flows are running (or should be)
// Stop the active flows (according to deploy type and the diff)
return stop(type,diff,muteLog,true).then(() => {
// Once stopped, allow context to remove anything no longer needed
return context.clean(activeFlowConfig)
}).then(() => {
if (!isLoad) {
log.info(log._("nodes.flows.updated-flows"));
}
// Start the active flows
start(type,diff,muteLog,true).then(() => {
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true});
});
// Return the new revision asynchronously to the actual start
return flowRevision;
}).catch(function(err) { })
} else {
if (!isLoad) {
log.info(log._("nodes.flows.updated-flows"));
}
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true});
return flowRevision;
}
});
}
function getNode(id) {
var node;
if (activeNodesToFlow[id] && activeFlows[activeNodesToFlow[id]]) {
return activeFlows[activeNodesToFlow[id]].getNode(id,true);
}
for (var flowId in activeFlows) {
if (activeFlows.hasOwnProperty(flowId)) {
node = activeFlows[flowId].getNode(id,true);
if (node) {
return node;
}
}
}
return null;
}
function eachNode(cb) {
for (var id in activeFlowConfig.allNodes) {
if (activeFlowConfig.allNodes.hasOwnProperty(id)) {
cb(activeFlowConfig.allNodes[id]);
}
}
}
function getFlows() {
return activeConfig;
}
async function start(type,diff,muteLog,isDeploy) {
type = type || "full";
if (diff && diff.globalConfigChanged) {
type = 'full'
}
started = true;
state = 'start'
var i;
// If there are missing types, report them, emit the necessary runtime event and return
if (activeFlowConfig.missingTypes.length > 0) {
log.info(log._("nodes.flows.missing-types"));
var knownUnknowns = 0;
for (i=0;i<activeFlowConfig.missingTypes.length;i++) {
var nodeType = activeFlowConfig.missingTypes[i];
var info = deprecated.get(nodeType);
if (info) {
log.info(log._("nodes.flows.missing-type-provided",{type:activeFlowConfig.missingTypes[i],module:info.module}));
knownUnknowns += 1;
} else {
log.info(" - "+activeFlowConfig.missingTypes[i]);
}
}
if (knownUnknowns > 0) {
log.info(log._("nodes.flows.missing-type-install-1"));
log.info(" npm install <module name>");
log.info(log._("nodes.flows.missing-type-install-2"));
log.info(" "+settings.userDir);
}
events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true});
return;
}
try {
await typeRegistry.checkFlowDependencies(activeConfig.flows);
} catch(err) {
log.info("Failed to load external modules required by this flow:");
const missingModules = [];
for (i=0;i<err.length;i++) {
let errMessage = err[i].error.toString()
missingModules.push({module:err[i].module.module, error: err[i].error.code || err[i].error.toString()})
log.info(` - ${err[i].module.spec} [${err[i].error.code || "unknown_error"}]`);
}
events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});
return;
}
// In safe mode, don't actually start anything, emit the necessary runtime event and return
if (settings.safeMode) {
log.info("*****************************************************************")
log.info(log._("nodes.flows.safe-mode"));
log.info("*****************************************************************")
state = 'safe'
events.emit("runtime-event",{id:"runtime-state",payload:{state: 'safe', error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true});
return;
}
let runtimeState
try {
runtimeState = settings.get('runtimeFlowState') || 'start'
} catch (err) {}
if (runtimeState === 'stop') {
log.info(log._("nodes.flows.stopped-flows"));
events.emit("runtime-event",{id:"runtime-state",payload:{ state: 'stop', deploy:isDeploy },retain:true});
state = 'stop'
started = false
return
}
if (!muteLog) {
if (type !== "full") {
log.info(log._("nodes.flows.starting-modified-"+type));
} else {
log.info(log._("nodes.flows.starting-flows"));
}
}
events.emit("flows:starting", {config: activeConfig, type: type, diff: diff})
var id;
if (type === "full") {
// A full start means everything should
// Check the 'global' flow is running
if (!activeFlows['global']) {
log.debug("red/nodes/flows.start : starting flow : global");
activeFlows['global'] = Flow.create(flowAPI,activeFlowConfig);
}
// Check each flow in the active configuration
for (id in activeFlowConfig.flows) {
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(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);
}
}
}
} else {
// A modified-type deploy means restarting things that have changed
// Update the global flow
if (activeFlows['global']) {
activeFlows['global'].update(activeFlowConfig,activeFlowConfig);
} else {
log.debug("red/nodes/flows.start : starting flow : global");
activeFlows['global'] = Flow.create(flowAPI,activeFlowConfig);
}
for (id in activeFlowConfig.flows) {
if (activeFlowConfig.flows.hasOwnProperty(id)) {
if (!activeFlowConfig.flows[id].disabled) {
if (activeFlows[id]) {
// This flow exists and is not disabled, so update it
activeFlows[id].update(activeFlowConfig,activeFlowConfig.flows[id]);
} else {
// This flow didn't previously exist, so create it
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);
}
}
}
}
for (id in activeFlows) {
if (activeFlows.hasOwnProperty(id)) {
try {
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) {
activeNodesToFlow[nid] = id;
});
} catch(err) {
console.log(err.stack);
}
}
}
events.emit("flows:started", {config: activeConfig, type: type, diff: diff});
// Deprecated event
events.emit("nodes-started");
if (credentialsPendingReset === true) {
credentialsPendingReset = false;
} else {
events.emit("runtime-event",{id:"runtime-state", payload:{ state: 'start', deploy:isDeploy}, retain:true});
}
if (!muteLog) {
if (type !== "full") {
log.info(log._("nodes.flows.started-modified-"+type));
} else {
log.info(log._("nodes.flows.started-flows"));
}
}
return;
}
function stop(type,diff,muteLog,isDeploy) {
if (!started) {
return Promise.resolve();
}
type = type||"full";
diff = diff||{
added:[],
changed:[],
removed:[],
rewired:[],
linked:[],
flowChanged:[]
};
if (!muteLog) {
if (type !== "full") {
log.info(log._("nodes.flows.stopping-modified-"+type));
} else {
log.info(log._("nodes.flows.stopping-flows"));
}
}
if (diff.globalConfigChanged) {
type = 'full'
}
started = false;
state = 'stop'
var promises = [];
var stopList;
var removedList = diff.removed;
if (type === 'nodes') {
stopList = diff.changed.concat(diff.removed);
} else if (type === 'flows') {
stopList = diff.changed.concat(diff.removed).concat(diff.linked).concat(diff.rewired);
}
events.emit("flows:stopping",{config: activeConfig, type: type, diff: diff})
// Stop the global flow object last
let activeFlowIds = Object.keys(activeFlows);
let globalIndex = activeFlowIds.indexOf("global");
if (globalIndex !== -1) {
activeFlowIds.splice(globalIndex,1);
activeFlowIds.push("global");
}
activeFlowIds.forEach(id => {
if (activeFlows.hasOwnProperty(id)) {
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) {
delete activeFlows[id];
}
}
});
return Promise.all(promises).then(function() {
for (let id in activeNodesToFlow) {
if (activeNodesToFlow.hasOwnProperty(id)) {
if (!activeFlows[activeNodesToFlow[id]]) {
delete activeNodesToFlow[id];
}
}
}
if (stopList) {
stopList.forEach(function(id) {
delete activeNodesToFlow[id];
});
}
if (!muteLog) {
if (type !== "full") {
log.info(log._("nodes.flows.stopped-modified-"+type));
} else {
log.info(log._("nodes.flows.stopped-flows"));
}
}
events.emit("flows:stopped",{config: activeConfig, type: type, diff: diff});
events.emit("runtime-event",{ id:"runtime-state", payload:{ state: 'stop', deploy:isDeploy }, retain:true });
// Deprecated event
events.emit("nodes-stopped");
});
}
function checkTypeInUse(id) {
var nodeInfo = typeRegistry.getNodeInfo(id);
if (!nodeInfo) {
throw new Error(log._("nodes.index.unrecognised-id", {id:id}));
} else {
var inUse = {};
var config = getFlows();
config.flows.forEach(function(n) {
inUse[n.type] = (inUse[n.type]||0)+1;
});
var nodesInUse = [];
nodeInfo.types.forEach(function(t) {
if (inUse[t]) {
nodesInUse.push(t);
}
});
if (nodesInUse.length > 0) {
var msg = nodesInUse.join(", ");
var err = new Error(log._("nodes.index.type-in-use", {msg:msg}));
err.code = "type_in_use";
throw err;
}
}
}
function updateMissingTypes() {
var subflowInstanceRE = /^subflow:(.+)$/;
activeFlowConfig.missingTypes = [];
for (var id in activeFlowConfig.allNodes) {
if (activeFlowConfig.allNodes.hasOwnProperty(id)) {
var node = activeFlowConfig.allNodes[id];
if (node.type !== 'tab' && node.type !== 'subflow') {
var subflowDetails = subflowInstanceRE.exec(node.type);
if ( (subflowDetails && !activeFlowConfig.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(node.type)) ) {
if (activeFlowConfig.missingTypes.indexOf(node.type) === -1) {
activeFlowConfig.missingTypes.push(node.type);
}
}
}
}
}
}
async function addFlow(flow, user) {
var i,node;
if (!flow.hasOwnProperty('nodes')) {
throw new Error('missing nodes property');
}
flow.id = redUtil.generateId();
var tabNode = {
type:'tab',
label:flow.label,
id:flow.id
}
if (flow.hasOwnProperty('info')) {
tabNode.info = flow.info;
}
if (flow.hasOwnProperty('disabled')) {
tabNode.disabled = flow.disabled;
}
if (flow.hasOwnProperty('env')) {
tabNode.env = flow.env;
}
var nodes = [tabNode];
for (i=0;i<flow.nodes.length;i++) {
node = flow.nodes[i];
if (activeFlowConfig.allNodes[node.id]) {
// TODO nls
throw new Error('duplicate id');
}
if (node.type === 'tab' || node.type === 'subflow') {
throw new Error('invalid node type: '+node.type);
}
node.z = flow.id;
nodes.push(node);
}
if (flow.configs) {
for (i=0;i<flow.configs.length;i++) {
node = flow.configs[i];
if (activeFlowConfig.allNodes[node.id]) {
// TODO nls
throw new Error('duplicate id');
}
if (node.type === 'tab' || node.type === 'subflow') {
throw new Error('invalid node type: '+node.type);
}
node.z = flow.id;
nodes.push(node);
}
}
var newConfig = jsonClone(activeConfig.flows);
newConfig = newConfig.concat(nodes);
return setFlows(newConfig, null, 'flows', true, null, user).then(function() {
log.info(log._("nodes.flows.added-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"}));
return flow.id;
});
}
function getFlow(id) {
var flow;
if (id === 'global') {
flow = activeFlowConfig;
} else {
flow = activeFlowConfig.flows[id];
}
if (!flow) {
return null;
}
var result = {
id: id
};
if (flow.label) {
result.label = flow.label;
}
if (flow.hasOwnProperty('disabled')) {
result.disabled = flow.disabled;
}
if (flow.hasOwnProperty('info')) {
result.info = flow.info;
}
if (flow.hasOwnProperty('env')) {
result.env = flow.env;
}
if (id !== 'global') {
result.nodes = [];
}
if (flow.groups) {
var nodeIds = Object.keys(flow.groups);
if (nodeIds.length > 0) {
nodeIds.forEach(function(nodeId) {
var node = jsonClone(flow.groups[nodeId]);
delete node.credentials;
result.nodes.push(node)
})
}
}
if (flow.nodes) {
var nodeIds = Object.keys(flow.nodes);
if (nodeIds.length > 0) {
nodeIds.forEach(function(nodeId) {
var node = jsonClone(flow.nodes[nodeId]);
if (node.type === 'link out') {
delete node.wires;
}
delete node.credentials;
result.nodes.push(node)
})
}
}
if (flow.configs) {
var configIds = Object.keys(flow.configs);
result.configs = configIds.map(function(configId) {
const node = jsonClone(flow.configs[configId]);
delete node.credentials;
return node
})
if (result.configs.length === 0) {
delete result.configs;
}
}
if (flow.subflows) {
var subflowIds = Object.keys(flow.subflows);
result.subflows = subflowIds.map(function(subflowId) {
var subflow = jsonClone(flow.subflows[subflowId]);
var nodeIds = Object.keys(subflow.nodes);
subflow.nodes = nodeIds.map(function(id) {
const node = jsonClone(subflow.nodes[id])
delete node.credentials
return node
});
if (subflow.groups) {
var nodeIds = Object.keys(subflow.groups);
if (nodeIds.length > 0) {
nodeIds.forEach(function(nodeId) {
var node = jsonClone(subflow.groups[nodeId]);
delete node.credentials;
subflow.nodes.push(node)
})
}
delete subflow.groups
}
if (subflow.configs) {
var configIds = Object.keys(subflow.configs);
subflow.configs = configIds.map(function(id) {
const node = jsonClone(subflow.configs[id])
delete node.credentials
return node
})
}
delete subflow.instances;
return subflow;
});
if (result.subflows.length === 0) {
delete result.subflows;
}
}
return result;
}
async function updateFlow(id,newFlow, user) {
var label = id;
if (id !== 'global') {
if (!activeFlowConfig.flows[id]) {
var e = new Error();
e.code = 404;
throw e;
}
label = activeFlowConfig.flows[id].label;
}
var newConfig = jsonClone(activeConfig.flows);
var nodes;
if (id === 'global') {
// Remove all nodes whose z is not a known flow
// When subflows can be owned by a flow, this logic will have to take
// that into account
newConfig = newConfig.filter(function(node) {
return node.type === 'tab' || (node.hasOwnProperty('z') && activeFlowConfig.flows.hasOwnProperty(node.z));
})
// Add in the new config nodes
nodes = newFlow.configs||[];
if (newFlow.subflows) {
// Add in the new subflows
newFlow.subflows.forEach(function(sf) {
nodes = nodes.concat(sf.nodes||[]).concat(sf.configs||[]);
delete sf.nodes;
delete sf.configs;
nodes.push(sf);
});
}
} else {
newConfig = newConfig.filter(function(node) {
return node.z !== id && node.id !== id;
});
var tabNode = {
type:'tab',
label:newFlow.label,
id:id
}
if (newFlow.hasOwnProperty('info')) {
tabNode.info = newFlow.info;
}
if (newFlow.hasOwnProperty('disabled')) {
tabNode.disabled = newFlow.disabled;
}
if (newFlow.hasOwnProperty('env')) {
tabNode.env = newFlow.env;
}
if (newFlow.hasOwnProperty('credentials')) {
tabNode.credentials = newFlow.credentials;
}
nodes = [tabNode].concat(newFlow.nodes||[]).concat(newFlow.configs||[]);
nodes.forEach(function(n) {
if (n.type !== 'tab') {
n.z = id;
}
});
}
newConfig = newConfig.concat(nodes);
return setFlows(newConfig, null, 'flows', true, null, user).then(function() {
log.info(log._("nodes.flows.updated-flow",{label:(label?label+" ":"")+"["+id+"]"}));
})
}
async function removeFlow(id, user) {
if (id === 'global') {
// TODO: nls + error code
throw new Error('not allowed to remove global');
}
var flow = activeFlowConfig.flows[id];
if (!flow) {
var e = new Error();
e.code = 404;
throw e;
}
var newConfig = jsonClone(activeConfig.flows);
newConfig = newConfig.filter(function(node) {
return node.z !== id && node.id !== id;
});
return setFlows(newConfig, null, 'flows', true, null, user).then(function() {
log.info(log._("nodes.flows.removed-flow",{label:(flow.label?flow.label+" ":"")+"["+flow.id+"]"}));
});
}
const flowAPI = {
getNode: getNode,
handleError: () => false,
handleStatus: () => false,
getSetting: k => flowUtil.getEnvVar(k),
log: m => log.log(m)
}
module.exports = {
init: init,
/**
* Load the current flow configuration from storage
* @return a promise for the loading of the config
*/
load: load,
loadFlows: load,
get:getNode,
eachNode: eachNode,
/**
* Gets the current flow configuration
*/
getFlows: getFlows,
/**
* Sets the current active config.
* @param config the configuration to enable
* @param type the type of deployment to do: full (default), nodes, flows, load
* @return a promise for the saving/starting of the new flow
*/
setFlows: setFlows,
/**
* Starts the current flow configuration
*/
startFlows: start,
/**
* Stops the current flow configuration
* @return a promise for the stopping of the flow
*/
stopFlows: stop,
get started() { return started },
state: () => { return state },
// handleError: handleError,
// handleStatus: handleStatus,
checkTypeInUse: checkTypeInUse,
addFlow: addFlow,
getFlow: getFlow,
updateFlow: updateFlow,
removeFlow: removeFlow,
disableFlow:null,
enableFlow:null,
isDeliveryModeAsync: function() {
// If settings is null, this is likely being run by unit tests
return !settings || !settings.runtimeSyncDelivery
}
};
@@ -0,0 +1,735 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const jsonClone = require("rfdc")();
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 = {};
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) {
return true;
}
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;
}
for (var i=0;i<newKeys.length;i++) {
var p = newKeys[i];
if (!redUtil.compareObjects(oldNode[p],newNode[p])) {
return true;
}
}
return false;
}
var EnvVarPropertyRE_old = /^\$\((\S+)\)$/;
var EnvVarPropertyRE = /^\${(\S+)}$/;
function mapEnvVarProperties(obj,prop,flow,config) {
const v = obj[prop];
if (Buffer.isBuffer(v)) {
return;
} else if (Array.isArray(v)) {
for (let i=0;i<v.length;i++) {
mapEnvVarProperties(v,i,flow,config);
}
} else if (typeof obj[prop] === 'string') {
if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(v) || EnvVarPropertyRE.test(v)) ) {
const envVar = v.substring(2,v.length-1);
const r = redUtil.getSetting(config, envVar, flow);
if (r !== undefined) {
obj[prop] = r
}
}
} else {
for (var p in v) {
if (v.hasOwnProperty(p)) {
mapEnvVarProperties(v,p,flow,config);
}
}
}
}
async function evaluateEnvProperties(flow, env, credentials) {
credentials = 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',{
// Fake a node object to provide access to _flow and context
_flow: flow,
context: () => {
return {
flow: {
get: (value, store, callback) => {
return flow.getContext('flow').get(value, store, callback)
}
},
global: {
get: (value, store, callback) => {
return flow.getContext('global').get(value, store, callback)
}
}
}
}
}, null, (err, result) => {
if (!err) {
if (typeof result === 'object') {
result = { value: result, __clone__: true}
}
evaluatedEnv[name] = result
} else {
evaluatedEnv[name] = undefined
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
}
resolve()
});
}))
} else if (type === "conf-type" && /^\${[^}]+}$/.test(value)) {
// Get the config node from the parent subflow
const name = value.substring(2, value.length - 1);
value = flow.getSetting(name);
} else {
try {
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
if (typeof value === 'object') {
value = { value: value, __clone__: true}
}
} catch (err) {
value = undefined
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
}
}
evaluatedEnv[name] = value
}
if (pendingEvaluations.length > 0) {
await Promise.all(pendingEvaluations)
}
// Now loop over the env types and evaluate them properly
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: {
// Provide a hook so when it tries to look up a flow setting,
// we can insert the just-evaluated value which hasn't yet
// been set on the flow object - otherwise delegate up to the flow
getSetting: function(name) {
if (evaluatedEnv.hasOwnProperty(name)){
return evaluatedEnv[name]
}
return flow.getSetting(name)
}
}}, null, null);
}
if (typeof value === 'object' && !value.__clone__) {
value = { value: value, __clone__: true}
}
evaluatedEnv[name] = value
}
// console.log(evaluatedEnv)
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;
try {
var nodeTypeConstructor = typeRegistry.get(type);
if (typeof nodeTypeConstructor === "function") {
var conf = jsonClone(config);
delete conf.credentials;
try {
Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true })
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
Object.defineProperty(conf,'_path', {value: `${flow.path}/${config._alias||config.id}`, enumerable: false, writable: true })
for (var p in conf) {
if (conf.hasOwnProperty(p)) {
mapEnvVarProperties(conf,p,flow,conf);
}
}
newNode = new nodeTypeConstructor(conf);
} catch (err) {
Log.log({
level: Log.ERROR,
id:conf.id,
type: type,
msg: err
});
}
} else if (nodeTypeConstructor) {
// console.log(nodeTypeConstructor)
var subflowConfig = parseConfig([nodeTypeConstructor.subflow].concat(nodeTypeConstructor.subflow.flow));
var subflowInstanceConfig = subflowConfig.subflows[nodeTypeConstructor.subflow.id];
delete subflowConfig.subflows[nodeTypeConstructor.subflow.id];
subflowInstanceConfig.subflows = subflowConfig.subflows;
var instanceConfig = jsonClone(config);
instanceConfig.env = jsonClone(nodeTypeConstructor.subflow.env);
instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => {
var nodePropType;
var nodePropValue = config[nodeProp.name];
if (nodeProp.type === "cred") {
nodePropType = "cred";
} else {
switch(typeof config[nodeProp.name]) {
case "string": nodePropType = "str"; break;
case "number": nodePropType = "num"; break;
case "boolean": nodePropType = "bool"; nodePropValue == nodeProp?"true":"false"; break;
default:
nodePropType = config[nodeProp.name].type;
nodePropValue = config[nodeProp.name].value;
}
}
return {
name: nodeProp.name,
type: nodePropType,
value: nodePropValue
}
})
var subflow = require("./Subflow").createModuleInstance(
nodeTypeConstructor.type,
flow,
flow.global,
subflowInstanceConfig,
instanceConfig
);
// Register this subflow as an instance node of the parent flow.
// 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
await subflow.start();
return subflow.node;
}
} catch(err) {
Log.error(err);
}
return newNode;
}
function parseConfig(config) {
var flow = {};
flow.allNodes = {};
flow.subflows = {};
flow.configs = {};
flow.flows = {};
flow.missingTypes = [];
config.forEach(function (n) {
flow.allNodes[n.id] = jsonClone(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 = [];
}
});
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);
}
} 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]) {
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 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;
if (!oldConfig) {
oldConfig = {
flows:{},
allNodes:{}
}
}
var changedSubflows = {};
var added = {};
var removed = {};
var changed = {};
var flowChanged = {};
var wiringChanged = {};
var globalConfigChanged = false;
var linkMap = {};
var allNestedGroups = []
// 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) {
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<node.wires.length;j++) {
wires = node.wires[j];
for (k=0;k<wires.length;k++) {
linkMap[node.id].push(wires[k]);
nn = oldConfig.allNodes[wires[k]];
if (nn) {
linkMap[nn.id] = linkMap[nn.id] || [];
linkMap[nn.id].push(node.id);
}
}
}
}
// This node has been removed or its flow disabled
if (removed[node.z] || !newConfig.allNodes.hasOwnProperty(id)) {
removed[id] = node;
// Mark the container as changed
if (!removed[node.z] && newConfig.allNodes[removed[id].z]) {
changed[removed[id].z] = newConfig.allNodes[removed[id].z];
if (changed[removed[id].z].type === "subflow") {
changedSubflows[removed[id].z] = changed[removed[id].z];
//delete removed[id];
}
}
} else {
if (added[node.z]) {
added[id] = node;
} else {
var currentState = node.d;
var newState = newConfig.allNodes[id].d;
if (!currentState && newState) {
removed[id] = node;
}
// This node has a material configuration change
if (diffNodes(node,newConfig.allNodes[id]) || newConfig.allNodes[id].credentials) {
changed[id] = newConfig.allNodes[id];
if (changed[id].type === "subflow") {
changedSubflows[id] = changed[id];
}
// Mark the container as changed
if (newConfig.allNodes[changed[id].z]) {
changed[changed[id].z] = newConfig.allNodes[changed[id].z];
if (changed[changed[id].z].type === "subflow") {
changedSubflows[changed[id].z] = changed[changed[id].z];
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)) {
wiringChanged[id] = newConfig.allNodes[id];
// Mark the container as changed
if (newConfig.allNodes[wiringChanged[id].z]) {
changed[wiringChanged[id].z] = newConfig.allNodes[wiringChanged[id].z];
if (changed[wiringChanged[id].z].type === "subflow") {
changedSubflows[wiringChanged[id].z] = changed[wiringChanged[id].z];
delete wiringChanged[id];
}
}
}
}
}
} else if (!removed[id]) {
if (JSON.stringify(node.env) !== JSON.stringify(newConfig.allNodes[id].env)) {
flowChanged[id] = newConfig.allNodes[id];
}
}
}
}
// Look for added nodes
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] || [];
for (j=0;j<node.wires.length;j++) {
wires = node.wires[j];
for (k=0;k<wires.length;k++) {
if (linkMap[node.id].indexOf(wires[k]) === -1) {
linkMap[node.id].push(wires[k]);
}
nn = newConfig.allNodes[wires[k]];
if (nn) {
linkMap[nn.id] = linkMap[nn.id] || [];
if (linkMap[nn.id].indexOf(node.id) === -1) {
linkMap[nn.id].push(node.id);
}
}
}
}
}
// This node has been added
if (!oldConfig.allNodes.hasOwnProperty(id)) {
added[id] = node;
// Mark the container as changed
if (newConfig.allNodes[added[id].z]) {
changed[added[id].z] = newConfig.allNodes[added[id].z];
if (changed[added[id].z].type === "subflow") {
changedSubflows[added[id].z] = changed[added[id].z];
delete added[id];
}
}
}
}
}
var madeChange;
// Loop through the nodes looking for references to changed config nodes
// Repeat the loop if anything is marked as changed as it may need to be
// propagated to parent nodes.
// TODO: looping through all nodes every time is a bit inefficient - could be more targeted
do {
madeChange = false;
for (id in newConfig.allNodes) {
if (newConfig.allNodes.hasOwnProperty(id)) {
node = newConfig.allNodes[id];
for (var prop in node) {
if (node.hasOwnProperty(prop) && prop != "z" && prop != "id" && prop != "wires") {
// This node has a property that references a changed/removed node
// Assume it is a config node change and mark this node as
// changed.
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];
}
}
}
}
}
}
}
}
} while (madeChange===true)
// 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];
}
}
}
// 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) {
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 diff = {
added:Object.keys(added),
changed:Object.keys(changed),
removed:Object.keys(removed),
rewired:Object.keys(wiringChanged),
linked:[],
flowChanged: Object.keys(flowChanged),
globalConfigChanged
}
// 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) {
// 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(
// "- "+(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
}