1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00
node-red/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js
Nick O'Leary 6fb96fa3c1
Move exec and events components to util module
The exec and events components are common components that
are used by both runtime and registry. It makes sense to
move them into the util package.

This also adds some docs to the registry module
2020-12-07 11:49:01 +00:00

510 lines
20 KiB
JavaScript

/**
* 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 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: {},
subflows: {}
}
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 = clone(subflowDef.subflows || {});
remapSubflowNodes(subflowInternalFlowConfig.configs,node_map);
remapSubflowNodes(subflowInternalFlowConfig.nodes,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);
this.templateCredentials = credentials.get(subflowDef.id);
this.instanceCredentials = credentials.get(this.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 = 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]
*/
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,
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 = clone(subflowInstanceConfig.wires);
}
this.node = new Node(subflowInstanceConfig);
this.node.on("input", function(msg) { this.send(msg);});
this.node.on("close", function() { this.status({}); })
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 = clone(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._originalWires) {
node.wires = clone(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];
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._originalWires) {
node._originalWires = clone(node.wires);
}
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(this.subflowInstance.wires[i]);
}
}
}
}
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._originalWires) {
node._originalWires = clone(node.wires);
}
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]);
node.wires[wires[j].port].push(subflowStatusId);
}
}
}
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} name 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);
}
var parent = this.parent;
if (parent) {
var val = parent.getSetting(name);
return val;
}
return undefined;
}
/**
* 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 = clone(def);
let nid = 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') && 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;
}
}
}
}
}
}
}
function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
}
module.exports = {
init: function(runtime) {},
create: createSubflow
}