UNPKG

cloud-red

Version:

Harnessing Serverless for your cloud integration needs

493 lines (459 loc) 16.7 kB
/** * 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 util = require('util'); const redUtil = require('../../../../util').util; const flowUtil = require('./util'); var Log; /** * 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); // 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; var env = []; if (this.subflowDef.env) { this.subflowDef.env.forEach(e => { env[e.name] = e; }); } if (this.subflowInstance.env) { this.subflowInstance.env.forEach(e => { env[e.name] = e; }); } 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('../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 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); 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); } /** * Get environment variable of subflow * @param {String} name name of env var * @return {Object} val value of env var */ getSetting(name) { this.trace('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; if (val.type === 'env') { value = value.replace( new RegExp('\\${' + name + '}', 'g'), '${$parent.' + name + '}' ); } try { var ret = redUtil.evaluateNodeProperty( value, val.type, this.node, null, null ); return ret; } 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.id, "--->",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 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 (j = 0; j < outputs.length; j++) { wires = outputs[j]; for (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 === '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) { Log = runtime.log; }, create: createSubflow };