node-red/packages/node_modules/@node-red/runtime/lib/flows/util.js

671 lines
27 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.
**/
var clone = require("clone");
var redUtil = require("@node-red/util").util;
var Log = require("@node-red/util").log;
var subflowInstanceRE = /^subflow:(.+)$/;
var typeRegistry = require("@node-red/registry");
const credentials = require("../nodes/credentials");
let _runtime = null;
var envVarExcludes = {};
function diffNodes(oldNode,newNode) {
if (oldNode == null) {
return true;
}
var oldKeys = Object.keys(oldNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" });
var newKeys = Object.keys(newNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" });
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) {
var v = obj[prop];
if (Buffer.isBuffer(v)) {
return;
} else if (Array.isArray(v)) {
for (var 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)) ) {
var envVar = v.substring(2,v.length-1);
var r = redUtil.getSetting(config, envVar, flow);
obj[prop] = r ? r : obj[prop];
}
} else {
for (var p in v) {
if (v.hasOwnProperty(p)) {
mapEnvVarProperties(v,p,flow,config);
}
}
}
}
async function evaluateEnvProperties(flow, env, credentials) {
const pendingEvaluations = []
const evaluatedEnv = {}
const envTypes = []
for (let i = 0; i < env.length; i++) {
let { name, value, type } = env[i]
if (type === "env") {
// Do env types last as they may include references to other env vars
// at this level which need to be resolved before they can be looked-up
envTypes.push(env[i])
} else if (type === "bool") {
value = (value === "true") || (value === true);
} else if (type === "cred") {
if (credentials.hasOwnProperty(name)) {
value = credentials[name];
}
} else if (type ==='jsonata') {
pendingEvaluations.push(new Promise((resolve, _) => {
redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => {
if (!err) {
evaluatedEnv[name] = result
}
resolve()
});
}))
} else {
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
}
evaluatedEnv[name] = value
}
if (pendingEvaluations.length > 0) {
await Promise.all(pendingEvaluations)
}
for (let i = 0; i < envTypes.length; i++) {
let { name, value, type } = envTypes[i]
// If an env-var wants to lookup itself, delegate straight to the parent
// https://github.com/node-red/node-red/issues/2099
if (value === name) {
value = `$parent.${name}`
}
if (evaluatedEnv.hasOwnProperty(value)) {
value = evaluatedEnv[value]
} else {
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
}
evaluatedEnv[name] = value
}
return evaluatedEnv
}
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 getGlobalEnv(name) {
const nodes = _runtime.nodes;
if (!nodes) {
return null;
}
const gconf = nodes.getGlobalConfig();
const env = gconf ? gconf.env : null;
if (env) {
const cred = (gconf ? credentials.get(gconf.id) : null) || {
map: {}
};
const map = cred.map;
for (let i = 0; i < env.length; i++) {
const item = env[i];
if (item.name === name) {
if (item.type === "cred") {
return {
name: name,
value: map[name],
type: "cred"
};
}
return item;
}
}
}
return null;
}
module.exports = {
init: function(runtime) {
_runtime = runtime;
envVarExcludes = {};
if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) {
runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true);
}
},
getEnvVar: function(k) {
if (!envVarExcludes[k]) {
const item = getGlobalEnv(k);
if (item) {
const val = redUtil.evaluateNodeProperty(item.value, item.type, null, null, null);
return val;
}
return process.env[k];
}
return undefined;
},
diffNodes,
mapEnvVarProperties,
evaluateEnvProperties,
parseConfig,
diffConfigs: function(oldConfig, newConfig) {
var id;
var node;
var nn;
var wires;
var j,k;
if (!oldConfig) {
oldConfig = {
flows:{},
allNodes:{}
}
}
var changedSubflows = {};
var added = {};
var removed = {};
var changed = {};
var wiringChanged = {};
var linkMap = {};
var changedTabs = {};
// Look for tabs that have been removed
for (id in oldConfig.flows) {
if (oldConfig.flows.hasOwnProperty(id) && (!newConfig.flows.hasOwnProperty(id))) {
removed[id] = oldConfig.allNodes[id];
}
}
// Look for tabs that have been disabled
for (id in oldConfig.flows) {
if (oldConfig.flows.hasOwnProperty(id) && newConfig.flows.hasOwnProperty(id)) {
var originalState = oldConfig.flows[id].disabled||false;
var newState = newConfig.flows[id].disabled||false;
if (originalState !== newState) {
changedTabs[id] = true;
if (originalState) {
added[id] = oldConfig.allNodes[id];
} else {
removed[id] = oldConfig.allNodes[id];
}
}
}
}
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];
}
}
}
// 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];
}
}
}
}
}
}
}
}
// Look for added nodes
for (id in newConfig.allNodes) {
if (newConfig.allNodes.hasOwnProperty(id)) {
node = newConfig.allNodes[id];
// 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 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:[]
}
// Traverse the links of all modified nodes to mark the connected nodes
var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired);
var visited = {};
while (modifiedNodes.length > 0) {
node = modifiedNodes.pop();
if (!visited[node]) {
visited[node] = true;
if (linkMap[node]) {
if (!changed[node] && !added[node] && !removed[node] && !wiringChanged[node]) {
diff.linked.push(node);
}
modifiedNodes = modifiedNodes.concat(linkMap[node]);
}
}
}
// console.log(diff);
// for (id in newConfig.allNodes) {
// console.log(
// (added[id]?"a":(changed[id]?"c":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"l":" "),
// newConfig.allNodes[id].type.padEnd(10),
// id.padEnd(16),
// (newConfig.allNodes[id].z||"").padEnd(16),
// newConfig.allNodes[id].name||newConfig.allNodes[id].label||""
// );
// }
// for (id in removed) {
// console.log(
// "- "+(diff.linked.indexOf(id)!==-1?"~":" "),
// id,
// oldConfig.allNodes[id].type,
// oldConfig.allNodes[id].name||oldConfig.allNodes[id].label||""
// );
// }
return diff;
},
/**
* Create a new instance of a node
* @param {Flow} flow The containing flow
* @param {object} config The node configuration object
* @return {Node} The instance of the node
*/
createNode: createNode,
evaluateEnvProperties
}