/** * 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("rfdc")({proto:true, circles: true}); 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 { redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => { if (!err) { evaluatedEnv[name] = result } resolve() }); })) } else { value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); } evaluatedEnv[name] = value } if (pendingEvaluations.length > 0) { await Promise.all(pendingEvaluations) } for (let i = 0; i < envTypes.length; i++) { let { name, value, type } = envTypes[i] // If an env-var wants to lookup itself, delegate straight to the parent // https://github.com/node-red/node-red/issues/2099 if (value === name) { value = `$parent.${name}` } if (evaluatedEnv.hasOwnProperty(value)) { value = evaluatedEnv[value] } else { value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); } evaluatedEnv[name] = value } 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 = clone(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 = clone(config); instanceConfig.env = clone(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] = clone(n); if (n.type === 'tab') { flow.flows[n.id] = n; flow.flows[n.id].subflows = {}; flow.flows[n.id].configs = {}; flow.flows[n.id].nodes = {}; flow.flows[n.id].groups = {}; } else if (n.type === 'subflow') { flow.subflows[n.id] = n; flow.subflows[n.id].configs = {}; flow.subflows[n.id].nodes = {}; flow.subflows[n.id].groups = {}; flow.subflows[n.id].instances = []; } }); 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 { 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 { 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 }