1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge branch 'flowengine'

This commit is contained in:
Nick O'Leary 2015-11-03 20:24:55 +00:00
commit 71ecb89abc
19 changed files with 3277 additions and 2777 deletions

View File

@ -1,845 +0,0 @@
/**
* Copyright 2015 IBM Corp.
*
* 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 when = require("when");
var clone = require("clone");
var typeRegistry = require("./registry");
var credentials = require("./credentials");
var redUtil = require("../util");
var events = require("../events");
var Log = require("../log");
function getID() {
return (1+Math.random()*4294967295).toString(16);
}
var EnvVarPropertyRE = /^\$\((\S+)\)$/;
function mapEnvVarProperties(obj,prop) {
if (Buffer.isBuffer(obj[prop])) {
return;
} else if (Array.isArray(obj[prop])) {
for (var i=0;i<obj[prop].length;i++) {
mapEnvVarProperties(obj[prop],i);
}
} else if (typeof obj[prop] === 'string') {
var m;
if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) {
if (process.env.hasOwnProperty(m[1])) {
obj[prop] = process.env[m[1]];
}
}
} else {
for (var p in obj[prop]) {
if (obj[prop].hasOwnProperty) {
mapEnvVarProperties(obj[prop],p);
}
}
}
}
function createNode(type,config) {
var nn = null;
var m;
var nt = typeRegistry.get(type);
if (nt) {
for (var p in config) {
if (config.hasOwnProperty(p)) {
mapEnvVarProperties(config,p);
}
}
try {
nn = new nt(clone(config));
}
catch (err) {
Log.log({
level: Log.ERROR,
id:config.id,
type: type,
msg: err
});
}
} else {
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
}
return nn;
}
function createSubflow(sf,sfn,subflows) {
//console.log("CREATE SUBFLOW",sf.config.id,sfn.id);
var nodes = [];
var node_map = {};
var newNodes = [];
var node;
var wires;
var i,j,k;
// Clone all of the subflow node definitions and give them new IDs
for (i=0;i<sf.nodes.length;i++) {
node = clone(sf.nodes[i].config);
var nid = getID();
node_map[node.id] = node;
node._alias = node.id;
node.id = nid;
node.z = sfn.id;
newNodes.push(node);
}
// Look for any catch/status nodes and update their scope ids
// Update all subflow interior wiring to reflect new node IDs
for (i=0;i<newNodes.length;i++) {
node = newNodes[i];
var outputs = node.wires;
for (j=0;j<outputs.length;j++) {
wires = outputs[j];
for (k=0;k<wires.length;k++) {
outputs[j][k] = node_map[outputs[j][k]].id
}
}
if ((node.type === 'catch' || node.type === 'status') && node.scope) {
node.scope = node.scope.map(function(id) {
return node_map[id]?node_map[id].id:""
})
}
}
// Create a subflow node to accept inbound messages and route appropriately
var Node = require("./Node");
var subflowInstance = {
id: sfn.id,
type: sfn.type,
z: sfn.z,
name: sfn.name,
wires: []
}
if (sf.config.in) {
subflowInstance.wires = sf.config.in.map(function(n) { return n.wires.map(function(w) { return node_map[w.id].id;})})
subflowInstance._originalWires = clone(subflowInstance.wires);
}
var subflowNode = new Node(subflowInstance);
subflowNode.on("input", function(msg) { this.send(msg);});
subflowNode._updateWires = subflowNode.updateWires;
subflowNode.updateWires = function(newWires) {
// Wire the subflow outputs
if (sf.config.out) {
var node,wires,i,j;
// Restore the original wiring to the internal nodes
subflowInstance.wires = clone(subflowInstance._originalWires);
for (i=0;i<sf.config.out.length;i++) {
wires = sf.config.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id != sf.config.id) {
node = node_map[wires[j].id];
if (node._originalWires) {
node.wires = clone(node._originalWires);
}
}
}
}
var modifiedNodes = {};
var subflowInstanceModified = false;
for (i=0;i<sf.config.out.length;i++) {
wires = sf.config.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id === sf.config.id) {
subflowInstance.wires[wires[j].port] = subflowInstance.wires[wires[j].port].concat(newWires[i]);
subflowInstanceModified = true;
} else {
node = 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];
subflowNode.instanceNodes[id].updateWires(node.wires);
});
if (subflowInstanceModified) {
subflowNode._updateWires(subflowInstance.wires);
}
}
}
nodes.push(subflowNode);
// Wire the subflow outputs
if (sf.config.out) {
var modifiedNodes = {};
for (i=0;i<sf.config.out.length;i++) {
wires = sf.config.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id === sf.config.id) {
// A subflow input wired straight to a subflow output
subflowInstance.wires[wires[j].port] = subflowInstance.wires[wires[j].port].concat(sfn.wires[i])
subflowNode._updateWires(subflowInstance.wires);
} else {
node = node_map[wires[j].id];
modifiedNodes[node.id] = node;
if (!node._originalWires) {
node._originalWires = clone(node.wires);
}
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(sfn.wires[i]);
}
}
}
}
// Instantiate the nodes
for (i=0;i<newNodes.length;i++) {
node = newNodes[i];
var type = node.type;
var m = /^subflow:(.+)$/.exec(type);
if (!m) {
nodes.push(createNode(type,node));
} else {
var subflowId = m[1];
nodes = nodes.concat(createSubflow(subflows[subflowId],node,subflows));
}
}
subflowNode.instanceNodes = {};
nodes.forEach(function(node) {
subflowNode.instanceNodes[node.id] = node;
});
return nodes;
}
function diffNodeConfigs(oldNode,newNode) {
if (oldNode == null) {
return true;
} else {
for (var p in newNode) {
if (newNode.hasOwnProperty(p) && p != "x" && p != "y" && p != "wires") {
if (!redUtil.compareObjects(oldNode[p],newNode[p])) {
return true;
}
}
}
}
return false;
}
var subflowInstanceRE = /^subflow:(.+)$/;
function Flow(config) {
this.activeNodes = {};
this.subflowInstanceNodes = {};
this.catchNodeMap = {};
this.statusNodeMap = {};
this.started = false;
this.parseConfig(config);
}
Flow.prototype.parseConfig = function(config) {
var i;
var nodeConfig;
var nodeType;
this.config = config;
this.allNodes = {};
this.nodes = {};
this.subflows = {};
this.configNodes = {};
var unknownTypes = {};
for (i=0;i<this.config.length;i++) {
nodeConfig = this.config[i];
nodeType = nodeConfig.type;
this.allNodes[nodeConfig.id] = nodeConfig;
if (nodeType == "subflow") {
this.subflows[nodeConfig.id] = {
type: "subflow",
config: nodeConfig,
nodes: []
}
}
}
//console.log("Known subflows:",Object.keys(this.subflows));
for (i=0;i<this.config.length;i++) {
nodeConfig = this.config[i];
nodeType = nodeConfig.type;
if (nodeConfig.credentials) {
delete nodeConfig.credentials;
}
if (nodeType != "tab" && nodeType != "subflow") {
var m = subflowInstanceRE.exec(nodeType);
if ((m && !this.subflows[m[1]]) || (!m && !typeRegistry.get(nodeType))) {
// This is an unknown subflow or an unknown type
unknownTypes[nodeType] = true;
} else {
var nodeInfo = {
type: nodeType,
config:nodeConfig
}
if (m) {
nodeInfo.subflow = m[1];
}
if (this.subflows[nodeConfig.z]) {
this.subflows[nodeConfig.z].nodes.push(nodeInfo);
} else {
this.nodes[nodeConfig.id] = nodeInfo;
}
if (nodeConfig.type != "catch" && nodeConfig.type != "status") {
for (var prop in nodeConfig) {
if (nodeConfig.hasOwnProperty(prop) &&
prop != "type" &&
prop != "id" &&
prop != "z" &&
prop != "wires" &&
this.allNodes[nodeConfig[prop]]) {
this.configNodes[nodeConfig[prop]] = this.allNodes[nodeConfig[prop]];
}
}
}
}
}
}
//console.log("NODES");
//for (i in this.nodes) {
// if (this.nodes.hasOwnProperty(i)) {
// console.log(" ",i,this.nodes[i].type,this.nodes[i].config.name||"");
// }
//}
//console.log("SUBFLOWS");
//for (i in this.subflows) {
// if (this.subflows.hasOwnProperty(i)) {
// console.log(" ",i,this.subflows[i].type,this.subflows[i].config.name||"");
// for (var j=0;j<this.subflows[i].nodes.length;j++) {
// console.log(" ",this.subflows[i].nodes[j].config.id,this.subflows[i].nodes[j].type,this.subflows[i].nodes[j].config.name||"");
// }
// }
//}
this.missingTypes = Object.keys(unknownTypes);
}
Flow.prototype.start = function(configDiff) {
if (configDiff) {
for (var j=0;j<configDiff.rewire.length;j++) {
var rewireNode = this.activeNodes[configDiff.rewire[j]];
if (rewireNode) {
rewireNode.updateWires(this.allNodes[rewireNode.id].wires);
}
}
}
this.started = true;
if (this.missingTypes.length > 0) {
throw new Error(Log._("nodes.flow.missing-types"));
}
events.emit("nodes-starting");
var id;
var node;
for (id in this.configNodes) {
if (this.configNodes.hasOwnProperty(id)) {
node = this.configNodes[id];
if (!this.activeNodes[id]) {
this.activeNodes[id] = createNode(node.type,node);
}
}
}
for (id in this.nodes) {
if (this.nodes.hasOwnProperty(id)) {
node = this.nodes[id];
if (!node.subflow) {
if (!this.activeNodes[id]) {
this.activeNodes[id] = createNode(node.type,node.config);
//console.log(id,"created");
} else {
//console.log(id,"already running");
}
} else {
if (!this.subflowInstanceNodes[id]) {
var nodes = createSubflow(this.subflows[node.subflow],node.config,this.subflows);
this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id});
for (var i=0;i<nodes.length;i++) {
this.activeNodes[nodes[i].id] = nodes[i];
}
//console.log(id,"(sf) created");
} else {
//console.log(id,"(sf) already running");
}
}
}
}
this.catchNodeMap = {};
this.statusNodeMap = {};
for (id in this.activeNodes) {
if (this.activeNodes.hasOwnProperty(id)) {
node = this.activeNodes[id];
if (node.type === "catch") {
this.catchNodeMap[node.z] = this.catchNodeMap[node.z] || [];
this.catchNodeMap[node.z].push(node);
} else if (node.type === "status") {
this.statusNodeMap[node.z] = this.statusNodeMap[node.z] || [];
this.statusNodeMap[node.z].push(node);
}
}
}
credentials.clean(this.config);
events.emit("nodes-started");
}
Flow.prototype.stop = function(configDiff) {
var nodeList;
if (configDiff) {
nodeList = configDiff.stop;
} else {
nodeList = Object.keys(this.activeNodes);
}
var flow = this;
return when.promise(function(resolve) {
events.emit("nodes-stopping");
var promises = [];
for (var i=0;i<nodeList.length;i++) {
var node = flow.activeNodes[nodeList[i]];
if (node) {
try {
var p = node.close();
if (p) {
promises.push(p);
}
} catch(err) {
node.error(err);
}
delete flow.subflowInstanceNodes[nodeList[i]];
delete flow.activeNodes[nodeList[i]];
}
}
when.settle(promises).then(function() {
events.emit("nodes-stopped");
flow.started = false;
resolve();
});
});
}
Flow.prototype.getMissingTypes = function() {
return this.missingTypes;
}
Flow.prototype.typeRegistered = function(type) {
if (this.missingTypes.length > 0) {
var i = this.missingTypes.indexOf(type);
if (i != -1) {
this.missingTypes.splice(i,1);
if (this.missingTypes.length === 0 && this.started) {
this.start();
}
return true;
}
}
return false;
}
Flow.prototype.getNode = function(id) {
return this.activeNodes[id];
}
Flow.prototype.getFlow = function() {
//console.log(this.config);
return this.config;
}
Flow.prototype.eachNode = function(callback) {
for (var id in this.activeNodes) {
if (this.activeNodes.hasOwnProperty(id)) {
callback(this.activeNodes[id]);
}
}
}
Flow.prototype.diffConfig = function(config,type) {
var activeNodesToStop = [];
var nodesToRewire = [];
if (type && type!="full") {
var diff = diffFlow(this,config);
//var diff = {
// deleted:[]
// changed:[]
// linked:[]
// wiringChanged: []
//}
var nodesToStop = [];
nodesToRewire = diff.wiringChanged;
if (type == "nodes") {
nodesToStop = diff.deleted.concat(diff.changed);
} else if (type == "flows") {
nodesToStop = diff.deleted.concat(diff.changed).concat(diff.linked);
}
for (var i=0;i<nodesToStop.length;i++) {
var id = nodesToStop[i];
if (this.subflowInstanceNodes[id]) {
activeNodesToStop = activeNodesToStop.concat(this.subflowInstanceNodes[id]);
} else if (this.activeNodes[id]) {
activeNodesToStop.push(id);
}
}
} else {
activeNodesToStop = Object.keys(this.activeNodes);
}
return {
type: type,
stop: activeNodesToStop,
rewire: nodesToRewire,
config: config
}
}
function diffFlow(flow,config) {
//if (!flow.started) {
// throw new Error("Cannot diff an unstarted flow");
//}
var flowNodes = {};
var changedNodes = {};
var deletedNodes = {};
var deletedSubflows = {};
var deletedTabs = {};
var linkChangedNodes = {};
var activeLinks = {};
var newLinks = {};
var changedSubflowStack = [];
var changedSubflows = {};
var buildNodeLinks = function(nodeLinks,n,nodes) {
nodeLinks[n.id] = nodeLinks[n.id] || [];
if (n.wires) {
for (var j=0;j<n.wires.length;j++) {
var wires = n.wires[j];
for (var k=0;k<wires.length;k++) {
nodeLinks[n.id].push(wires[k]);
var nn = nodes[wires[k]];
if (nn) {
nodeLinks[nn.id] = nodeLinks[nn.id] || [];
nodeLinks[nn.id].push(n.id);
}
}
}
}
}
config.forEach(function(node) {
flowNodes[node.id] = node;
});
config.forEach(function(node) {
var changed = false;
if (node.credentials) {
changed = true;
delete node.credentials;
} else {
changed = diffNodeConfigs(flow.allNodes[node.id],node);
if (!changed) {
if (flowNodes[node.z] && flowNodes[node.z].type == "subflow") {
var originalNode = flow.allNodes[node.id];
if (originalNode && !redUtil.compareObjects(originalNode.wires,node.wires)) {
// This is a node in a subflow whose wiring has changed. Mark subflow as changed
changed = true;
}
}
}
}
if (changed) {
changedNodes[node.id] = node;
}
});
flow.config.forEach(function(node) {
if (!flowNodes[node.id]) {
if (node.type === "tab") {
deletedTabs[node.id] = node;
} else if (node.type === "subflow") {
deletedSubflows[node.id] = node;
} else {
deletedNodes[node.id] = node;
}
}
buildNodeLinks(activeLinks,node,flow.allNodes);
});
flow.config.forEach(function(node) {
for (var prop in node) {
if (node.hasOwnProperty(prop) && prop != "z" && prop != "id" && prop != "wires") {
// This node has a property that references a changed node
// Assume it is a config node change and mark this node as
// changed.
if (changedNodes[node[prop]]) {
changedNodes[node.id] = node;
}
}
}
});
var checkSubflowMembership = function(nodes,id) {
var node = nodes[id];
if (node) {
if (node.type == "subflow") {
changedSubflows[id] = node;
changedSubflowStack.push(id);
} else if (nodes[node.z] && nodes[node.z].type == "subflow") {
if (!changedSubflows[node.z]) {
changedSubflows[node.z] = nodes[node.z];
changedSubflowStack.push(node.z);
}
}
}
};
Object.keys(changedNodes).forEach(function(n) { checkSubflowMembership(flowNodes,n)});
Object.keys(deletedNodes).forEach(function(n) { checkSubflowMembership(flow.allNodes,n)});
while (changedSubflowStack.length > 0) {
var subflowId = changedSubflowStack.pop();
config.forEach(function(node) {
if (node.type == "subflow:"+subflowId) {
if (!changedNodes[node.id]) {
changedNodes[node.id] = node;
checkSubflowMembership(flowNodes,node.id);
}
}
});
}
config.forEach(function(node) {
buildNodeLinks(newLinks,node,flowNodes);
});
var markLinkedNodes = function(linkChanged,otherChangedNodes,linkMap,allNodes) {
var stack = Object.keys(changedNodes).concat(Object.keys(otherChangedNodes));
var visited = {};
while(stack.length > 0) {
var id = stack.pop();
var linkedNodes = linkMap[id];
if (linkedNodes) {
for (var i=0;i<linkedNodes.length;i++) {
var linkedNodeId = linkedNodes[i];
if (changedNodes[linkedNodeId] || deletedNodes[linkedNodeId] || otherChangedNodes[linkedNodeId] || linkChanged[linkedNodeId]) {
// Do nothing - this linked node is already marked as changed, so will get done
} else {
linkChanged[linkedNodeId] = true;
stack.push(linkedNodeId);
}
}
}
}
}
markLinkedNodes(linkChangedNodes,{},newLinks,flowNodes);
markLinkedNodes(linkChangedNodes,{},activeLinks,flow.allNodes);
var modifiedLinkNodes = {};
config.forEach(function(node) {
if (!changedNodes[node.id]) {
// only concerned about unchanged nodes whose wiring may have changed
var newNodeLinks = newLinks[node.id];
var oldNodeLinks = activeLinks[node.id];
var newLinkMap = {};
newNodeLinks.forEach(function(l) { newLinkMap[l] = (newLinkMap[l]||0)+1;});
var oldLinkMap = {};
oldNodeLinks.forEach(function(l) { oldLinkMap[l] = (oldLinkMap[l]||0)+1;});
newNodeLinks.forEach(function(link) {
if (newLinkMap[link] != oldLinkMap[link]) {
modifiedLinkNodes[node.id] = node;
linkChangedNodes[node.id] = node;
if (!changedNodes[link] && !deletedNodes[link]) {
modifiedLinkNodes[link] = flowNodes[link];
linkChangedNodes[link] = flowNodes[link];
}
}
});
oldNodeLinks.forEach(function(link) {
if (newLinkMap[link] != oldLinkMap[link]) {
modifiedLinkNodes[node.id] = node;
linkChangedNodes[node.id] = node;
if (!changedNodes[link] && !deletedNodes[link]) {
modifiedLinkNodes[link] = flowNodes[link];
linkChangedNodes[link] = flowNodes[link];
}
}
});
}
});
markLinkedNodes(linkChangedNodes,modifiedLinkNodes,newLinks,flowNodes);
// config.forEach(function(n) {
// console.log((changedNodes[n.id]!=null)?"[C]":"[ ]",(linkChangedNodes[n.id]!=null)?"[L]":"[ ]","[ ]",n.id,n.type,n.name);
// });
//
// Object.keys(deletedNodes).forEach(function(id) {
// var n = flow.allNodes[id];
// console.log("[ ] [ ] [D]",n.id,n.type);
// });
var diff = {
deleted: Object.keys(deletedNodes).filter(function(id) { return deletedNodes[id].type != "subflow" && (!deletedNodes[id].z || deletedTabs[deletedNodes[id].z] || !(deletedSubflows[deletedNodes[id].z] || flowNodes[deletedNodes[id].z].type == "subflow"))}),
changed: Object.keys(changedNodes).filter(function(id) { return changedNodes[id].type != "subflow" && (!changedNodes[id].z || flowNodes[changedNodes[id].z].type != "subflow")}),
linked: Object.keys(linkChangedNodes).filter(function(id) { return linkChangedNodes[id].type != "subflow" && (!linkChangedNodes[id].z || flowNodes[linkChangedNodes[id].z].type != "subflow")}),
wiringChanged: []
}
config.forEach(function(n) {
if (!flowNodes[n.z] || flowNodes[n.z].type != "subflow") {
var originalNode = flow.allNodes[n.id];
if (originalNode && !redUtil.compareObjects(originalNode.wires,n.wires)) {
diff.wiringChanged.push(n.id);
}
}
});
return diff;
}
Flow.prototype.handleStatus = function(node,statusMessage) {
var targetStatusNodes = null;
var reportingNode = node;
var handled = false;
while(reportingNode && !handled) {
targetStatusNodes = this.statusNodeMap[reportingNode.z];
if (targetStatusNodes) {
targetStatusNodes.forEach(function(targetStatusNode) {
if (targetStatusNode.scope && targetStatusNode.scope.indexOf(node.id) === -1) {
return;
}
var message = {
status: {
text: "",
source: {
id: node.id,
type: node.type,
name: node.name
}
}
};
if (statusMessage.text) {
message.status.text = statusMessage.text;
}
targetStatusNode.receive(message);
handled = true;
});
}
if (!handled) {
reportingNode = this.activeNodes[reportingNode.z];
}
}
}
Flow.prototype.handleError = function(node,logMessage,msg) {
var count = 1;
if (msg && msg.hasOwnProperty("error")) {
if (msg.error.hasOwnProperty("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;
}
}
}
}
var targetCatchNodes = null;
var throwingNode = node;
var handled = false;
while (throwingNode && !handled) {
targetCatchNodes = this.catchNodeMap[throwingNode.z];
if (targetCatchNodes) {
targetCatchNodes.forEach(function(targetCatchNode) {
if (targetCatchNode.scope && targetCatchNode.scope.indexOf(node.id) === -1) {
return;
}
var 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,
count: count
}
};
targetCatchNode.receive(errorMessage);
handled = true;
});
}
if (!handled) {
throwingNode = this.activeNodes[throwingNode.z];
}
}
}
module.exports = Flow;

View File

@ -33,12 +33,16 @@ function Node(n) {
if (n.name) {
this.name = n.name;
}
if (n._alias) {
this._alias = n._alias;
}
this.updateWires(n.wires);
}
util.inherits(Node, EventEmitter);
Node.prototype.updateWires = function(wires) {
//console.log("UPDATE",this.id);
this.wires = wires || [];
delete this._wire;

View File

@ -68,7 +68,7 @@ module.exports = {
storage = _storage;
redApp = _app;
},
/**
* Loads the credentials from storage.
*/
@ -79,7 +79,7 @@ module.exports = {
log.warn(log._("nodes.credentials.error",{message: err}));
});
},
/**
* Adds a set of credentials for the given node id.
* @param id the node id for the credentials
@ -118,7 +118,7 @@ module.exports = {
clean: function (config) {
var existingIds = {};
config.forEach(function(n) {
existingIds[n.id] = true;
existingIds[n.id] = true;
});
var deletedCredentials = false;
for (var c in credentialCache) {
@ -135,7 +135,7 @@ module.exports = {
return when.resolve();
}
},
/**
* Registers a node credential definition.
* @param type the node type
@ -146,14 +146,14 @@ module.exports = {
credentialsDef[dashedType] = definition;
registerEndpoint(dashedType);
},
/**
* Extracts and stores any credential updates in the provided node.
* The provided node may have a .credentials property that contains
* new credentials for the node.
* This function loops through the credentials in the definition for
* the node-type and applies any of the updates provided in the node.
*
*
* This function does not save the credentials to disk as it is expected
* to be called multiple times when a new flow is deployed.
*
@ -171,7 +171,7 @@ module.exports = {
log.warn(log._("nodes.credentials.not-registered",{type:nodeType}));
return;
}
for (var cred in definition) {
if (definition.hasOwnProperty(cred)) {
if (newCreds[cred] === undefined) {
@ -191,7 +191,7 @@ module.exports = {
delete node.credentials;
}
},
/**
* Saves the credentials to storage
* @return a promise for the saving of credentials to storage
@ -199,7 +199,7 @@ module.exports = {
save: function () {
return storage.saveCredentials(credentialCache);
},
/**
* Gets the credential definition for the given node type
* @param type the node type

View File

@ -1,199 +0,0 @@
/**
* Copyright 2014, 2015 IBM Corp.
*
* 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 when = require("when");
var typeRegistry = require("./registry");
var credentials = require("./credentials");
var Flow = require("./Flow");
var log = require("../log");
var events = require("../events");
var redUtil = require("../util");
var storage = null;
var settings = null;
var deprecated = require("./deprecated");
var activeFlow = null;
var nodes = {};
var subflows = {};
var activeConfig = [];
var activeConfigNodes = {};
events.on('type-registered',function(type) {
if (activeFlow && activeFlow.typeRegistered(type)) {
log.info(log._("nodes.flows.registered-missing", {type:type}));
}
});
var flowNodes = module.exports = {
init: function(_settings, _storage) {
settings = _settings;
storage = _storage;
},
/**
* Load the current activeConfig from storage and start it running
* @return a promise for the loading of the config
*/
load: function() {
return storage.getFlows().then(function(flows) {
return credentials.load().then(function() {
activeFlow = new Flow(flows);
flowNodes.startFlows();
});
}).otherwise(function(err) {
log.warn(log._("nodes.flows.error",{message:err.toString()}));
console.log(err.stack);
});
},
/**
* Get a node
* @param i the node id
* @return the node
*/
get: function(i) {
return activeFlow.getNode(i);
},
eachNode: function(cb) {
activeFlow.eachNode(cb);
},
/**
* @return the active configuration
*/
getFlows: function() {
return activeFlow.getFlow();
},
/**
* Sets the current active config.
* @param config the configuration to enable
* @param type the type of deployment to do: full (default), nodes, flows
* @return a promise for the starting of the new flow
*/
setFlows: function (config,type) {
type = type||"full";
var credentialsChanged = false;
var credentialSavePromise = null;
// Clone config and extract credentials prior to saving
// Original config needs to retain credentials so that flow.applyConfig
// knows which nodes have had changes.
var cleanConfig = clone(config);
cleanConfig.forEach(function(node) {
if (node.credentials) {
credentials.extract(node);
credentialsChanged = true;
}
});
if (credentialsChanged) {
credentialSavePromise = credentials.save();
} else {
credentialSavePromise = when.resolve();
}
if (type=="full") {
return credentialSavePromise
.then(function() { return storage.saveFlows(cleanConfig);})
.then(function() { return flowNodes.stopFlows(); })
.then(function() { activeFlow = new Flow(config); flowNodes.startFlows();});
} else {
return credentialSavePromise
.then(function() { return storage.saveFlows(cleanConfig);})
.then(function() {
var configDiff = activeFlow.diffConfig(config,type);
return flowNodes.stopFlows(configDiff).then(function() {
activeFlow.parseConfig(config);
flowNodes.startFlows(configDiff);
});
});
}
},
startFlows: function(configDiff) {
if (configDiff) {
log.info(log._("nodes.flows.starting-modified-"+configDiff.type));
} else {
log.info(log._("nodes.flows.starting-flows"));
}
try {
activeFlow.start(configDiff);
if (configDiff) {
log.info(log._("nodes.flows.started-modified-"+configDiff.type));
} else {
log.info(log._("nodes.flows.started-flows"));
}
} catch(err) {
var missingTypes = activeFlow.getMissingTypes();
if (missingTypes.length > 0) {
log.info(log._("nodes.flows.missing-types"));
var knownUnknowns = 0;
for (var i=0;i<missingTypes.length;i++) {
var type = missingTypes[i];
var info = deprecated.get(type);
if (info) {
log.info(log._("nodes.flows.missing-type-provided",{type:missingTypes[i],module:info.module}));
knownUnknowns += 1;
} else {
log.info(" - "+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);
}
}
}
},
stopFlows: function(configDiff) {
if (configDiff) {
log.info(log._("nodes.flows.stopping-modified-"+configDiff.type));
} else {
log.info(log._("nodes.flows.stopping-flows"));
}
if (activeFlow) {
return activeFlow.stop(configDiff).then(function() {
if (configDiff) {
log.info(log._("nodes.flows.stopped-modified-"+configDiff.type));
} else {
log.info(log._("nodes.flows.stopped-flows"));
}
return;
});
} else {
log.info(log._("nodes.flows.stopped"));
return;
}
},
handleError: function(node,logMessage,msg) {
activeFlow.handleError(node,logMessage,msg);
},
handleStatus: function(node,statusMessage) {
activeFlow.handleStatus(node,statusMessage);
}
};
var activeFlow = null;

466
red/nodes/flows/Flow.js Normal file
View File

@ -0,0 +1,466 @@
/**
* Copyright 2015 IBM Corp.
*
* 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 when = require("when");
var clone = require("clone");
var typeRegistry = require("../registry");
var Log = require("../../log");
var redUtil = require("../../util");
var flowUtil = require("./util");
function Flow(global,flow) {
if (typeof flow === 'undefined') {
flow = global;
}
var activeNodes = {};
var subflowInstanceNodes = {};
var catchNodeMap = {};
var statusNodeMap = {};
this.start = function(diff) {
var node;
var id;
catchNodeMap = {};
statusNodeMap = {};
for (id in flow.configs) {
if (flow.configs.hasOwnProperty(id)) {
node = flow.configs[id];
if (!activeNodes[id]) {
activeNodes[id] = createNode(node.type,node);
}
}
}
if (diff) {
for (var j=0;j<diff.rewired.length;j++) {
var rewireNode = activeNodes[diff.rewired[j]];
if (rewireNode) {
rewireNode.updateWires(flow.nodes[rewireNode.id].wires);
}
}
}
for (id in flow.nodes) {
if (flow.nodes.hasOwnProperty(id)) {
node = flow.nodes[id];
if (!node.subflow) {
if (!activeNodes[id]) {
activeNodes[id] = createNode(node.type,node);
}
} else {
if (!subflowInstanceNodes[id]) {
try {
var nodes = createSubflow(flow.subflows[node.subflow]||global.subflows[node.subflow],node,flow.subflows,global.subflows,activeNodes);
subflowInstanceNodes[id] = nodes.map(function(n) { return n.id});
for (var i=0;i<nodes.length;i++) {
activeNodes[nodes[i].id] = nodes[i];
}
} catch(err) {
console.log(err.stack)
}
}
}
}
}
for (id in activeNodes) {
if (activeNodes.hasOwnProperty(id)) {
node = activeNodes[id];
if (node.type === "catch") {
catchNodeMap[node.z] = catchNodeMap[node.z] || [];
catchNodeMap[node.z].push(node);
} else if (node.type === "status") {
statusNodeMap[node.z] = statusNodeMap[node.z] || [];
statusNodeMap[node.z].push(node);
}
}
}
}
this.stop = function(stopList) {
return when.promise(function(resolve) {
var i;
if (stopList) {
for (i=0;i<stopList.length;i++) {
if (subflowInstanceNodes[stopList[i]]) {
// The first in the list is the instance node we already
// know about
stopList = stopList.concat(subflowInstanceNodes[stopList[i]].slice(1))
}
}
} else {
stopList = Object.keys(activeNodes);
}
var promises = [];
for (i=0;i<stopList.length;i++) {
var node = activeNodes[stopList[i]];
if (node) {
delete activeNodes[stopList[i]];
if (subflowInstanceNodes[stopList[i]]) {
delete subflowInstanceNodes[stopList[i]];
}
try {
var p = node.close();
if (p) {
promises.push(p);
}
} catch(err) {
node.error(err);
}
}
}
when.settle(promises).then(function() {
resolve();
});
});
}
this.update = function(_global,_flow) {
global = _global;
flow = _flow;
}
this.getNode = function(id) {
return activeNodes[id];
}
this.getActiveNodes = function() {
return activeNodes;
}
this.handleStatus = function(node,statusMessage) {
var targetStatusNodes = null;
var reportingNode = node;
var handled = false;
while(reportingNode && !handled) {
targetStatusNodes = statusNodeMap[reportingNode.z];
if (targetStatusNodes) {
targetStatusNodes.forEach(function(targetStatusNode) {
if (targetStatusNode.scope && targetStatusNode.scope.indexOf(node.id) === -1) {
return;
}
var message = {
status: {
text: "",
source: {
id: node.id,
type: node.type,
name: node.name
}
}
};
if (statusMessage.text) {
message.status.text = statusMessage.text;
}
targetStatusNode.receive(message);
handled = true;
});
}
if (!handled) {
reportingNode = activeNodes[reportingNode.z];
}
}
}
this.handleError = function(node,logMessage,msg) {
var count = 1;
if (msg && msg.hasOwnProperty("error")) {
if (msg.error.hasOwnProperty("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;
}
}
}
}
var targetCatchNodes = null;
var throwingNode = node;
var handled = false;
while (throwingNode && !handled) {
targetCatchNodes = catchNodeMap[throwingNode.z];
if (targetCatchNodes) {
targetCatchNodes.forEach(function(targetCatchNode) {
if (targetCatchNode.scope && targetCatchNode.scope.indexOf(throwingNode.id) === -1) {
return;
}
var 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
}
};
targetCatchNode.receive(errorMessage);
handled = true;
});
}
if (!handled) {
throwingNode = activeNodes[throwingNode.z];
}
}
}
}
function getID() {
return (1+Math.random()*4294967295).toString(16);
}
var EnvVarPropertyRE = /^\$\((\S+)\)$/;
function mapEnvVarProperties(obj,prop) {
if (Buffer.isBuffer(obj[prop])) {
return;
} else if (Array.isArray(obj[prop])) {
for (var i=0;i<obj[prop].length;i++) {
mapEnvVarProperties(obj[prop],i);
}
} else if (typeof obj[prop] === 'string') {
var m;
if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) {
if (process.env.hasOwnProperty(m[1])) {
obj[prop] = process.env[m[1]];
}
}
} else {
for (var p in obj[prop]) {
if (obj[prop].hasOwnProperty) {
mapEnvVarProperties(obj[prop],p);
}
}
}
}
function createNode(type,config) {
// console.log("CREATE",type,config.id);
var nn = null;
var nt = typeRegistry.get(type);
if (nt) {
var conf = clone(config);
delete conf.credentials;
for (var p in conf) {
if (conf.hasOwnProperty(p)) {
mapEnvVarProperties(conf,p);
}
}
try {
nn = new nt(conf);
}
catch (err) {
Log.log({
level: Log.ERROR,
id:conf.id,
type: type,
msg: err
});
}
} else {
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
}
return nn;
}
function createSubflow(sf,sfn,subflows,globalSubflows,activeNodes) {
//console.log("CREATE SUBFLOW",sf.id,sfn.id);
var nodes = [];
var node_map = {};
var newNodes = [];
var node;
var wires;
var i,j,k;
var createNodeInSubflow = function(def) {
node = clone(def);
var nid = getID();
node_map[node.id] = node;
node._alias = node.id;
node.id = nid;
node.z = sfn.id;
newNodes.push(node);
}
// Clone all of the subflow node definitions and give them new IDs
for (i in sf.configs) {
if (sf.configs.hasOwnProperty(i)) {
createNodeInSubflow(sf.configs[i]);
}
}
// Clone all of the subflow node definitions and give them new IDs
for (i in sf.nodes) {
if (sf.nodes.hasOwnProperty(i)) {
createNodeInSubflow(sf.nodes[i]);
}
}
// Look for any catch/status nodes and update their scope ids
// Update all subflow interior wiring to reflect new node IDs
for (i=0;i<newNodes.length;i++) {
node = newNodes[i];
if (node.wires) {
var outputs = node.wires;
for (j=0;j<outputs.length;j++) {
wires = outputs[j];
for (k=0;k<wires.length;k++) {
outputs[j][k] = node_map[outputs[j][k]].id
}
}
if ((node.type === 'catch' || node.type === 'status') && node.scope) {
node.scope = node.scope.map(function(id) {
return node_map[id]?node_map[id].id:""
})
} else {
for (var prop in node) {
if (node.hasOwnProperty(prop) && prop !== '_alias') {
if (node_map[node[prop]]) {
//console.log("Mapped",node.type,node.id,prop,node_map[node[prop]].id);
node[prop] = node_map[node[prop]].id;
}
}
}
}
}
}
// Create a subflow node to accept inbound messages and route appropriately
var Node = require("../Node");
var subflowInstance = {
id: sfn.id,
type: sfn.type,
z: sfn.z,
name: sfn.name,
wires: []
}
if (sf.in) {
subflowInstance.wires = sf.in.map(function(n) { return n.wires.map(function(w) { return node_map[w.id].id;})})
subflowInstance._originalWires = clone(subflowInstance.wires);
}
var subflowNode = new Node(subflowInstance);
subflowNode.on("input", function(msg) { this.send(msg);});
subflowNode._updateWires = subflowNode.updateWires;
subflowNode.updateWires = function(newWires) {
// Wire the subflow outputs
if (sf.out) {
var node,wires,i,j;
// Restore the original wiring to the internal nodes
subflowInstance.wires = clone(subflowInstance._originalWires);
for (i=0;i<sf.out.length;i++) {
wires = sf.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id != sf.id) {
node = node_map[wires[j].id];
if (node._originalWires) {
node.wires = clone(node._originalWires);
}
}
}
}
var modifiedNodes = {};
var subflowInstanceModified = false;
for (i=0;i<sf.out.length;i++) {
wires = sf.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id === sf.id) {
subflowInstance.wires[wires[j].port] = subflowInstance.wires[wires[j].port].concat(newWires[i]);
subflowInstanceModified = true;
} else {
node = 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];
subflowNode.instanceNodes[id].updateWires(node.wires);
});
if (subflowInstanceModified) {
subflowNode._updateWires(subflowInstance.wires);
}
}
}
nodes.push(subflowNode);
// Wire the subflow outputs
if (sf.out) {
var modifiedNodes = {};
for (i=0;i<sf.out.length;i++) {
wires = sf.out[i].wires;
for (j=0;j<wires.length;j++) {
if (wires[j].id === sf.id) {
// A subflow input wired straight to a subflow output
subflowInstance.wires[wires[j].port] = subflowInstance.wires[wires[j].port].concat(sfn.wires[i])
subflowNode._updateWires(subflowInstance.wires);
} else {
node = node_map[wires[j].id];
modifiedNodes[node.id] = node;
if (!node._originalWires) {
node._originalWires = clone(node.wires);
}
node.wires[wires[j].port] = (node.wires[wires[j].port]||[]).concat(sfn.wires[i]);
}
}
}
}
// Instantiate the nodes
for (i=0;i<newNodes.length;i++) {
node = newNodes[i];
var type = node.type;
var m = /^subflow:(.+)$/.exec(type);
if (!m) {
var newNode = createNode(type,node);
activeNodes[node.id] = newNode;
nodes.push(newNode);
} else {
var subflowId = m[1];
nodes = nodes.concat(createSubflow(subflows[subflowId]||globalSubflows[subflowId],node,subflows,globalSubflows,activeNodes));
}
}
subflowNode.instanceNodes = {};
nodes.forEach(function(node) {
subflowNode.instanceNodes[node.id] = node;
});
return nodes;
}
module.exports = {
create: function(global,conf) {
return new Flow(global,conf);
}
}

366
red/nodes/flows/index.js Normal file
View File

@ -0,0 +1,366 @@
/**
* Copyright 2014, 2015 IBM Corp.
*
* 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 when = require("when");
var Flow = require('./Flow');
var typeRegistry = require("../registry");
var credentials = require("../credentials");
var flowUtil = require("./util");
var log = require("../../log");
var events = require("../../events");
var redUtil = require("../../util");
var deprecated = require("../registry/deprecated");
var storage = null;
var settings = null;
var activeConfig = null;
var activeFlowConfig = null;
var activeFlows = {};
var started = false;
var activeNodesToFlow = {};
var subflowInstanceNodeMap = {};
var typeEventRegistered = false;
function init(_settings, _storage) {
if (started) {
throw new Error("Cannot init without a stop");
}
settings = _settings;
storage = _storage;
started = false;
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) {
start();
}
}
}
});
typeEventRegistered = true;
}
}
function load() {
return storage.getFlows().then(function(flows) {
return credentials.load().then(function() {
return setConfig(flows,"load");
});
}).otherwise(function(err) {
log.warn(log._("nodes.flows.error",{message:err.toString()}));
console.log(err.stack);
});
}
function setConfig(_config,type) {
var config = clone(_config);
type = type||"full";
var credentialsChanged = false;
var credentialSavePromise = null;
var configSavePromise = null;
var diff;
var newFlowConfig = flowUtil.parseConfig(clone(config));
if (type !== 'full' && type !== 'load') {
diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig);
}
config.forEach(function(node) {
if (node.credentials) {
credentials.extract(node);
credentialsChanged = true;
}
});
if (credentialsChanged) {
credentialSavePromise = credentials.save();
} else {
credentialSavePromise = when.resolve();
}
if (type === 'load') {
configSavePromise = credentialSavePromise;
type = 'full';
} else {
configSavePromise = credentialSavePromise.then(function() {
return storage.saveFlows(config);
});
}
return configSavePromise
.then(function() {
activeConfig = config;
activeFlowConfig = newFlowConfig;
return credentials.clean(activeConfig).then(function() {
if (started) {
return stop(type,diff).then(function() {
start(type,diff);
}).otherwise(function(err) {
})
}
});
});
}
function getNode(id) {
var node;
if (activeNodesToFlow[id]) {
return activeFlows[activeNodesToFlow[id]].getNode(id);
}
for (var flowId in activeFlows) {
if (activeFlows.hasOwnProperty(flowId)) {
node = activeFlows[flowId].getNode(id);
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 getConfig() {
return activeConfig;
}
function delegateError(node,logMessage,msg) {
if (activeFlows[node.z]) {
activeFlows[node.z].handleError(node,logMessage,msg);
} else if (activeNodesToFlow[node.z]) {
activeFlows[activeNodesToFlow[node.z]].handleError(node,logMessage,msg);
} else if (activeFlowConfig.subflows[node.z]) {
subflowInstanceNodeMap[node.id].forEach(function(n) {
delegateError(getNode(n),logMessage,msg);
});
}
}
function handleError(node,logMessage,msg) {
if (node.z) {
delegateError(node,logMessage,msg);
} else {
if (activeFlowConfig.configs[node.id]) {
activeFlowConfig.configs[node.id]._users.forEach(function(id) {
var userNode = activeFlowConfig.allNodes[id];
delegateError(userNode,logMessage,msg);
})
}
}
}
function delegateStatus(node,statusMessage) {
if (activeFlows[node.z]) {
activeFlows[node.z].handleStatus(node,statusMessage);
}
}
function handleStatus(node,statusMessage) {
if (node.z) {
delegateStatus(node,statusMessage);
} else {
if (activeFlowConfig.configs[node.id]) {
activeFlowConfig.configs[node.id]._users.forEach(function(id) {
var userNode = activeFlowConfig.allNodes[id];
delegateStatus(userNode,statusMessage);
})
}
}
}
function start(type,diff) {
type = type||"full";
started = true;
var i;
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);
}
return;
}
if (diff) {
log.info(log._("nodes.flows.starting-modified-"+type));
} else {
log.info(log._("nodes.flows.starting-flows"));
}
var id;
if (!diff) {
activeFlows['_GLOBAL_'] = Flow.create(activeFlowConfig);
for (id in activeFlowConfig.flows) {
if (activeFlowConfig.flows.hasOwnProperty(id)) {
activeFlows[id] = Flow.create(activeFlowConfig,activeFlowConfig.flows[id]);
}
}
} else {
activeFlows['_GLOBAL_'].update(activeFlowConfig,activeFlowConfig);
for (id in activeFlowConfig.flows) {
if (activeFlowConfig.flows.hasOwnProperty(id)) {
if (activeFlows[id]) {
activeFlows[id].update(activeFlowConfig,activeFlowConfig.flows[id]);
} else {
activeFlows[id] = Flow.create(activeFlowConfig,activeFlowConfig.flows[id]);
}
}
}
}
for (id in activeFlows) {
if (activeFlows.hasOwnProperty(id)) {
activeFlows[id].start(diff);
var activeNodes = activeFlows[id].getActiveNodes();
Object.keys(activeNodes).forEach(function(nid) {
activeNodesToFlow[nid] = id;
if (activeNodes[nid]._alias) {
subflowInstanceNodeMap[activeNodes[nid]._alias] = subflowInstanceNodeMap[activeNodes[nid]._alias] || [];
subflowInstanceNodeMap[activeNodes[nid]._alias].push(nid);
}
});
}
}
events.emit("nodes-started");
if (diff) {
log.info(log._("nodes.flows.started-modified-"+type));
} else {
log.info(log._("nodes.flows.started-flows"));
}
}
function stop(type,diff) {
type = type||"full";
if (diff) {
log.info(log._("nodes.flows.stopping-modified-"+type));
} else {
log.info(log._("nodes.flows.stopping-flows"));
}
started = false;
var promises = [];
var stopList;
if (type === 'nodes') {
stopList = diff.changed.concat(diff.removed);
} else if (type === 'flows') {
stopList = diff.changed.concat(diff.removed).concat(diff.linked);
}
for (var id in activeFlows) {
if (activeFlows.hasOwnProperty(id)) {
promises = promises.concat(activeFlows[id].stop(stopList));
if (!diff || diff.removed[id]) {
delete activeFlows[id];
}
}
}
return when.promise(function(resolve,reject) {
when.settle(promises).then(function() {
for (id in activeNodesToFlow) {
if (activeNodesToFlow.hasOwnProperty(id)) {
if (!activeFlows[activeNodesToFlow[id]]) {
delete activeNodesToFlow[id];
}
}
}
if (stopList) {
stopList.forEach(function(id) {
delete activeNodesToFlow[id];
});
}
// Ideally we'd prune just what got stopped - but mapping stopList
// id to the list of subflow instance nodes is something only Flow
// can do... so cheat by wiping the map knowing it'll be rebuilt
// in start()
subflowInstanceNodeMap = {};
if (diff) {
log.info(log._("nodes.flows.stopped-modified-"+type));
} else {
log.info(log._("nodes.flows.stopped-flows"));
}
resolve();
});
});
}
module.exports = {
init: init,
/**
* Load the current flow configuration from storage
* @return a promise for the loading of the config
*/
load: load,
get:getNode,
eachNode: eachNode,
/**
* Gets the current flow configuration
*/
getFlows: getConfig,
/**
* 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: setConfig,
/**
* Starts the current flow configuration
*/
startFlows: start,
/**
* Stops the current flow configuration
* @return a promise for the stopping of the flow
*/
stopFlows: stop,
handleError: handleError,
handleStatus: handleStatus
};

327
red/nodes/flows/util.js Normal file
View File

@ -0,0 +1,327 @@
/**
* Copyright 2015 IBM Corp.
*
* 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("../../util");
var subflowInstanceRE = /^subflow:(.+)$/;
var typeRegistry = require("../registry");
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;
}
module.exports = {
diffNodes: diffNodes,
parseConfig: function(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 = {};
}
});
config.forEach(function(n) {
if (n.type === 'subflow') {
flow.subflows[n.id] = n;
flow.subflows[n.id].configs = {};
flow.subflows[n.id].nodes = {};
flow.subflows[n.id].instances = [];
}
});
config.forEach(function(n) {
if (n.type !== 'subflow' && n.type !== 'tab') {
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);
}
} else {
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;
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 = [];
}
}
}
}
});
config.forEach(function(n) {
if (n.type !== 'subflow' && n.type !== 'tab') {
for (var prop in n) {
if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== '_users' && flow.configs[n[prop]]) {
// This property references a global config node
flow.configs[n[prop]]._users.push(n.id)
}
}
}
});
return flow;
},
diffConfigs: function(oldConfig, newConfig) {
var id;
var node;
var nn;
var wires;
var j,k;
var changedSubflows = {};
var added = {};
var removed = {};
var changed = {};
var wiringChanged = {};
var linkMap = {};
for (id in oldConfig.allNodes) {
if (oldConfig.allNodes.hasOwnProperty(id)) {
node = oldConfig.allNodes[id];
// 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
if (!newConfig.allNodes.hasOwnProperty(id)) {
removed[id] = node;
// Mark the container as changed
if (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 {
// 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];
}
}
}
}
}
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.
if (changed[node[prop]] || removed[node[prop]]) {
if (!changed[node.id]) {
changed[node.id] = node;
if (newConfig.allNodes[node.z]) {
changed[node.z] = newConfig.allNodes[node.z];
if (changed[node.z].type === "subflow") {
changedSubflows[node.z] = changed[node.z];
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]);
}
}
}
// for (id in newConfig.allNodes) {
// console.log(
// (added[id]?"+":(changed[id]?"!":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"~":" "),
// id,
// newConfig.allNodes[id].type,
// 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;
}
}

View File

@ -140,6 +140,7 @@ module.exports = {
// Flow handling
loadFlows: flows.load,
startFlows: flows.startFlows,
stopFlows: flows.stopFlows,
setFlows: flows.setFlows,
getFlows: flows.getFlows,
@ -149,4 +150,3 @@ module.exports = {
getCredentials: credentials.get,
deleteCredentials: credentials.delete
};

View File

@ -107,7 +107,7 @@ function start() {
}
}
log.info(log._("runtime.paths.settings",{path:settings.settingsFile}));
redNodes.loadFlows();
redNodes.loadFlows().then(redNodes.startFlows);
comms.start();
}).otherwise(function(err) {
console.log(err);

View File

@ -110,6 +110,7 @@ module.exports = {
testNode(red);
}
flows.load().then(function() {
flows.startFlows();
should.deepEqual(testFlows, flows.getFlows());
cb();
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,454 @@
/**
* Copyright 2014, 2015 IBM Corp.
*
* 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 should = require("should");
var sinon = require("sinon");
var when = require("when");
var clone = require("clone");
var flows = require("../../../../red/nodes/flows");
var RedNode = require("../../../../red/nodes/Node");
var RED = require("../../../../red/nodes");
var events = require("../../../../red/events");
var credentials = require("../../../../red/nodes/credentials");
var typeRegistry = require("../../../../red/nodes/registry");
var Flow = require("../../../../red/nodes/flows/Flow");
describe('flows/index', function() {
var storage;
var eventsOn;
var credentialsExtract;
var credentialsSave;
var credentialsClean;
var credentialsLoad;
var flowCreate;
var getType;
before(function() {
getType = sinon.stub(typeRegistry,"get",function(type) {
return type.indexOf('missing') === -1;
});
});
after(function() {
getType.restore();
});
beforeEach(function() {
eventsOn = sinon.spy(events,"on");
credentialsExtract = sinon.stub(credentials,"extract",function(conf) {
delete conf.credentials;
});
credentialsSave = sinon.stub(credentials,"save",function() {
return when.resolve();
});
credentialsClean = sinon.stub(credentials,"clean",function(conf) {
return when.resolve();
});
credentialsLoad = sinon.stub(credentials,"load",function() {
return when.resolve();
});
flowCreate = sinon.stub(Flow,"create",function(global, flow) {
var id;
if (typeof flow === 'undefined') {
flow = global;
id = '_GLOBAL_';
} else {
id = flow.id;
}
flowCreate.flows[id] = {
flow: flow,
global: global,
start: sinon.spy(),
update: sinon.spy(),
stop: sinon.spy(),
getActiveNodes: function() {
return flow.nodes||{};
},
handleError: sinon.spy(),
handleStatus: sinon.spy()
}
return flowCreate.flows[id];
});
flowCreate.flows = {};
storage = {
saveFlows: function(conf) {
storage.conf = conf;
return when.resolve();
}
}
});
afterEach(function(done) {
eventsOn.restore();
credentialsExtract.restore();
credentialsSave.restore();
credentialsClean.restore();
credentialsLoad.restore();
flowCreate.restore();
flows.stopFlows().then(done);
});
// describe('#init',function() {
// it('registers the type-registered handler', function() {
// flows.init({},{});
// eventsOn.calledOnce.should.be.true;
// });
// });
describe('#setFlows', function() {
it('sets the full flow', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
flows.init({},storage);
flows.setFlows(originalConfig).then(function() {
credentialsExtract.called.should.be.false;
credentialsClean.called.should.be.true;
storage.hasOwnProperty('conf').should.be.true;
flows.getFlows().should.eql(originalConfig);
done();
});
});
it('sets the full flow for type load', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
flows.init({},storage);
flows.setFlows(originalConfig,"load").then(function() {
credentialsExtract.called.should.be.false;
credentialsClean.called.should.be.true;
// 'load' type does not trigger a save
storage.hasOwnProperty('conf').should.be.false;
flows.getFlows().should.eql(originalConfig);
done();
});
});
it('extracts credentials from the full flow', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[],credentials:{}},
{id:"t1",type:"tab"}
];
flows.init({},storage);
flows.setFlows(originalConfig).then(function() {
credentialsExtract.called.should.be.true;
credentialsClean.called.should.be.true;
storage.hasOwnProperty('conf').should.be.true;
var cleanedFlows = flows.getFlows();
storage.conf.should.eql(cleanedFlows);
cleanedFlows.should.not.eql(originalConfig);
cleanedFlows[0].credentials = {};
cleanedFlows.should.eql(originalConfig);
done();
});
});
it('updates existing flows with partial deployment - nodes', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
var newConfig = clone(originalConfig);
newConfig.push({id:"t1-2",x:10,y:10,z:"t1",type:"test",wires:[]});
newConfig.push({id:"t2",type:"tab"});
newConfig.push({id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]});
storage.getFlows = function() {
return when.resolve(originalConfig);
}
events.once('nodes-started',function() {
flows.setFlows(newConfig,"nodes").then(function() {
flows.getFlows().should.eql(newConfig);
flowCreate.flows['t1'].update.called.should.be.true;
flowCreate.flows['t2'].start.called.should.be.true;
flowCreate.flows['_GLOBAL_'].update.called.should.be.true;
done();
})
});
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
});
});
it('updates existing flows with partial deployment - flows', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
var newConfig = clone(originalConfig);
newConfig.push({id:"t1-2",x:10,y:10,z:"t1",type:"test",wires:[]});
newConfig.push({id:"t2",type:"tab"});
newConfig.push({id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]});
storage.getFlows = function() {
return when.resolve(originalConfig);
}
events.once('nodes-started',function() {
flows.setFlows(newConfig,"nodes").then(function() {
flows.getFlows().should.eql(newConfig);
flowCreate.flows['t1'].update.called.should.be.true;
flowCreate.flows['t2'].start.called.should.be.true;
flowCreate.flows['_GLOBAL_'].update.called.should.be.true;
flows.stopFlows().then(done);
})
});
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
});
});
});
describe('#load', function() {
it('loads the flow config', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
flows.init({},storage);
flows.load().then(function() {
credentialsExtract.called.should.be.false;
credentialsLoad.called.should.be.true;
credentialsClean.called.should.be.true;
// 'load' type does not trigger a save
storage.hasOwnProperty('conf').should.be.false;
flows.getFlows().should.eql(originalConfig);
done();
});
});
});
describe('#startFlows', function() {
it('starts the loaded config', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
events.once('nodes-started',function() {
Object.keys(flowCreate.flows).should.eql(['_GLOBAL_','t1']);
done();
});
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
});
});
it('does not start if nodes missing', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
flowCreate.called.should.be.false;
done();
});
});
it('starts when missing nodes registered', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]},
{id:"t1-3",x:10,y:10,z:"t1",type:"missing2",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
flowCreate.called.should.be.false;
events.emit("type-registered","missing");
setTimeout(function() {
flowCreate.called.should.be.false;
events.emit("type-registered","missing2");
setTimeout(function() {
flowCreate.called.should.be.true;
done();
},10);
},10);
});
});
});
describe('#get',function() {
});
describe('#eachNode', function() {
it('iterates the flow nodes', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
flows.init({},storage);
flows.load().then(function() {
var c = 0;
flows.eachNode(function(node) {
c++
})
c.should.equal(2);
done();
});
});
});
describe('#stopFlows', function() {
});
describe('#handleError', function() {
it('passes error to correct flow', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
events.once('nodes-started',function() {
flows.handleError(originalConfig[0],"message",{});
flowCreate.flows['t1'].handleError.called.should.be.true;
done();
});
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
});
});
it('passes error to flows that use the originating global config', function(done) {
var originalConfig = [
{id:"configNode",type:"test"},
{id:"t1",type:"tab"},
{id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]},
{id:"t2",type:"tab"},
{id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]},
{id:"t3",type:"tab"},
{id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
events.once('nodes-started',function() {
flows.handleError(originalConfig[0],"message",{});
try {
flowCreate.flows['t1'].handleError.called.should.be.true;
flowCreate.flows['t2'].handleError.called.should.be.false;
flowCreate.flows['t3'].handleError.called.should.be.true;
done();
} catch(err) {
done(err);
}
});
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
});
});
});
describe('#handleStatus', function() {
it('passes status to correct flow', function(done) {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
events.once('nodes-started',function() {
flows.handleStatus(originalConfig[0],"message");
flowCreate.flows['t1'].handleStatus.called.should.be.true;
done();
});
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
});
});
it('passes status to flows that use the originating global config', function(done) {
var originalConfig = [
{id:"configNode",type:"test"},
{id:"t1",type:"tab"},
{id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]},
{id:"t2",type:"tab"},
{id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]},
{id:"t3",type:"tab"},
{id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]}
];
storage.getFlows = function() {
return when.resolve(originalConfig);
}
events.once('nodes-started',function() {
flows.handleStatus(originalConfig[0],"message");
try {
flowCreate.flows['t1'].handleStatus.called.should.be.true;
flowCreate.flows['t2'].handleStatus.called.should.be.false;
flowCreate.flows['t3'].handleStatus.called.should.be.true;
done();
} catch(err) {
done(err);
}
});
flows.init({},storage);
flows.load().then(function() {
flows.startFlows();
});
});
});
});

View File

@ -0,0 +1,604 @@
/**
* Copyright 2015 IBM Corp.
*
* 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 should = require("should");
var sinon = require("sinon");
var when = require("when");
var clone = require("clone");
var flowUtil = require("../../../../red/nodes/flows/util");
var typeRegistry = require("../../../../red/nodes/registry");
var redUtil = require("../../../../red/util");
describe('flows/util', function() {
var getType;
before(function() {
getType = sinon.stub(typeRegistry,"get",function(type) {
return type!=='missing';
});
});
after(function() {
getType.restore();
});
describe('#diffNodes',function() {
it('handles a null old node', function() {
flowUtil.diffNodes(null,{}).should.be.true;
});
it('ignores x/y changes', function() {
flowUtil.diffNodes({x:10,y:10},{x:20,y:10}).should.be.false;
flowUtil.diffNodes({x:10,y:10},{x:10,y:20}).should.be.false;
});
it('ignores wiring changes', function() {
flowUtil.diffNodes({wires:[]},{wires:[1,2,3]}).should.be.false;
});
it('spots existing property change - string', function() {
flowUtil.diffNodes({a:"foo"},{a:"bar"}).should.be.true;
});
it('spots existing property change - number', function() {
flowUtil.diffNodes({a:0},{a:1}).should.be.true;
});
it('spots existing property change - boolean', function() {
flowUtil.diffNodes({a:true},{a:false}).should.be.true;
});
it('spots existing property change - truthy', function() {
flowUtil.diffNodes({a:true},{a:1}).should.be.true;
});
it('spots existing property change - falsey', function() {
flowUtil.diffNodes({a:false},{a:0}).should.be.true;
});
it('spots existing property change - array', function() {
flowUtil.diffNodes({a:[0,1,2]},{a:[0,2,3]}).should.be.true;
});
it('spots existing property change - object', function() {
flowUtil.diffNodes({a:{a:[0,1,2]}},{a:{a:[0,2,3]}}).should.be.true;
flowUtil.diffNodes({a:{a:[0,1,2]}},{a:{b:[0,1,2]}}).should.be.true;
});
it('spots added property', function() {
flowUtil.diffNodes({a:"foo"},{a:"foo",b:"bar"}).should.be.true;
});
it('spots removed property', function() {
flowUtil.diffNodes({a:"foo",b:"bar"},{a:"foo"}).should.be.true;
});
});
describe('#parseConfig',function() {
it('parses a single-tab flow', function() {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t1",type:"tab"}
];
var parsedConfig = flowUtil.parseConfig(originalConfig);
var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"missingTypes":[]};
redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true;
});
it('parses a single-tab flow with global config node', function() {
var originalConfig = [
{id:"t1-1",x:10,y:10,z:"t1",type:"test",foo:"cn", wires:[]},
{id:"cn",type:"test"},
{id:"t1",type:"tab"}
];
var parsedConfig = flowUtil.parseConfig(originalConfig);
var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]};
redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true;
});
it('parses a multi-tab flow', function() {
var originalConfig = [
{id:"t1",type:"tab"},
{id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]},
{id:"t2",type:"tab"},
{id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]}
];
var parsedConfig = flowUtil.parseConfig(originalConfig);
var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"missingTypes":[]};
redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true;
});
it('parses a subflow flow', function() {
var originalConfig = [
{id:"t1",type:"tab"},
{id:"t1-1",x:10,y:10,z:"t1",type:"subflow:sf1",wires:[]},
{id:"sf1",type:"subflow"},
{id:"sf1-1",x:10,y:10,z:"sf1",type:"test",wires:[]}
];
var parsedConfig = flowUtil.parseConfig(originalConfig);
var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"missingTypes":[]};
redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true;
});
it('parses a flow with a missing type', function() {
var originalConfig = [
{id:"t1",type:"tab"},
{id:"t1-1",x:10,y:10,z:"t1",type:"sf1",wires:[]},
{id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]},
];
var parsedConfig = flowUtil.parseConfig(originalConfig);
parsedConfig.missingTypes.should.eql(['missing']);
var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]}}}},"missingTypes":["missing"]};
redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true;
});
});
describe('#diffConfigs', function() {
it('handles an identical configuration', function() {
var config = [{id:"123",type:"test",foo:"a",wires:[]}];
var originalConfig = flowUtil.parseConfig(clone(config));
var changedConfig = flowUtil.parseConfig(clone(config));
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.have.length(0);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.should.have.length(0);
});
it('identifies nodes with changed properties, including downstream linked', function() {
var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig[0].foo = "b";
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.eql(["1"]);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.should.eql(["2"]);
});
it('identifies nodes with changed properties, including upstream linked', function() {
var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig[1].bar = "c";
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.eql(["2"]);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.should.eql(["1"]);
});
it('identifies nodes with changed credentials, including downstream linked', function() {
var config = [{id:"1",type:"test",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig[0].credentials = {};
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.eql(["1"]);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.should.eql(["2"]);
});
it('identifies nodes with changed wiring', function() {
var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig[1].wires[0][0] = "3";
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.have.length(0);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.eql(["2"]);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('identifies nodes with changed wiring - second connection added', function() {
var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig[1].wires[0].push("1");
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.have.length(0);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.eql(["2"]);
diffResult.linked.sort().should.eql(["1"]);
});
it('identifies nodes with changed wiring - node connected', function() {
var config = [{id:"1",type:"test",foo:"a",wires:[["2"]]},{id:"2",type:"test",bar:"b",wires:[[]]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig[1].wires.push("3");
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.have.length(0);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.eql(["2"]);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('identifies new nodes', function() {
var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig.push({id:"2",type:"test",bar:"b",wires:[["1"]]});
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.eql(["2"]);
diffResult.changed.should.have.length(0);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1"]);
});
it('identifies deleted nodes', function() {
var config = [{id:"1",type:"test",foo:"a",wires:[["2"]]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}];
var newConfig = clone(config);
newConfig.splice(1,1);
newConfig[0].wires = [];
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.should.have.length(0);
diffResult.removed.should.eql(["2"]);
diffResult.rewired.should.eql(["1"]);
diffResult.linked.sort().should.eql(["3"]);
});
it('identifies config nodes changes', function() {
var config = [
{id:"1",type:"test",foo:"configNode",wires:[["2"]]},
{id:"2",type:"test",bar:"b",wires:[["3"]]},
{id:"3",type:"test",foo:"a",wires:[]},
{id:"configNode",type:"testConfig"}
];
var newConfig = clone(config);
newConfig[3].foo = "bar";
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(["1","configNode"]);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["2","3"]);
});
it('marks a parent subflow as changed for an internal property change', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow"},
{id:"sf1-1",z:"sf1",type:"test",foo:"a",wires:[]},
{id:"4",type:"subflow:sf1",wires:[]}
];
var newConfig = clone(config);
newConfig[4].foo = "b";
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', '4', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal wiring change', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow"},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig[4].wires = [["sf1-2"]];
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal node add', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow"},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig.push({id:"sf1-3",z:"sf1",type:"test",wires:[]});
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal node delete', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow"},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig.splice(5,1);
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal subflow wiring change - input removed', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig[3].in[0].wires = [];
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal subflow wiring change - input added', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig[3].in[0].wires.push({"id":"sf1-2"});
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal subflow wiring change - output added', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig[3].out[0].wires.push({"id":"sf1-2","port":0});
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal subflow wiring change - output removed', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig[3].out[0].wires = [];
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for a global config node change', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow"},
{id:"sf1-1",z:"sf1",prop:"configNode",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"test",wires:[]},
{id:"configNode",a:"foo",type:"test",wires:[]}
];
var newConfig = clone(config);
newConfig[6].a = "bar";
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', "configNode", 'sf1']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
it('marks a parent subflow as changed for an internal subflow instance change', function() {
var config = [
{id:"1",type:"test",wires:[["2"]]},
{id:"2",type:"subflow:sf1",wires:[["3"]]},
{id:"3",type:"test",wires:[]},
{id:"sf1",type:"subflow"},
{id:"sf2",type:"subflow"},
{id:"sf1-1",z:"sf1",type:"test",wires:[]},
{id:"sf1-2",z:"sf1",type:"subflow:sf2",wires:[]},
{id:"sf2-1",z:"sf2",type:"test",wires:[]},
{id:"sf2-2",z:"sf2",type:"test",wires:[]},
];
var newConfig = clone(config);
newConfig[8].a = "bar";
var originalConfig = flowUtil.parseConfig(config);
var changedConfig = flowUtil.parseConfig(newConfig);
originalConfig.missingTypes.should.have.length(0);
var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig);
diffResult.added.should.have.length(0);
diffResult.changed.sort().should.eql(['2', 'sf1', 'sf2']);
diffResult.removed.should.have.length(0);
diffResult.rewired.should.have.length(0);
diffResult.linked.sort().should.eql(["1","3"]);
});
});
});

View File

@ -1,230 +0,0 @@
/**
* Copyright 2014 IBM Corp.
*
* 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 should = require("should");
var sinon = require("sinon");
var when = require("when");
var clone = require("clone");
var flows = require("../../../red/nodes/flows");
var RedNode = require("../../../red/nodes/Node");
var RED = require("../../../red/nodes");
var events = require("../../../red/events");
var credentials = require("../../../red/nodes/credentials");
var typeRegistry = require("../../../red/nodes/registry");
var Flow = require("../../../red/nodes/Flow");
var settings = {
available: function() { return false; }
}
function loadFlows(testFlows, cb) {
var storage = {
getFlows: function() {
return when.resolve(testFlows);
},
getCredentials: function() {
return when.resolve({});
}
};
RED.init(settings, storage);
flows.load().then(function() {
should.deepEqual(testFlows, flows.getFlows());
cb();
});
}
describe('flows', function() {
afterEach(function(done) {
flows.stopFlows().then(function() {
loadFlows([],done);
});
});
describe('#load',function() {
it('should load nothing when storage is empty',function(done) {
loadFlows([], done);
});
it('should load and start an empty tab flow',function(done) {
loadFlows([{"type":"tab","id":"tab1","label":"Sheet 1"}], function() {});
events.once('nodes-started', function() { done(); });
});
it('should load and start a registered node type', function(done) {
RED.registerType('debug', function() {});
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
return RedNode;
});
loadFlows([{"id":"n1","type":"debug"}], function() { });
events.once('nodes-started', function() {
typeRegistryGet.restore();
done();
});
});
it('should load and start when node type is registered', function(done) {
var typeRegistryGet = sinon.stub(typeRegistry,"get");
typeRegistryGet.onCall(0).returns(null);
typeRegistryGet.returns(RedNode);
loadFlows([{"id":"n2","type":"inject"}], function() {
events.emit('type-registered','inject');
});
events.once('nodes-started', function() {
typeRegistryGet.restore();
done();
});
});
it('should not instantiate nodes of an unused subflow', function(done) {
RED.registerType('abc', function() {});
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
return RedNode;
});
loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[],wires:[]},
{"id":"n2","type":"abc","z":"n1",wires:[]}
],function() { });
events.once('nodes-started', function() {
(flows.get("n2") == null).should.be.true;
var ncount = 0
flows.eachNode(function(n) {
ncount++;
});
ncount.should.equal(0);
typeRegistryGet.restore();
done();
});
});
it('should instantiate nodes of an used subflow with new IDs', function(done) {
RED.registerType('abc', function() {});
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
return RedNode;
});
loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[]},
{"id":"n2","type":"abc","z":"n1","name":"def",wires:[]},
{"id":"n3","type":"subflow:n1"}
], function() { });
events.once('nodes-started', function() {
// n2 should not get instantiated with that id
(flows.get("n2") == null).should.be.true;
var ncount = 0
var nodes = [];
flows.eachNode(function(n) {
nodes.push(n);
});
nodes.should.have.lengthOf(2);
// Assume the nodes are instantiated in this order - not
// a requirement, but makes the test easier to write.
nodes[0].should.have.property("id","n3");
nodes[0].should.have.property("type","subflow:n1");
nodes[1].should.not.have.property("id","n2");
nodes[1].should.have.property("name","def");
// TODO: verify instance wiring is correct
typeRegistryGet.restore();
done();
});
});
});
describe('#setFlows',function() {
var credentialsExtact;
var credentialsSave;
var stopFlows;
var startFlows;
var credentialsExtractNode;
beforeEach(function() {
credentialsExtact = sinon.stub(credentials,"extract",function(node) {credentialsExtractNode = clone(node);delete node.credentials;});
credentialsSave = sinon.stub(credentials,"save",function() { return when.resolve();});
stopFlows = sinon.stub(flows,"stopFlows",function() {return when.resolve();});
startFlows = sinon.stub(flows,"startFlows",function() {});
});
afterEach(function() {
credentialsExtact.restore();
credentialsSave.restore();
startFlows.restore();
stopFlows.restore();
});
it('should extract credentials from nodes', function(done) {
var testFlow = [{"type":"testNode","credentials":{"a":1}},{"type":"testNode2"}];
var resultFlow = clone(testFlow);
var storage = { saveFlows: sinon.spy() };
flows.init({},storage);
flows.setFlows(testFlow,"full").then(function() {
try {
credentialsExtact.calledOnce.should.be.true;
// credential property stripped
testFlow.should.not.have.property("credentials");
credentialsExtractNode.should.eql(resultFlow[0]);
credentialsExtractNode.should.not.equal(resultFlow[0]);
credentialsSave.calledOnce.should.be.true;
storage.saveFlows.calledOnce.should.be.true;
storage.saveFlows.args[0][0].should.eql(testFlow);
stopFlows.calledOnce.should.be.true;
startFlows.calledOnce.should.be.true;
done();
} catch(err) {
done(err);
}
});
});
it('should apply diff on partial deployment', function(done) {
var testFlow = [{"type":"testNode"},{"type":"testNode2"}];
var testFlow2 = [{"type":"testNode3"},{"type":"testNode4"}];
var storage = { saveFlows: sinon.spy() };
flows.init({},storage);
flows.setFlows(testFlow,"full").then(function() {
flows.setFlows(testFlow2,"nodes").then(function() {
try {
credentialsExtact.called.should.be.false;
storage.saveFlows.calledTwice.should.be.true;
storage.saveFlows.args[1][0].should.eql(testFlow2);
stopFlows.calledTwice.should.be.true;
startFlows.calledTwice.should.be.true;
var configDiff = {
type: 'nodes',
stop: [],
rewire: [],
config: testFlow2
}
stopFlows.args[1][0].should.eql(configDiff);
startFlows.args[1][0].should.eql(configDiff);
done();
} catch(err) {
done(err);
}
});
});
});
});
});

View File

@ -21,8 +21,15 @@ var when = require("when");
var sinon = require('sinon');
var index = require("../../../red/nodes/index");
var flows = require("../../../red/nodes/flows");
describe("red/nodes/index", function() {
before(function() {
sinon.stub(flows,"startFlows");
});
after(function() {
flows.startFlows.restore();
});
afterEach(function() {
index.clearRegistry();

View File

@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var deprecated = require("../../../red/nodes/deprecated.js");
var deprecated = require("../../../../red/nodes/registry/deprecated.js");
describe('deprecated', function() {
it('should return info on a node',function() {

View File

@ -30,11 +30,11 @@ var log = require("../../red/log");
describe("red/server", function() {
var commsMessages = [];
var commsPublish;
beforeEach(function() {
commsMessages = [];
});
before(function() {
commsPublish = sinon.stub(comms,"publish", function(topic,msg,retained) {
commsMessages.push({topic:topic,msg:msg,retained:retained});
@ -43,22 +43,22 @@ describe("red/server", function() {
after(function() {
commsPublish.restore();
});
it("initialises components", function() {
var commsInit = sinon.stub(comms,"init",function() {});
var dummyServer = {};
server.init(dummyServer,{testSettings: true, httpAdminRoot:"/", load:function() { return when.resolve();}});
commsInit.called.should.be.true;
should.exist(server.app);
should.exist(server.nodeApp);
server.server.should.equal(dummyServer);
commsInit.restore();
});
describe("start",function() {
var commsInit;
var storageInit;
@ -73,8 +73,9 @@ describe("red/server", function() {
var redNodesCleanModuleList;
var redNodesGetNodeList;
var redNodesLoadFlows;
var redNodesStartFlows;
var commsStart;
beforeEach(function() {
commsInit = sinon.stub(comms,"init",function() {});
storageInit = sinon.stub(storage,"init",function(settings) {return when.resolve();});
@ -86,7 +87,8 @@ describe("red/server", function() {
redNodesInit = sinon.stub(redNodes,"init", function() {});
redNodesLoad = sinon.stub(redNodes,"load", function() {return when.resolve()});
redNodesCleanModuleList = sinon.stub(redNodes,"cleanModuleList",function(){});
redNodesLoadFlows = sinon.stub(redNodes,"loadFlows",function() {});
redNodesLoadFlows = sinon.stub(redNodes,"loadFlows",function() {return when.resolve()});
redNodesStartFlows = sinon.stub(redNodes,"startFlows",function() {});
commsStart = sinon.stub(comms,"start",function(){});
});
afterEach(function() {
@ -99,9 +101,10 @@ describe("red/server", function() {
logLog.restore();
redNodesInit.restore();
redNodesLoad.restore();
redNodesGetNodeList.restore();
redNodesGetNodeList.restore();
redNodesCleanModuleList.restore();
redNodesLoadFlows.restore();
redNodesStartFlows.restore();
commsStart.restore();
});
it("reports errored/missing modules",function(done) {
@ -120,7 +123,7 @@ describe("red/server", function() {
redNodesLoad.calledOnce.should.be.true;
commsStart.calledOnce.should.be.true;
redNodesLoadFlows.calledOnce.should.be.true;
logWarn.calledWithMatch("Failed to register 1 node type");
logWarn.calledWithMatch("Missing node modules");
logWarn.calledWithMatch(" - module: typeA, typeB");
@ -168,7 +171,7 @@ describe("red/server", function() {
});
server.init({},{testSettings: true, verbose:true, httpAdminRoot:"/", load:function() { return when.resolve();}});
server.start().then(function() {
try {
apiInit.calledOnce.should.be.true;
logWarn.neverCalledWithMatch("Failed to register 1 node type");
@ -179,7 +182,7 @@ describe("red/server", function() {
}
});
});
it("reports runtime metrics",function(done) {
var commsStop = sinon.stub(comms,"stop",function() {} );
var stopFlows = sinon.stub(redNodes,"stopFlows",function() {} );
@ -208,8 +211,8 @@ describe("red/server", function() {
}
},500);
});
});
});
it("doesn't init api if httpAdminRoot set to false",function(done) {
redNodesGetNodeList = sinon.stub(redNodes,"getNodeList", function() {return []});
server.init({},{testSettings: true, httpAdminRoot:false, load:function() { return when.resolve();}});
@ -225,20 +228,20 @@ describe("red/server", function() {
});
});
});
it("stops components", function() {
var commsStop = sinon.stub(comms,"stop",function() {} );
var stopFlows = sinon.stub(redNodes,"stopFlows",function() {} );
server.stop();
commsStop.called.should.be.true;
stopFlows.called.should.be.true;
commsStop.restore();
stopFlows.restore();
});
it("reports added modules", function() {
var nodes = {nodes:[
{types:["a"]},
@ -246,13 +249,13 @@ describe("red/server", function() {
{types:["c"],err:"error"}
]};
var result = server.reportAddedModules(nodes);
result.should.equal(nodes);
commsMessages.should.have.length(1);
commsMessages[0].topic.should.equal("node/added");
commsMessages[0].msg.should.eql(nodes.nodes);
});
it("reports removed modules", function() {
var nodes = [
{types:["a"]},
@ -260,13 +263,13 @@ describe("red/server", function() {
{types:["c"],err:"error"}
];
var result = server.reportRemovedModules(nodes);
result.should.equal(nodes);
commsMessages.should.have.length(1);
commsMessages[0].topic.should.equal("node/removed");
commsMessages[0].msg.should.eql(nodes);
});
describe("installs module", function() {
it("rejects invalid module names", function(done) {
var promises = [];
@ -278,12 +281,12 @@ describe("red/server", function() {
done();
});
});
it("rejects when npm returns a 404", function(done) {
var exec = sinon.stub(child_process,"exec",function(cmd,opt,cb) {
cb(new Error(),""," 404 this_wont_exist");
});
server.installModule("this_wont_exist").otherwise(function(err) {
err.code.should.be.eql(404);
done();
@ -295,7 +298,7 @@ describe("red/server", function() {
var exec = sinon.stub(child_process,"exec",function(cmd,opt,cb) {
cb(new Error("test_error"),"","");
});
server.installModule("this_wont_exist").then(function() {
done(new Error("Unexpected success"));
}).otherwise(function(err) {
@ -312,7 +315,7 @@ describe("red/server", function() {
var addModule = sinon.stub(redNodes,"addModule",function(md) {
return when.resolve(nodeInfo);
});
server.installModule("this_wont_exist").then(function(info) {
info.should.eql(nodeInfo);
commsMessages.should.have.length(1);
@ -347,7 +350,7 @@ describe("red/server", function() {
var exec = sinon.stub(child_process,"exec",function(cmd,opt,cb) {
cb(new Error("test_error"),"","");
});
server.uninstallModule("this_wont_exist").then(function() {
done(new Error("Unexpected success"));
}).otherwise(function(err) {
@ -366,7 +369,7 @@ describe("red/server", function() {
cb(null,"","");
});
var exists = sinon.stub(fs,"existsSync", function(fn) { return true; });
server.uninstallModule("this_wont_exist").then(function(info) {
info.should.eql(nodeInfo);
commsMessages.should.have.length(1);
@ -382,5 +385,5 @@ describe("red/server", function() {
});
});
});
});