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

Add dynamic node api

Closes #322
- nodes modules can be installed/removed dynamically at runtime
- nodes can be enabled/disabled
- onpaletteadd/onpaletteremove api added to node definitions
- initial implementation of nr-cli
This commit is contained in:
Nick O'Leary 2014-08-28 00:35:07 +01:00
parent 00cb8d5bce
commit da61fe12d0
24 changed files with 1540 additions and 381 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ flows.backup
nodes/node-red-nodes/ nodes/node-red-nodes/
.npm .npm
/coverage /coverage
.config.json

View File

@ -88,101 +88,104 @@
} }
}); });
} }
},
onpaletteadd: function() {
var content = document.createElement("div");
content.id = "tab-debug";
var toolbar = document.createElement("div");
toolbar.id = "debug-toolbar";
content.appendChild(toolbar);
toolbar.innerHTML = '<div class="btn-group pull-right"><a id="debug-tab-clear" title="clear log" class="btn btn-mini" href="#"><i class="fa fa-trash"></i></a></div> ';
var messages = document.createElement("div");
messages.id = "debug-content";
content.appendChild(messages);
RED.sidebar.addTab("debug",content);
function getTimestamp() {
var d = new Date();
return d.toLocaleString();
}
var sbc = document.getElementById("debug-content");
var messageCount = 0;
var that = this;
RED._debug = function(msg) {
that.handleDebugMessage("",{
name:"debug",
msg:msg
});
}
this.handleDebugMessage = function(t,o) {
var msg = document.createElement("div");
msg.onmouseover = function() {
msg.style.borderRightColor = "#999";
var n = RED.nodes.node(o.id);
if (n) {
n.highlighted = true;
n.dirty = true;
}
RED.view.redraw();
};
msg.onmouseout = function() {
msg.style.borderRightColor = "";
var n = RED.nodes.node(o.id);
if (n) {
n.highlighted = false;
n.dirty = true;
}
RED.view.redraw();
};
msg.onclick = function() {
var node = RED.nodes.node(o.id);
if (node) {
RED.view.showWorkspace(node.z);
}
};
var name = (o.name?o.name:o.id).toString().replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
var topic = (o.topic||"").toString().replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
var payload = (o.msg||"").toString().replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
msg.className = 'debug-message'+(o.level?(' debug-message-level-'+o.level):'')
msg.innerHTML = '<span class="debug-message-date">'+getTimestamp()+'</span>'+
'<span class="debug-message-name">['+name+']</span>'+
(o.topic?'<span class="debug-message-topic">'+topic+'</span>':'')+
'<span class="debug-message-payload">'+payload+'</span>';
var atBottom = (sbc.scrollHeight-messages.offsetHeight-sbc.scrollTop) < 5;
messageCount++;
$(messages).append(msg);
if (messageCount > 200) {
$("#debug-content .debug-message:first").remove();
messageCount--;
}
if (atBottom) {
$(sbc).scrollTop(sbc.scrollHeight);
}
};
RED.comms.subscribe("debug",this.handleDebugMessage);
$("#debug-tab-clear").click(function() {
$(".debug-message").remove();
messageCount = 0;
RED.nodes.eachNode(function(node) {
node.highlighted = false;
node.dirty = true;
});
RED.view.redraw();
});
},
onpaletteremove: function() {
RED.comms.unsubscribe("debug",this.handleDebugMessage);
RED.sidebar.removeTab("debug");
delete RED._debug;
} }
}); });
(function() {
var content = document.createElement("div");
content.id = "tab-debug";
var toolbar = document.createElement("div");
toolbar.id = "debug-toolbar";
content.appendChild(toolbar);
toolbar.innerHTML = '<div class="btn-group pull-right"><a id="debug-tab-clear" title="clear log" class="btn btn-mini" href="#"><i class="fa fa-trash"></i></a></div> ';
var messages = document.createElement("div");
messages.id = "debug-content";
content.appendChild(messages);
RED.sidebar.addTab("debug",content);
function getTimestamp() {
var d = new Date();
return d.toLocaleString();
}
var sbc = document.getElementById("debug-content");
var messageCount = 0;
RED._debug = function(msg) {
handleDebugMessage("",{
name:"debug",
msg:msg
});
}
var handleDebugMessage = function(t,o) {
var msg = document.createElement("div");
msg.onmouseover = function() {
msg.style.borderRightColor = "#999";
var n = RED.nodes.node(o.id);
if (n) {
n.highlighted = true;
n.dirty = true;
}
RED.view.redraw();
};
msg.onmouseout = function() {
msg.style.borderRightColor = "";
var n = RED.nodes.node(o.id);
if (n) {
n.highlighted = false;
n.dirty = true;
}
RED.view.redraw();
};
msg.onclick = function() {
var node = RED.nodes.node(o.id);
if (node) {
RED.view.showWorkspace(node.z);
}
};
var name = (o.name?o.name:o.id).toString().replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
var topic = (o.topic||"").toString().replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
var payload = (o.msg||"").toString().replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
msg.className = 'debug-message'+(o.level?(' debug-message-level-'+o.level):'')
msg.innerHTML = '<span class="debug-message-date">'+getTimestamp()+'</span>'+
'<span class="debug-message-name">['+name+']</span>'+
(o.topic?'<span class="debug-message-topic">'+topic+'</span>':'')+
'<span class="debug-message-payload">'+payload+'</span>';
var atBottom = (sbc.scrollHeight-messages.offsetHeight-sbc.scrollTop) < 5;
messageCount++;
$(messages).append(msg);
if (messageCount > 200) {
$("#debug-content .debug-message:first").remove();
messageCount--;
}
if (atBottom) {
$(sbc).scrollTop(sbc.scrollHeight);
}
};
RED.comms.subscribe("debug",handleDebugMessage);
$("#debug-tab-clear").click(function() {
$(".debug-message").remove();
messageCount = 0;
RED.nodes.eachNode(function(node) {
node.highlighted = false;
node.dirty = true;
});
RED.view.redraw();
});
})();
</script> </script>
<style> <style>

View File

@ -43,7 +43,8 @@
"uglify-js":"2.4.15", "uglify-js":"2.4.15",
"nodemailer":"1.3.0", "nodemailer":"1.3.0",
"imap":"0.8.13", "imap":"0.8.13",
"request":"2.42.0" "request":"2.42.0",
"colors":"0.6.2"
}, },
"devDependencies": { "devDependencies": {
"grunt": "0.4.5", "grunt": "0.4.5",

View File

@ -71,9 +71,23 @@ RED.comms = (function() {
} }
} }
function unsubscribe(topic,callback) {
if (subscriptions.topic) {
for (var i=0;i<subscriptions.topic.length;i++) {
if (subscriptions.topic[i] === callback) {
subscriptions.topic.splice(i,1);
break;
}
}
if (subscriptions.topic.length === 0) {
delete subscriptions.topic;
}
}
}
return { return {
connect: connectWS, connect: connectWS,
subscribe: subscribe subscribe: subscribe,
unsubscribe:unsubscribe
} }
})(); })();

View File

@ -145,17 +145,35 @@ var RED = (function() {
$.get('settings', function(data) { $.get('settings', function(data) {
RED.settings = data; RED.settings = data;
console.log("Node-RED: "+data.version); console.log("Node-RED: "+data.version);
loadNodes(); loadNodeList();
});
}
function loadNodeList() {
$.ajax({
headers: {
"Accept":"application/json"
},
url: 'nodes',
success: function(data) {
RED.nodes.setNodeList(data);
loadNodes();
}
}); });
} }
function loadNodes() { function loadNodes() {
$.get('nodes', function(data) { $.ajax({
$("body").append(data); headers: {
$(".palette-spinner").hide(); "Accept":"text/html"
$(".palette-scroll").show(); },
$("#palette-search").show(); url: 'nodes',
loadFlows(); success: function(data) {
$("body").append(data);
$(".palette-spinner").hide();
$(".palette-scroll").show();
$("#palette-search").show();
loadFlows();
}
}); });
} }
@ -176,24 +194,56 @@ var RED = (function() {
} }
}); });
RED.comms.subscribe("node/#",function(topic,msg) { RED.comms.subscribe("node/#",function(topic,msg) {
var i; var i,m;
var typeList;
var info;
if (topic == "node/added") { if (topic == "node/added") {
var addedTypes = [];
for (i=0;i<msg.length;i++) { for (i=0;i<msg.length;i++) {
var m = msg[i]; m = msg[i];
var id = m.id; var id = m.id;
$.get('nodes/'+id, function(data) { RED.nodes.addNodeSet(m);
$("body").append(data); if (m.loaded) {
var typeList = "<ul><li>"+m.types.join("</li><li>")+"</li></ul>"; addedTypes = addedTypes.concat(m.types);
RED.notify("Node"+(m.types.length!=1 ? "s":"")+" added to palette:"+typeList,"success"); $.get('nodes/'+id, function(data) {
}); $("body").append(data);
});
}
}
if (addedTypes.length) {
typeList = "<ul><li>"+addedTypes.join("</li><li>")+"</li></ul>";
RED.notify("Node"+(addedTypes.length!=1 ? "s":"")+" added to palette:"+typeList,"success");
} }
} else if (topic == "node/removed") { } else if (topic == "node/removed") {
if (msg.types) { for (i=0;i<msg.length;i++) {
for (i=0;i<msg.types.length;i++) { m = msg[i];
RED.palette.remove(msg.types[i]); info = RED.nodes.removeNodeSet(m.id);
if (info.added) {
typeList = "<ul><li>"+m.types.join("</li><li>")+"</li></ul>";
RED.notify("Node"+(m.types.length!=1 ? "s":"")+" removed from palette:"+typeList,"success");
} }
var typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>"; }
RED.notify("Node"+(msg.types.length!=1 ? "s":"")+" removed from palette:"+typeList,"success"); } else if (topic == "node/enabled") {
if (msg.types) {
info = RED.nodes.getNodeSet(msg.id);
if (info.added) {
RED.nodes.enableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify("Node"+(msg.types.length!=1 ? "s":"")+" enabled:"+typeList,"success");
} else {
$.get('nodes/'+msg.id, function(data) {
$("body").append(data);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify("Node"+(msg.types.length!=1 ? "s":"")+" added to palette:"+typeList,"success");
});
}
}
} else if (topic == "node/disabled") {
if (msg.types) {
RED.nodes.disableNodeSet(msg.id);
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify("Node"+(msg.types.length!=1 ? "s":"")+" disabled:"+typeList,"success");
} }
} }
}); });

View File

@ -21,21 +21,101 @@ RED.nodes = (function() {
var links = []; var links = [];
var defaultWorkspace; var defaultWorkspace;
var workspaces = {}; var workspaces = {};
function registerType(nt,def) { var registry = (function() {
node_defs[nt] = def; var nodeList = [];
// TODO: too tightly coupled into palette UI var nodeSets = {};
RED.palette.add(nt,def); var typeToId = {};
} var nodeDefinitions = {};
var exports = {
getNodeList: function() {
return nodeList;
},
setNodeList: function(list) {
nodeList = [];
for(var i=0;i<list.length;i++) {
var ns = list[i];
exports.addNodeSet(ns);
}
},
addNodeSet: function(ns) {
ns.added = false;
nodeSets[ns.id] = ns;
for (var j=0;j<ns.types.length;j++) {
typeToId[ns.types[j]] = ns.id;
}
nodeList.push(ns);
},
removeNodeSet: function(id) {
var ns = nodeSets[id];
for (var j=0;j<ns.types.length;j++) {
if (ns.added) {
// TODO: too tightly coupled into palette UI
RED.palette.remove(ns.types[j]);
var def = nodeDefinitions[ns.types[j]];
if (def.onpaletteremove && typeof def.onpaletteremove === "function") {
def.onpaletteremove.call(def);
}
}
delete typeToId[ns.types[j]];
}
delete nodeSets[id];
for (var i=0;i<nodeList.length;i++) {
if (nodeList[i].id == id) {
nodeList.splice(i,1);
break;
}
}
return ns;
},
getNodeSet: function(id) {
return nodeSets[id];
},
enableNodeSet: function(id) {
var ns = nodeSets[id];
ns.enabled = true;
for (var j=0;j<ns.types.length;j++) {
// TODO: too tightly coupled into palette UI
RED.palette.show(ns.types[j]);
var def = nodeDefinitions[ns.types[j]];
if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
def.onpaletteadd.call(def);
}
}
},
disableNodeSet: function(id) {
var ns = nodeSets[id];
ns.enabled = false;
for (var j=0;j<ns.types.length;j++) {
// TODO: too tightly coupled into palette UI
RED.palette.hide(ns.types[j]);
var def = nodeDefinitions[ns.types[j]];
if (def.onpaletteremove && typeof def.onpaletteremove === "function") {
def.onpaletteremove.call(def);
}
}
},
registerNodeType: function(nt,def) {
nodeDefinitions[nt] = def;
nodeSets[typeToId[nt]].added = true;
// TODO: too tightly coupled into palette UI
RED.palette.add(nt,def);
if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
def.onpaletteadd.call(def);
}
},
getNodeType: function(nt) {
return nodeDefinitions[nt];
}
}
return exports;
})();
function getID() { function getID() {
return (1+Math.random()*4294967295).toString(16); return (1+Math.random()*4294967295).toString(16);
} }
function getType(type) {
return node_defs[type];
}
function addNode(n) { function addNode(n) {
if (n._def.category == "config") { if (n._def.category == "config") {
configNodes[n.id] = n; configNodes[n.id] = n;
@ -48,7 +128,7 @@ RED.nodes = (function() {
if (n._def.defaults.hasOwnProperty(d)) { if (n._def.defaults.hasOwnProperty(d)) {
var property = n._def.defaults[d]; var property = n._def.defaults[d];
if (property.type) { if (property.type) {
var type = getType(property.type) var type = registry.getNodeType(property.type)
if (type && type.category == "config") { if (type && type.category == "config") {
var configNode = configNodes[n[d]]; var configNode = configNodes[n[d]];
if (configNode) { if (configNode) {
@ -101,7 +181,7 @@ RED.nodes = (function() {
if (node._def.defaults.hasOwnProperty(d)) { if (node._def.defaults.hasOwnProperty(d)) {
var property = node._def.defaults[d]; var property = node._def.defaults[d];
if (property.type) { if (property.type) {
var type = getType(property.type) var type = registry.getNodeType(property.type)
if (type && type.category == "config") { if (type && type.category == "config") {
var configNode = configNodes[node[d]]; var configNode = configNodes[node[d]];
if (configNode) { if (configNode) {
@ -229,7 +309,7 @@ RED.nodes = (function() {
for (var d in node._def.defaults) { for (var d in node._def.defaults) {
if (node._def.defaults[d].type && node[d] in configNodes) { if (node._def.defaults[d].type && node[d] in configNodes) {
var confNode = configNodes[node[d]]; var confNode = configNodes[node[d]];
var exportable = getType(node._def.defaults[d].type).exportable; var exportable = registry.getNodeType(node._def.defaults[d].type).exportable;
if ((exportable == null || exportable)) { if ((exportable == null || exportable)) {
if (!(node[d] in exportedConfigNodes)) { if (!(node[d] in exportedConfigNodes)) {
exportedConfigNodes[node[d]] = true; exportedConfigNodes[node[d]] = true;
@ -288,7 +368,7 @@ RED.nodes = (function() {
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
n = newNodes[i]; n = newNodes[i];
// TODO: remove workspace in next release+1 // TODO: remove workspace in next release+1
if (n.type != "workspace" && n.type != "tab" && !getType(n.type)) { if (n.type != "workspace" && n.type != "tab" && !registry.getNodeType(n.type)) {
// TODO: get this UI thing out of here! (see below as well) // TODO: get this UI thing out of here! (see below as well)
n.name = n.type; n.name = n.type;
n.type = "unknown"; n.type = "unknown";
@ -347,7 +427,7 @@ RED.nodes = (function() {
n = newNodes[i]; n = newNodes[i];
// TODO: remove workspace in next release+1 // TODO: remove workspace in next release+1
if (n.type !== "workspace" && n.type !== "tab") { if (n.type !== "workspace" && n.type !== "tab") {
var def = getType(n.type); var def = registry.getNodeType(n.type);
if (def && def.category == "config") { if (def && def.category == "config") {
if (!RED.nodes.node(n.id)) { if (!RED.nodes.node(n.id)) {
var configNode = {id:n.id,type:n.type,users:[]}; var configNode = {id:n.id,type:n.type,users:[]};
@ -424,8 +504,17 @@ RED.nodes = (function() {
} }
return { return {
registerType: registerType, registry:registry,
getType: getType, setNodeList: registry.setNodeList,
getNodeSet: registry.getNodeSet,
addNodeSet: registry.addNodeSet,
removeNodeSet: registry.removeNodeSet,
enableNodeSet: registry.enableNodeSet,
disableNodeSet: registry.disableNodeSet,
registerType: registry.registerNodeType,
getType: registry.getNodeType,
convertNode: convertNode, convertNode: convertNode,
add: addNode, add: addNode,
addLink: addLink, addLink: addLink,

View File

@ -29,14 +29,21 @@ RED.palette = (function() {
'<div id="palette-'+category+'-function"></div>'+ '<div id="palette-'+category+'-function"></div>'+
'</div>'+ '</div>'+
'</div>'); '</div>');
$("#header-"+category).on('click', function(e) {
$(this).next().slideToggle();
$(this).children("i").toggleClass("expanded");
});
} }
core.forEach(createCategoryContainer); core.forEach(createCategoryContainer);
function addNodeType(nt,def) { function addNodeType(nt,def) {
if ($("#palette_node_"+nt).length) { var nodeTypeId = nt.replace(" ","_");
if ($("#palette_node_"+nodeTypeId).length) {
return; return;
} }
@ -46,7 +53,7 @@ RED.palette = (function() {
var rootCategory = category.split("-")[0]; var rootCategory = category.split("-")[0];
var d = document.createElement("div"); var d = document.createElement("div");
d.id = "palette_node_"+nt; d.id = "palette_node_"+nodeTypeId;
d.type = nt; d.type = nt;
var label = /^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1]; var label = /^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];
@ -106,17 +113,21 @@ RED.palette = (function() {
revert: true, revert: true,
revertDuration: 50 revertDuration: 50
}); });
$("#header-"+category[0]).off('click').on('click', function(e) {
$(this).next().slideToggle();
$(this).children("i").toggleClass("expanded");
});
} }
} }
function removeNodeType(type) { function removeNodeType(nt) {
$("#palette_node_"+type).remove(); var nodeTypeId = nt.replace(" ","_");
$("#palette_node_"+nodeTypeId).remove();
}
function hideNodeType(nt) {
var nodeTypeId = nt.replace(" ","_");
$("#palette_node_"+nodeTypeId).hide();
}
function showNodeType(nt) {
var nodeTypeId = nt.replace(" ","_");
$("#palette_node_"+nodeTypeId).show();
} }
function filterChange() { function filterChange() {
@ -164,6 +175,8 @@ RED.palette = (function() {
return { return {
add:addNodeType, add:addNodeType,
remove:removeNodeType remove:removeNodeType,
hide:hideNodeType,
show:showNodeType
}; };
})(); })();

View File

@ -34,6 +34,10 @@ RED.sidebar = (function() {
//$('#sidebar').tabs("refresh"); //$('#sidebar').tabs("refresh");
} }
function removeTab(title) {
sidebar_tabs.removeTab("tab-"+title);
}
var sidebarSeparator = {}; var sidebarSeparator = {};
$("#sidebar-separator").draggable({ $("#sidebar-separator").draggable({
axis: "x", axis: "x",
@ -141,6 +145,7 @@ RED.sidebar = (function() {
return { return {
addTab: addTab, addTab: addTab,
removeTab: removeTab,
show: showSidebar, show: showSidebar,
containsTab: containsTab, containsTab: containsTab,
toggleSidebar: toggleSidebar toggleSidebar: toggleSidebar

179
red/bin/nr-cli.js Executable file
View File

@ -0,0 +1,179 @@
#!/usr/bin/env node
;(function() {
/**
* 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 request = require("request");
var colors = require('colors');
function formatBoolean(v,c) {
if (v) {
return ("["+c+"]");
} else {
return ("[ ]");
}
}
function formatNodeInfo(n) {
var inError = n.hasOwnProperty("err");
var str = formatBoolean(n.enabled,"X")+formatBoolean(n.loaded,"L")+" ";
str += n.id;
if (n.enabled && n.loaded) {
str = str.green;
} else if (n.enabled && n.err) {
str = str.red;
} else {
str = str.yellow;
}
if (n.module) {
str += " ["+n.module+"]";
}
str += " "+n.types.join(", ");
if (n.err) {
str+=" "+n.err.red;
}
return str;
}
var options;
if (process.argv[2] == "nodes") {
options = {
url: 'http://localhost:1880/nodes',
headers: {
'Accept': 'application/json'
}
};
request(options, function (error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
for (var i=0;i<info.length;i++) {
var n = info[i];
console.log(formatNodeInfo(n))
}
} else if (error) {
console.log(error.toString().red);
} else {
console.log((response.statusCode+": "+body).red);
}
});
} else if (process.argv[2] == "node" && process.argv[3]) {
options = {
url: 'http://localhost:1880/nodes/'+process.argv[3],
headers: {
'Accept': 'application/json'
}
};
request(options, function (error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
console.log(formatNodeInfo(info));
} else if (error) {
console.log(error.toString().red);
} else {
console.log((response.statusCode+": "+body).red);
}
});
} else if (process.argv[2] == "enable-node" && process.argv[3]) {
options = {
method: "PUT",
url: 'http://localhost:1880/nodes/'+process.argv[3],
headers: {
'Accept': 'application/json',
'content-type':'application/json'
},
body: JSON.stringify({enabled:true})
};
request(options, function (error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
console.log(formatNodeInfo(info));
} else if (error) {
console.log(error.toString().red);
} else {
console.log((response.statusCode+": "+body).red);
}
});
} else if (process.argv[2] == "disable-node" && process.argv[3]) {
options = {
method: "PUT",
url: 'http://localhost:1880/nodes/'+process.argv[3],
headers: {
'Accept': 'application/json',
'content-type':'application/json'
},
body: JSON.stringify({enabled:false})
};
request(options, function (error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
console.log(formatNodeInfo(info));
} else if (error) {
console.log(error.toString().red);
} else {
console.log((response.statusCode+": "+body).red);
}
});
} else if (process.argv[2] == "install" && process.argv[3]) {
options = {
method: "POST",
url: 'http://localhost:1880/nodes',
headers: {
'Accept': 'application/json',
'content-type':'application/json'
},
body: JSON.stringify({module:process.argv[3]})
};
request(options, function (error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
for (var i=0;i<info.length;i++) {
var n = info[i];
console.log(formatNodeInfo(n))
}
} else if (error) {
console.log(error.toString().red);
} else {
console.log((response.statusCode+": "+body).red);
}
});
} else if (process.argv[2] == "remove" && process.argv[3]) {
options = {
method: "DELETE",
url: 'http://localhost:1880/nodes/'+process.argv[3],
headers: {
'Accept': 'application/json',
}
};
request(options, function (error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
for (var i=0;i<info.length;i++) {
var n = info[i];
console.log(formatNodeInfo(n))
}
} else if (error) {
console.log(error.toString().red);
} else {
console.log((response.statusCode+": "+body).red);
}
});
}
})();

View File

@ -51,11 +51,10 @@ function init(_settings,storage) {
registry.init(_settings); registry.init(_settings);
} }
function checkTypeInUse(id) {
function removeNode(info) { var nodeInfo = registry.getNodeInfo(id);
var nodeInfo = registry.getNodeInfo(info);
if (!nodeInfo) { if (!nodeInfo) {
throw new Error("Unrecognised type/id: "+info); throw new Error("Unrecognised id: "+info);
} }
var inUse = {}; var inUse = {};
flows.each(function(n) { flows.each(function(n) {
@ -71,7 +70,25 @@ function removeNode(info) {
var msg = nodesInUse.join(", "); var msg = nodesInUse.join(", ");
throw new Error("Type in use: "+msg); throw new Error("Type in use: "+msg);
} }
return registry.removeNode(nodeInfo.id); }
function removeNode(id) {
checkTypeInUse(id);
return registry.removeNode(id);
}
function removeModule(module) {
var info = registry.getNodeModuleInfo(module);
for (var i=0;i<info.nodes.length;i++) {
checkTypeInUse(info.nodes[i]);
}
return registry.removeModule(module);
}
function disableNode(id) {
checkTypeInUse(id);
return registry.disableNode(id);
} }
module.exports = { module.exports = {
@ -86,9 +103,17 @@ module.exports = {
addNode: registry.addNode, addNode: registry.addNode,
removeNode: removeNode, removeNode: removeNode,
addModule: registry.addModule,
removeModule: removeModule,
enableNode: registry.enableNode,
disableNode: disableNode,
// Node type registry // Node type registry
registerType: registerType, registerType: registerType,
getType: registry.get, getType: registry.get,
getNodeInfo: registry.getNodeInfo,
getNodeModuleInfo: registry.getNodeModuleInfo,
getNodeList: registry.getNodeList, getNodeList: registry.getNodeList,
getNodeConfigs: registry.getNodeConfigs, getNodeConfigs: registry.getNodeConfigs,
getNodeConfig: registry.getNodeConfig, getNodeConfig: registry.getNodeConfig,

View File

@ -30,11 +30,17 @@ var settings;
function filterNodeInfo(n) { function filterNodeInfo(n) {
var r = { var r = {
id: n.id, id: n.id,
types: n.types,
name: n.name, name: n.name,
types: n.types,
enabled: n.enabled enabled: n.enabled
} }
if (n.err) { if (n.hasOwnProperty("loaded")) {
r.loaded = n.loaded;
}
if (n.hasOwnProperty("module")) {
r.module = n.module;
}
if (n.hasOwnProperty("err")) {
r.err = n.err.toString(); r.err = n.err.toString();
} }
return r; return r;
@ -46,14 +52,52 @@ var registry = (function() {
var nodeList = []; var nodeList = [];
var nodeConstructors = {}; var nodeConstructors = {};
var nodeTypeToId = {}; var nodeTypeToId = {};
var nodeModules = {};
function saveNodeList() {
var nodeList = {};
for (var i in nodeConfigs) {
if (nodeConfigs.hasOwnProperty(i)) {
var nodeConfig = nodeConfigs[i];
var n = filterNodeInfo(nodeConfig);
n.file = nodeConfig.file;
delete n.loaded;
delete n.err;
delete n.file;
delete n.id;
nodeList[i] = n;
}
}
settings.set("nodes",nodeList);
}
return { return {
init: function() {
if (settings.available()) {
nodeConfigs = settings.get("nodes")||{};
} else {
nodeConfigs = {};
}
nodeModules = {};
nodeTypeToId = {};
nodeConstructors = {};
nodeList = [];
nodeConfigCache = null;
},
addNodeSet: function(id,set) { addNodeSet: function(id,set) {
if (!set.err) { if (!set.err) {
set.types.forEach(function(t) { set.types.forEach(function(t) {
nodeTypeToId[t] = id; nodeTypeToId[t] = id;
}); });
} }
if (set.module) {
nodeModules[set.module] = nodeModules[set.module]||{nodes:[]};
nodeModules[set.module].nodes.push(id);
}
nodeConfigs[id] = set; nodeConfigs[id] = set;
nodeList.push(id); nodeList.push(id);
nodeConfigCache = null; nodeConfigCache = null;
@ -72,9 +116,27 @@ var registry = (function() {
delete nodeConstructors[t]; delete nodeConstructors[t];
delete nodeTypeToId[t]; delete nodeTypeToId[t];
}); });
config.enabled = false;
config.loaded = false;
nodeConfigCache = null; nodeConfigCache = null;
return filterNodeInfo(config); return filterNodeInfo(config);
}, },
removeModule: function(module) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var nodes = nodeModules[module];
if (!nodes) {
throw new Error("Unrecognised module: "+module);
}
var infoList = [];
for (var i=0;i<nodes.nodes.length;i++) {
infoList.push(registry.removeNode(nodes.nodes[i]));
}
delete nodeModules[module];
saveNodeList();
return infoList;
},
getNodeInfo: function(typeOrId) { getNodeInfo: function(typeOrId) {
if (nodeTypeToId[typeOrId]) { if (nodeTypeToId[typeOrId]) {
return filterNodeInfo(nodeConfigs[nodeTypeToId[typeOrId]]); return filterNodeInfo(nodeConfigs[nodeTypeToId[typeOrId]]);
@ -84,10 +146,13 @@ var registry = (function() {
return null; return null;
}, },
getNodeList: function() { getNodeList: function() {
return nodeList.map(function(id) { var list = [];
var n = nodeConfigs[id]; for (var id in nodeConfigs) {
return filterNodeInfo(n); if (nodeConfigs.hasOwnProperty(id)) {
}); list.push(filterNodeInfo(nodeConfigs[id]))
}
}
return list;
}, },
registerNodeConstructor: function(type,constructor) { registerNodeConstructor: function(type,constructor) {
if (nodeConstructors[type]) { if (nodeConstructors[type]) {
@ -112,7 +177,7 @@ var registry = (function() {
var script = ""; var script = "";
for (var i=0;i<nodeList.length;i++) { for (var i=0;i<nodeList.length;i++) {
var config = nodeConfigs[nodeList[i]]; var config = nodeConfigs[nodeList[i]];
if (config.enabled) { if (config.enabled && !config.err) {
result += config.config; result += config.config;
script += config.script; script += config.script;
} }
@ -131,7 +196,9 @@ var registry = (function() {
var config = nodeConfigs[id]; var config = nodeConfigs[id];
if (config) { if (config) {
var result = config.config; var result = config.config;
result += '<script type="text/javascript">'+config.script+'</script>'; if (config.script) {
result += '<script type="text/javascript">'+config.script+'</script>';
}
return result; return result;
} else { } else {
return null; return null;
@ -140,7 +207,7 @@ var registry = (function() {
getNodeConstructor: function(type) { getNodeConstructor: function(type) {
var config = nodeConfigs[nodeTypeToId[type]]; var config = nodeConfigs[nodeTypeToId[type]];
if (!config || config.enabled) { if (!config || (config.enabled && !config.err)) {
return nodeConstructors[type]; return nodeConstructors[type];
} }
return null; return null;
@ -158,25 +225,47 @@ var registry = (function() {
return nodeTypeToId[type]; return nodeTypeToId[type];
}, },
getModuleInfo: function(type) {
return nodeModules[type];
},
enableNodeSet: function(id) { enableNodeSet: function(id) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var config = nodeConfigs[id]; var config = nodeConfigs[id];
if (config) { if (config) {
if (config.err) { delete config.err;
throw new Error("cannot enable node with error");
}
config.enabled = true; config.enabled = true;
if (!config.loaded) {
// TODO: honour the promise this returns
loadNodeModule(config);
}
nodeConfigCache = null; nodeConfigCache = null;
saveNodeList();
} else {
throw new Error("Unrecognised id: "+id);
} }
return filterNodeInfo(config);
}, },
disableNodeSet: function(id) { disableNodeSet: function(id) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var config = nodeConfigs[id]; var config = nodeConfigs[id];
if (config) { if (config) {
// TODO: persist setting
config.enabled = false; config.enabled = false;
nodeConfigCache = null; nodeConfigCache = null;
saveNodeList();
} else {
throw new Error("Unrecognised id: "+id);
} }
return filterNodeInfo(config);
} },
saveNodeList: saveNodeList
} }
})(); })();
@ -185,6 +274,7 @@ var registry = (function() {
function init(_settings) { function init(_settings) {
Node = require("./Node"); Node = require("./Node");
settings = _settings; settings = _settings;
registry.init();
} }
/** /**
@ -244,22 +334,28 @@ function scanTreeForNodesModules(moduleName) {
var pm = path.join(dir,"node_modules"); var pm = path.join(dir,"node_modules");
try { try {
var files = fs.readdirSync(pm); var files = fs.readdirSync(pm);
files.forEach(function(fn) { for (var i=0;i<files.length;i++) {
if (!moduleName || fn == moduleName) { var fn = files[i];
var pkgfn = path.join(pm,fn,"package.json"); if (!registry.getModuleInfo(fn)) {
try { if (!moduleName || fn == moduleName) {
var pkg = require(pkgfn); var pkgfn = path.join(pm,fn,"package.json");
if (pkg['node-red']) { try {
var moduleDir = path.join(pm,fn); var pkg = require(pkgfn);
results.push({dir:moduleDir,package:pkg}); if (pkg['node-red']) {
var moduleDir = path.join(pm,fn);
results.push({dir:moduleDir,package:pkg});
}
} catch(err) {
if (err.code != "MODULE_NOT_FOUND") {
// TODO: handle unexpected error
}
} }
} catch(err) { if (fn == moduleName) {
if (err.code != "MODULE_NOT_FOUND") { break;
// TODO: handle unexpected error
} }
} }
} }
}); }
} catch(err) { } catch(err) {
} }
@ -313,15 +409,23 @@ function loadNodesFromModule(moduleDir,pkg) {
function loadNodeConfig(file,module,name) { function loadNodeConfig(file,module,name) {
var id = crypto.createHash('sha1').update(file).digest("hex"); var id = crypto.createHash('sha1').update(file).digest("hex");
if (registry.getNodeInfo(id)) { var info = registry.getNodeInfo(id);
throw new Error(file+" already loaded");
var isEnabled = true;
if (info) {
if (info.hasOwnProperty("loaded")) {
throw new Error(file+" already loaded");
}
isEnabled = info.enabled;
} }
var node = { var node = {
id: id, id: id,
file: file, file: file,
template: file.replace(/\.js$/,".html"), template: file.replace(/\.js$/,".html"),
enabled: true enabled: isEnabled,
loaded:false
} }
if (module) { if (module) {
@ -411,7 +515,9 @@ function load(defaultNodesDir,disableNodePathScan) {
when.settle(promises).then(function(results) { when.settle(promises).then(function(results) {
// Trigger a load of the configs to get it precached // Trigger a load of the configs to get it precached
registry.getAllNodeConfigs(); registry.getAllNodeConfigs();
if (settings.available()) {
registry.saveNodeList();
}
resolve(); resolve();
}); });
}); });
@ -428,6 +534,9 @@ function load(defaultNodesDir,disableNodePathScan) {
function loadNodeModule(node) { function loadNodeModule(node) {
var nodeDir = path.dirname(node.file); var nodeDir = path.dirname(node.file);
var nodeFn = path.basename(node.file); var nodeFn = path.basename(node.file);
if (!node.enabled) {
return when.resolve(node);
}
try { try {
var loadPromise = null; var loadPromise = null;
var r = require(node.file); var r = require(node.file);
@ -436,57 +545,74 @@ function loadNodeModule(node) {
if (promise != null && typeof promise.then === "function") { if (promise != null && typeof promise.then === "function") {
loadPromise = promise.then(function() { loadPromise = promise.then(function() {
node.enabled = true; node.enabled = true;
node.loaded = true;
return node; return node;
}).otherwise(function(err) { }).otherwise(function(err) {
node.err = err; node.err = err;
node.enabled = false;
return node; return node;
}); });
} }
} }
if (loadPromise == null) { if (loadPromise == null) {
node.enabled = true; node.enabled = true;
node.loaded = true;
loadPromise = when.resolve(node); loadPromise = when.resolve(node);
} }
return loadPromise; return loadPromise;
} catch(err) { } catch(err) {
node.err = err; node.err = err;
node.enabled = false;
return when.resolve(node); return when.resolve(node);
} }
} }
function addNode(options) { function loadNodeList(nodes) {
var nodes = [];
if (options.file) {
try {
nodes.push(loadNodeConfig(options.file));
} catch(err) {
return when.reject(err);
}
} else if (options.module) {
var moduleFiles = scanTreeForNodesModules(options.module);
if (moduleFiles.length === 0) {
var err = new Error("Cannot find module '" + options.module + "'");
err.code = 'MODULE_NOT_FOUND';
return when.reject(err);
}
moduleFiles.forEach(function(moduleFile) {
nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
});
}
var promises = []; var promises = [];
nodes.forEach(function(node) { nodes.forEach(function(node) {
promises.push(loadNodeModule(node)); promises.push(loadNodeModule(node));
}); });
return when.settle(promises).then(function(results) { return when.settle(promises).then(function(results) {
return results.map(function(r) { registry.saveNodeList();
var list = results.map(function(r) {
return filterNodeInfo(r.value); return filterNodeInfo(r.value);
}); });
return list;
}); });
} }
function addNode(file) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var nodes = [];
try {
nodes.push(loadNodeConfig(file));
} catch(err) {
return when.reject(err);
}
return loadNodeList(nodes);
}
function addModule(module) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var nodes = [];
if (registry.getModuleInfo(module)) {
return when.reject(new Error("Module already loaded"));
}
var moduleFiles = scanTreeForNodesModules(module);
if (moduleFiles.length === 0) {
var err = new Error("Cannot find module '" + module + "'");
err.code = 'MODULE_NOT_FOUND';
return when.reject(err);
}
moduleFiles.forEach(function(moduleFile) {
nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
});
return loadNodeList(nodes);
}
module.exports = { module.exports = {
init:init, init:init,
load:load, load:load,
@ -494,11 +620,15 @@ module.exports = {
registerType: registry.registerNodeConstructor, registerType: registry.registerNodeConstructor,
get: registry.getNodeConstructor, get: registry.getNodeConstructor,
getNodeInfo: registry.getNodeInfo, getNodeInfo: registry.getNodeInfo,
getNodeModuleInfo: registry.getModuleInfo,
getNodeList: registry.getNodeList, getNodeList: registry.getNodeList,
getNodeConfigs: registry.getAllNodeConfigs, getNodeConfigs: registry.getAllNodeConfigs,
getNodeConfig: registry.getNodeConfig, getNodeConfig: registry.getNodeConfig,
addNode: addNode, addNode: addNode,
removeNode: registry.removeNode, removeNode: registry.removeNode,
enableNode: registry.enableNodeSet, enableNode: registry.enableNodeSet,
disableNode: registry.disableNodeSet disableNode: registry.disableNodeSet,
addModule: addModule,
removeModule: registry.removeModule
} }

View File

@ -21,7 +21,7 @@ var comms = require("./comms");
var log = require("./log"); var log = require("./log");
var util = require("./util"); var util = require("./util");
var fs = require("fs"); var fs = require("fs");
var settings = null; var settings = require("./settings");
var credentials = require("./nodes/credentials"); var credentials = require("./nodes/credentials");
var path = require('path'); var path = require('path');
@ -33,9 +33,8 @@ var events = require("events");
var RED = { var RED = {
init: function(httpServer,userSettings) { init: function(httpServer,userSettings) {
settings = userSettings; userSettings.version = this.version();
settings.version = this.version(); settings.init(userSettings);
server.init(httpServer,settings); server.init(httpServer,settings);
library.init(); library.init();
return server.app; return server.app;
@ -49,6 +48,7 @@ var RED = {
events: events, events: events,
log: log, log: log,
comms: comms, comms: comms,
settings:settings,
util: util, util: util,
version: function () { version: function () {
var p = require(path.join(process.env.NODE_RED_HOME,"package.json")); var p = require(path.join(process.env.NODE_RED_HOME,"package.json"));
@ -64,6 +64,5 @@ RED.__defineGetter__("app", function() { console.log("Deprecated use of RED.app
RED.__defineGetter__("httpAdmin", function() { return server.app }); RED.__defineGetter__("httpAdmin", function() { return server.app });
RED.__defineGetter__("httpNode", function() { return server.nodeApp }); RED.__defineGetter__("httpNode", function() { return server.nodeApp });
RED.__defineGetter__("server", function() { return server.server }); RED.__defineGetter__("server", function() { return server.server });
RED.__defineGetter__("settings", function() { return settings });
module.exports = RED; module.exports = RED;

View File

@ -17,6 +17,7 @@
var express = require('express'); var express = require('express');
var util = require('util'); var util = require('util');
var when = require('when'); var when = require('when');
var exec = require('child_process').exec;
var createUI = require("./ui"); var createUI = require("./ui");
var redNodes = require("./nodes"); var redNodes = require("./nodes");
@ -36,10 +37,6 @@ function createServer(_server,_settings) {
app = createUI(settings); app = createUI(settings);
nodeApp = express(); nodeApp = express();
app.get("/nodes",function(req,res) {
res.send(redNodes.getNodeConfigs());
});
app.get("/flows",function(req,res) { app.get("/flows",function(req,res) {
res.json(redNodes.getFlows()); res.json(redNodes.getFlows());
}); });
@ -60,43 +57,82 @@ function createServer(_server,_settings) {
} }
); );
app.get("/nodes",function(req,res) {
if (req.get("accept") == "application/json") {
res.json(redNodes.getNodeList());
} else {
res.send(redNodes.getNodeConfigs());
}
});
app.post("/nodes", app.post("/nodes",
express.json(), express.json(),
function(req,res) { function(req,res) {
if (!settings.available()) {
res.send(400,new Error("Settings unavailable").toString());
return;
}
var node = req.body; var node = req.body;
if (!node.file && !node.module) { var promise;
if (node.file) {
promise = redNodes.addNode(node.file).then(reportAddedModules);
} else if (node.module) {
var module = redNodes.getNodeModuleInfo(node.module);
if (module) {
res.send(400,"Module already loaded");
return;
}
promise = installModule(node.module);
} else {
res.send(400,"Invalid request"); res.send(400,"Invalid request");
return; return;
} }
redNodes.addNode(node).then(function(info) { promise.then(function(info) {
comms.publish("node/added",info,false);
util.log("[red] Added node types:");
for (var j=0;j<info.length;j++) {
for (var i=0;i<info[j].types.length;i++) {
util.log("[red] - "+info[j].types[i]);
}
}
res.json(info); res.json(info);
}).otherwise(function(err) { }).otherwise(function(err) {
res.send(400,err.toString()); if (err.code === 404) {
res.send(404);
} else {
res.send(400,err.toString());
}
}); });
}, },
function(err,req,res,next) { function(err,req,res,next) {
console.log(err.toString());
res.send(400,err); res.send(400,err);
} }
); );
app.delete("/nodes/:id", app.delete("/nodes/:id",
function(req,res) { function(req,res) {
if (!settings.available()) {
res.send(400,new Error("Settings unavailable").toString());
return;
}
var id = req.params.id; var id = req.params.id;
var removedNodes = [];
try { try {
var info = redNodes.removeNode(id); var node = redNodes.getNodeInfo(id);
comms.publish("node/removed",info,false); var promise = null;
util.log("[red] Removed node types:"); if (!node) {
for (var i=0;i<info.types.length;i++) { var module = redNodes.getNodeModuleInfo(id);
util.log("[red] - "+info.types[i]); if (!module) {
res.send(404);
return;
} else {
promise = uninstallModule(id);
}
} else {
promise = when.resolve([redNodes.removeNode(id)]).then(reportRemovedModules);
} }
res.json(info);
promise.then(function(removedNodes) {
res.json(removedNodes);
}).otherwise(function(err) {
console.log(err.stack);
res.send(400,err.toString());
});
} catch(err) { } catch(err) {
res.send(400,err.toString()); res.send(400,err.toString());
} }
@ -108,46 +144,199 @@ function createServer(_server,_settings) {
app.get("/nodes/:id", function(req,res) { app.get("/nodes/:id", function(req,res) {
var id = req.params.id; var id = req.params.id;
var config = redNodes.getNodeConfig(id); var result = null;
if (config) { if (req.get("accept") == "application/json") {
res.send(config); result = redNodes.getNodeInfo(id);
} else {
result = redNodes.getNodeConfig(id);
}
if (result) {
res.send(result);
} else { } else {
res.send(404); res.send(404);
} }
}); });
app.put("/nodes/:id",
express.json(),
function(req,res) {
if (!settings.available()) {
res.send(400,new Error("Settings unavailable").toString());
return;
}
var body = req.body;
if (!body.hasOwnProperty("enabled")) {
res.send(400,"Invalid request");
return;
}
try {
var info;
var id = req.params.id;
var node = redNodes.getNodeInfo(id);
if (!node) {
res.send(404);
} else if (!node.err && node.enabled === body.enabled) {
res.json(node);
} else {
if (body.enabled) {
info = redNodes.enableNode(id);
} else {
info = redNodes.disableNode(id);
}
if (info.enabled == body.enabled && !info.err) {
comms.publish("node/"+(body.enabled?"enabled":"disabled"),info,false);
util.log("[red] "+(body.enabled?"Enabled":"Disabled")+" node types:");
for (var i=0;i<info.types.length;i++) {
util.log("[red] - "+info.types[i]);
}
} else if (body.enabled && info.err) {
util.log("[red] Failed to enable node:");
util.log("[red] - "+info.name+" : "+info.err);
}
res.json(info);
}
} catch(err) {
res.send(400,err.toString());
}
}
);
}
function reportAddedModules(info) {
comms.publish("node/added",info,false);
if (info.length > 0) {
util.log("[red] Added node types:");
for (var i=0;i<info.length;i++) {
for (var j=0;j<info[i].types.length;j++) {
util.log("[red] - "+
(info[i].module?info[i].module+":":"")+
info[i].types[j]+
(info[i].err?" : "+info[i].err:"")
);
}
}
}
return info;
}
function reportRemovedModules(removedNodes) {
comms.publish("node/removed",removedNodes,false);
util.log("[red] Removed node types:");
for (var j=0;j<removedNodes.length;j++) {
for (var i=0;i<removedNodes[j].types.length;i++) {
util.log("[red] - "+(removedNodes[i].module?removedNodes[i].module+":":"")+removedNodes[j].types[i]);
}
}
return removedNodes;
}
function installModule(module) {
//TODO: ensure module is 'safe'
return when.promise(function(resolve,reject) {
if (/[\s;]/.test(module)) {
reject(new Error("Invalid module name"));
return;
}
util.log("[red] Installing module: "+module);
var child = exec('npm install --production '+module, function(err, stdin, stdout) {
if (err) {
var lookFor404 = new RegExp(" 404 .*"+module+"$","m");
if (lookFor404.test(stdout)) {
util.log("[red] Installation of module "+module+" failed: not found");
var e = new Error();
e.code = 404;
reject(e);
} else {
util.log("[red] Installation of module "+module+" failed:");
util.log("------------------------------------------");
console.log(err.toString());
util.log("------------------------------------------");
reject(new Error("Install failed"));
}
} else {
util.log("[red] Installed module: "+module);
resolve(redNodes.addModule(module).then(reportAddedModules));
}
});
});
}
function uninstallModule(module) {
var list = redNodes.removeModule(module);
return when.promise(function(resolve,reject) {
if (/[\s;]/.test(module)) {
reject(new Error("Invalid module name"));
return;
}
util.log("[red] Removing module: "+module);
var child = exec('npm remove '+module, function(err, stdin, stdout) {
if (err) {
util.log("[red] Removal of module "+module+" failed:");
util.log("------------------------------------------");
console.log(err.toString());
util.log("------------------------------------------");
reject(new Error("Removal failed"));
} else {
util.log("[red] Removed module: "+module);
reportRemovedModules(list);
resolve(list);
}
});
});
} }
function start() { function start() {
var defer = when.defer(); var defer = when.defer();
storage.init(settings).then(function() { storage.init(settings).then(function() {
console.log("\nWelcome to Node-RED\n===================\n"); settings.load(storage).then(function() {
if (settings.version) { console.log("\nWelcome to Node-RED\n===================\n");
util.log("[red] Version: "+settings.version); if (settings.version) {
} util.log("[red] Version: "+settings.version);
util.log("[red] Loading palette nodes");
redNodes.init(settings,storage);
redNodes.load().then(function() {
var nodes = redNodes.getNodeList();
var nodeErrors = nodes.filter(function(n) { return n.err!=null;});
if (nodeErrors.length > 0) {
util.log("------------------------------------------");
if (settings.verbose) {
for (var i=0;i<nodeErrors.length;i+=1) {
util.log("["+nodeErrors[i].name+"] "+nodeErrors[i].err);
}
} else {
util.log("[red] Failed to register "+nodeErrors.length+" node type"+(nodeErrors.length==1?"":"s"));
util.log("[red] Run with -v for details");
}
util.log("------------------------------------------");
} }
defer.resolve(); util.log("[red] Loading palette nodes");
redNodes.init(settings,storage);
redNodes.loadFlows(); redNodes.load().then(function() {
var i;
var nodes = redNodes.getNodeList();
var nodeErrors = nodes.filter(function(n) { return n.err!=null;});
var nodeMissing = nodes.filter(function(n) { return n.module && n.enabled && !n.loaded && !n.err;});
if (nodeErrors.length > 0) {
util.log("------------------------------------------");
if (settings.verbose) {
for (i=0;i<nodeErrors.length;i+=1) {
util.log("["+nodeErrors[i].name+"] "+nodeErrors[i].err);
}
} else {
util.log("[red] Failed to register "+nodeErrors.length+" node type"+(nodeErrors.length==1?"":"s"));
util.log("[red] Run with -v for details");
}
util.log("------------------------------------------");
}
if (nodeMissing.length > 0) {
util.log("[red] Missing node modules:");
var missingModules = {};
for (i=0;i<nodeMissing.length;i++) {
var missing = nodeMissing[i];
missingModules[missing.module] = (missingModules[missing.module]||[]).concat(missing.types);
}
var promises = [];
for (i in missingModules) {
if (missingModules.hasOwnProperty(i)) {
util.log(" - "+i+": "+missingModules[i].join(", "));
promises.push(installModule(i));
}
}
}
defer.resolve();
redNodes.loadFlows();
}).otherwise(function(err) {
console.log(err);
});
comms.start();
}); });
comms.start();
}).otherwise(function(err) { }).otherwise(function(err) {
defer.reject(err); defer.reject(err);
}); });

70
red/settings.js Normal file
View File

@ -0,0 +1,70 @@
/**
* 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 when = require("when");
var userSettings = null;
var globalSettings = null;
var storage = null;
var persistentSettings = {
init: function(settings) {
userSettings = settings;
for (var i in settings) {
if (settings.hasOwnProperty(i)) {
(function() {
var j = i;
persistentSettings.__defineGetter__(j,function() { return userSettings[j]; });
persistentSettings.__defineSetter__(j,function() { throw new Error("Property '"+i+"' is read-only"); });
})();
}
}
globalSettings = null;
},
load: function(_storage) {
storage = _storage;
return storage.getSettings().then(function(_settings) {
globalSettings = _settings;
});
},
get: function(prop) {
if (userSettings.hasOwnProperty(prop)) {
return userSettings[prop];
}
if (globalSettings === null) {
throw new Error("Settings not available");
}
return globalSettings[prop];
},
set: function(prop,value) {
if (userSettings.hasOwnProperty(prop)) {
throw new Error("Property '"+prop+"' is read-only");
}
if (globalSettings === null) {
throw new Error("Settings not available");
}
globalSettings[prop] = value;
return storage.saveSettings(globalSettings);
},
available: function() {
return (globalSettings !== null);
}
}
module.exports = persistentSettings;

View File

@ -1,5 +1,5 @@
/** /**
* Copyright 2013 IBM Corp. * Copyright 2013, 2014 IBM Corp.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
var when = require('when'); var when = require('when');
var storageModule; var storageModule;
var settingsAvailable;
function moduleSelector(aSettings) { function moduleSelector(aSettings) {
var toReturn; var toReturn;
@ -38,48 +39,64 @@ function is_malicious(path) {
} }
var storageModuleInterface = { var storageModuleInterface = {
init : function(settings) { init: function(settings) {
try { try {
storageModule = moduleSelector(settings); storageModule = moduleSelector(settings);
settingsAvailable = storageModule.hasOwnProperty("getSettings") && storageModule.hasOwnProperty("saveSettings");
} catch (e) { } catch (e) {
return when.reject(e); return when.reject(e);
} }
return storageModule.init(settings); return storageModule.init(settings);
}, },
getFlows : function() { getFlows: function() {
return storageModule.getFlows(); return storageModule.getFlows();
}, },
saveFlows : function(flows) { saveFlows: function(flows) {
return storageModule.saveFlows(flows); return storageModule.saveFlows(flows);
}, },
getCredentials : function() { getCredentials: function() {
return storageModule.getCredentials(); return storageModule.getCredentials();
}, },
saveCredentials : function(credentials) { saveCredentials: function(credentials) {
return storageModule.saveCredentials(credentials); return storageModule.saveCredentials(credentials);
}, },
getAllFlows : function() { getSettings: function() {
if (settingsAvailable) {
return storageModule.getSettings();
} else {
return when.resolve(null);
}
},
saveSettings: function(settings) {
if (settingsAvailable) {
return storageModule.saveSettings(settings);
} else {
return when.resolve();
}
},
/* Library Functions */
getAllFlows: function() {
return storageModule.getAllFlows(); return storageModule.getAllFlows();
}, },
getFlow : function(fn) { getFlow: function(fn) {
if (is_malicious(fn)) { if (is_malicious(fn)) {
return when.reject(new Error('forbidden flow name')); return when.reject(new Error('forbidden flow name'));
} }
return storageModule.getFlow(fn); return storageModule.getFlow(fn);
}, },
saveFlow : function(fn, data) { saveFlow: function(fn, data) {
if (is_malicious(fn)) { if (is_malicious(fn)) {
return when.reject(new Error('forbidden flow name')); return when.reject(new Error('forbidden flow name'));
} }
return storageModule.saveFlow(fn, data); return storageModule.saveFlow(fn, data);
}, },
getLibraryEntry : function(type, path) { getLibraryEntry: function(type, path) {
if (is_malicious(path)) { if (is_malicious(path)) {
return when.reject(new Error('forbidden flow name')); return when.reject(new Error('forbidden flow name'));
} }
return storageModule.getLibraryEntry(type, path); return storageModule.getLibraryEntry(type, path);
}, },
saveLibraryEntry : function(type, path, meta, body) { saveLibraryEntry: function(type, path, meta, body) {
if (is_malicious(path)) { if (is_malicious(path)) {
return when.reject(new Error('forbidden flow name')); return when.reject(new Error('forbidden flow name'));
} }

View File

@ -33,6 +33,7 @@ var oldCredentialsFile;
var userDir; var userDir;
var libDir; var libDir;
var libFlowsDir; var libFlowsDir;
var globalSettingsFile;
function listFiles(dir) { function listFiles(dir) {
var dirs = {}; var dirs = {};
@ -140,6 +141,9 @@ var localfilesystem = {
libDir = fspath.join(userDir,"lib"); libDir = fspath.join(userDir,"lib");
libFlowsDir = fspath.join(libDir,"flows"); libFlowsDir = fspath.join(libDir,"flows");
globalSettingsFile = fspath.join(userDir,".config.json");
return promiseDir(libFlowsDir); return promiseDir(libFlowsDir);
}, },
@ -207,7 +211,20 @@ var localfilesystem = {
return nodeFn.call(fs.writeFile, credentialsFile, credentialData) return nodeFn.call(fs.writeFile, credentialsFile, credentialData)
}, },
getSettings: function() {
if (fs.existsSync(globalSettingsFile)) {
return nodeFn.call(fs.readFile,globalSettingsFile,'utf8').then(function(data) {
return JSON.parse(data);
});
}
return when.resolve({});
},
saveSettings: function(settings) {
return nodeFn.call(fs.writeFile,globalSettingsFile,JSON.stringify(settings),'utf8');
},
getAllFlows: function() { getAllFlows: function() {
return listFiles(libFlowsDir); return listFiles(libFlowsDir);
}, },

View File

@ -51,36 +51,36 @@ var walkDirectory = function(dir, topdir, done) {
errReturned = true; errReturned = true;
return done(error); return done(error);
} }
} } else {
file = path.resolve(dir, file);
file = path.resolve(dir, file); fs.stat(file, function(err, stat) {
fs.stat(file, function(err, stat) { if (stat && stat.isDirectory()) {
if (stat && stat.isDirectory()) { walkDirectory(file, false, function(err) {
walkDirectory(file, false, function(err) { if (!error) {
if (!error) { error = err;
error = err; }
next();
});
} else {
if (path.extname(file) === ".js") {
var testFile = file.replace(jsdir, testdir).replace(".js", "_spec.js");
fs.exists(testFile, function (exists) {
try {
exists.should.equal(true, testFile + " does not exist");
} catch (err) {
if (!topdir) {
return done(err);
} else {
error = err;
return;
}
}
});
} }
next(); next();
});
} else {
if (path.extname(file) === ".js") {
var testFile = file.replace(jsdir, testdir).replace(".js", "_spec.js");
fs.exists(testFile, function (exists) {
try {
exists.should.equal(true, testFile + " does not exist");
} catch (err) {
if (!topdir) {
return done(err);
} else {
error = err;
return;
}
}
});
} }
next(); });
} }
});
})(); })();
}); });
}; };

View File

@ -51,7 +51,11 @@ module.exports = {
return defer.promise; return defer.promise;
}, },
}; };
redNodes.init({}, storage); var settings = {
available: function() { return false; }
}
redNodes.init(settings, storage);
RED.nodes.registerType("helper", helperNode); RED.nodes.registerType("helper", helperNode);
testNode(RED); testNode(RED);
flows.load().then(function() { flows.load().then(function() {

View File

View File

@ -170,6 +170,12 @@ describe('Credentials', function() {
}, },
saveCredentials: function(creds) { saveCredentials: function(creds) {
return when(true); return when(true);
},
getSettings: function() {
return when({});
},
saveSettings: function(s) {
return when();
} }
}; };
function TestNode(n) { function TestNode(n) {
@ -188,8 +194,10 @@ describe('Credentials', function() {
sinon.stub(util, 'log', function(msg) { sinon.stub(util, 'log', function(msg) {
logmsg = msg; logmsg = msg;
}); });
var settings = {
index.init({}, storage); available: function() { return false;}
}
index.init(settings, storage);
index.registerType('test', TestNode); index.registerType('test', TestNode);
index.loadFlows().then(function() { index.loadFlows().then(function() {
var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); var testnode = new TestNode({id:'tab1',type:'test',name:'barney'});

View File

@ -15,26 +15,29 @@
**/ **/
var should = require("should"); var should = require("should");
var sinon = require("sinon");
var when = require("when"); var when = require("when");
var flows = require("../../../red/nodes/flows"); var flows = require("../../../red/nodes/flows");
var RedNode = require("../../../red/nodes/Node"); var RedNode = require("../../../red/nodes/Node");
var RED = require("../../../red/nodes"); var RED = require("../../../red/nodes");
var events = require("../../../red/events"); var events = require("../../../red/events");
var typeRegistry = require("../../../red/nodes/registry");
var settings = {
available: function() { return false; }
}
function loadFlows(testFlows, cb) { function loadFlows(testFlows, cb) {
var storage = { var storage = {
getFlows: function() { getFlows: function() {
var defer = when.defer(); return when.resolve(testFlows);
defer.resolve(testFlows);
return defer.promise;
}, },
getCredentials: function() { getCredentials: function() {
var defer = when.defer(); return when.resolve({});
defer.resolve({}); }
return defer.promise;
},
}; };
RED.init({}, storage); RED.init(settings, storage);
flows.load().then(function() { flows.load().then(function() {
should.deepEqual(testFlows, flows.getFlows()); should.deepEqual(testFlows, flows.getFlows());
cb(); cb();
@ -76,24 +79,34 @@ describe('flows', function() {
}); });
it('should load and start an empty tab flow',function(done) { it('should load and start an empty tab flow',function(done) {
loadFlows([{"type":"tab","id":"tab1","label":"Sheet 1"}], loadFlows([{"type":"tab","id":"tab1","label":"Sheet 1"}], function() {});
function() {});
events.once('nodes-started', function() { done(); }); events.once('nodes-started', function() { done(); });
}); });
it('should load and start a registered node type', function(done) { it('should load and start a registered node type', function(done) {
RED.registerType('debug', function() {}); RED.registerType('debug', function() {});
var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) {
return function() {};
});
loadFlows([{"id":"n1","type":"debug"}], function() { }); loadFlows([{"id":"n1","type":"debug"}], function() { });
events.once('nodes-started', function() { done(); }); events.once('nodes-started', function() {
typeRegistryGet.restore();
done();
});
}); });
it('should load and start when node type is registered', it('should load and start when node type is registered', function(done) {
function(done) { var typeRegistryGet = sinon.stub(typeRegistry,"get");
loadFlows([{"id":"n2","type":"inject"}], typeRegistryGet.onCall(0).returns(null);
function() { typeRegistryGet.returns(function(){});
RED.registerType('inject', function() { });
}); loadFlows([{"id":"n2","type":"inject"}], function() {
events.once('nodes-started', function() { done(); }); events.emit('type-registered','inject');
});
events.once('nodes-started', function() {
typeRegistryGet.restore();
done();
});
}); });
}); });
@ -112,7 +125,7 @@ describe('flows', function() {
return when(true); return when(true);
} }
}; };
RED.init({}, storage); RED.init(settings, storage);
flows.setFlows(testFlows); flows.setFlows(testFlows);
events.once('nodes-started', function() { done(); }); events.once('nodes-started', function() { done(); });
}); });

View File

@ -43,7 +43,11 @@ describe("red/nodes/index", function() {
saveCredentials: function(creds) { saveCredentials: function(creds) {
return when(true); return when(true);
} }
}; };
var settings = {
available: function() { return false }
};
function TestNode(n) { function TestNode(n) {
index.createNode(this, n); index.createNode(this, n);
@ -55,7 +59,7 @@ describe("red/nodes/index", function() {
it('nodes are initialised with credentials',function(done) { it('nodes are initialised with credentials',function(done) {
index.init({}, storage); index.init(settings, storage);
index.registerType('test', TestNode); index.registerType('test', TestNode);
index.loadFlows().then(function() { index.loadFlows().then(function() {
var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); var testnode = new TestNode({id:'tab1',type:'test',name:'barney'});
@ -69,7 +73,7 @@ describe("red/nodes/index", function() {
}); });
it('flows should be initialised',function(done) { it('flows should be initialised',function(done) {
index.init({}, storage); index.init(settings, storage);
index.loadFlows().then(function() { index.loadFlows().then(function() {
should.deepEqual(testFlows, index.getFlows()); should.deepEqual(testFlows, index.getFlows());
done(); done();
@ -131,7 +135,7 @@ describe("red/nodes/index", function() {
}); });
describe('allows nodes to be removed from the registry', function() { describe('allows nodes to be added/remove/enabled/disabled from the registry', function() {
var registry = require("../../../red/nodes/registry"); var registry = require("../../../red/nodes/registry");
var randomNodeInfo = {id:"5678",types:["random"]}; var randomNodeInfo = {id:"5678",types:["random"]};
@ -148,17 +152,21 @@ describe("red/nodes/index", function() {
sinon.stub(registry,"removeNode",function(id) { sinon.stub(registry,"removeNode",function(id) {
return randomNodeInfo; return randomNodeInfo;
}); });
sinon.stub(registry,"disableNode",function(id) {
return randomNodeInfo;
});
}); });
after(function() { after(function() {
registry.getNodeInfo.restore(); registry.getNodeInfo.restore();
registry.removeNode.restore(); registry.removeNode.restore();
registry.disableNode.restore();
}); });
it(': allows an unused node type to be removed',function(done) { it(': allows an unused node type to be removed',function(done) {
index.init({}, storage); index.init(settings, storage);
index.registerType('test', TestNode); index.registerType('test', TestNode);
index.loadFlows().then(function() { index.loadFlows().then(function() {
var info = index.removeNode("random"); var info = index.removeNode("5678");
registry.removeNode.calledOnce.should.be.true; registry.removeNode.calledOnce.should.be.true;
registry.removeNode.calledWith("5678").should.be.true; registry.removeNode.calledWith("5678").should.be.true;
info.should.eql(randomNodeInfo); info.should.eql(randomNodeInfo);
@ -167,9 +175,23 @@ describe("red/nodes/index", function() {
done(err); done(err);
}); });
}); });
it(': allows an unused node type to be disabled',function(done) {
index.init(settings, storage);
index.registerType('test', TestNode);
index.loadFlows().then(function() {
var info = index.disableNode("5678");
registry.disableNode.calledOnce.should.be.true;
registry.disableNode.calledWith("5678").should.be.true;
info.should.eql(randomNodeInfo);
done();
}).otherwise(function(err) {
done(err);
});
});
it(': prevents removing a node type that is in use',function(done) { it(': prevents removing a node type that is in use',function(done) {
index.init({}, storage); index.init(settings, storage);
index.registerType('test', TestNode); index.registerType('test', TestNode);
index.loadFlows().then(function() { index.loadFlows().then(function() {
/*jshint immed: false */ /*jshint immed: false */
@ -183,8 +205,23 @@ describe("red/nodes/index", function() {
}); });
}); });
it(': prevents disabling a node type that is in use',function(done) {
index.init(settings, storage);
index.registerType('test', TestNode);
index.loadFlows().then(function() {
/*jshint immed: false */
(function() {
index.disabledNode("test");
}).should.throw();
done();
}).otherwise(function(err) {
done(err);
});
});
it(': prevents removing a node type that is unknown',function(done) { it(': prevents removing a node type that is unknown',function(done) {
index.init({}, storage); index.init(settings, storage);
index.registerType('test', TestNode); index.registerType('test', TestNode);
index.loadFlows().then(function() { index.loadFlows().then(function() {
/*jshint immed: false */ /*jshint immed: false */
@ -192,6 +229,20 @@ describe("red/nodes/index", function() {
index.removeNode("doesnotexist"); index.removeNode("doesnotexist");
}).should.throw(); }).should.throw();
done();
}).otherwise(function(err) {
done(err);
});
});
it(': prevents disabling a node type that is unknown',function(done) {
index.init(settings, storage);
index.registerType('test', TestNode);
index.loadFlows().then(function() {
/*jshint immed: false */
(function() {
index.disableNode("doesnotexist");
}).should.throw();
done(); done();
}).otherwise(function(err) { }).otherwise(function(err) {
done(err); done(err);

View File

@ -31,6 +31,15 @@ describe('NodeRegistry', function() {
var resourcesDir = __dirname+ path.sep + "resources" + path.sep; var resourcesDir = __dirname+ path.sep + "resources" + path.sep;
function stubSettings(s,available) {
s.available = function() {return available;}
s.set = function(s,v) {},
s.get = function(s) { return null;}
return s
}
var settings = stubSettings({},false);
var settingsWithStorage = stubSettings({},true);
it('automatically registers new nodes',function() { it('automatically registers new nodes',function() {
var testNode = RedNodes.getNode('123'); var testNode = RedNodes.getNode('123');
should.not.exist(n); should.not.exist(n);
@ -42,7 +51,7 @@ describe('NodeRegistry', function() {
}); });
it('handles nodes that export a function', function(done) { it('handles nodes that export a function', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir + "TestNode1",true).then(function() { typeRegistry.load(resourcesDir + "TestNode1",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -64,7 +73,7 @@ describe('NodeRegistry', function() {
it('handles nodes that export a function returning a resolving promise', function(done) { it('handles nodes that export a function returning a resolving promise', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir + "TestNode2",true).then(function() { typeRegistry.load(resourcesDir + "TestNode2",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -84,15 +93,14 @@ describe('NodeRegistry', function() {
}); });
it('handles nodes that export a function returning a rejecting promise', function(done) { it('handles nodes that export a function returning a rejecting promise', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir + "TestNode3",true).then(function() { typeRegistry.load(resourcesDir + "TestNode3",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
list[0].should.have.property("id"); list[0].should.have.property("id");
list[0].should.have.property("name","TestNode3.js"); list[0].should.have.property("name","TestNode3.js");
list[0].should.have.property("types",["test-node-3"]); list[0].should.have.property("types",["test-node-3"]);
list[0].should.have.property("enabled",false); list[0].should.have.property("enabled",true);
list[0].should.have.property("err","fail"); list[0].should.have.property("err","fail");
var nodeConstructor = typeRegistry.get("test-node-3"); var nodeConstructor = typeRegistry.get("test-node-3");
@ -106,7 +114,7 @@ describe('NodeRegistry', function() {
}); });
it('handles files containing multiple nodes', function(done) { it('handles files containing multiple nodes', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir + "MultipleNodes1",true).then(function() { typeRegistry.load(resourcesDir + "MultipleNodes1",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -129,7 +137,7 @@ describe('NodeRegistry', function() {
}); });
it('handles nested directories', function(done) { it('handles nested directories', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir + "NestedDirectoryNode",true).then(function() { typeRegistry.load(resourcesDir + "NestedDirectoryNode",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -146,7 +154,7 @@ describe('NodeRegistry', function() {
it('emits type-registered and node-icon-dir events', function(done) { it('emits type-registered and node-icon-dir events', function(done) {
var eventEmitSpy = sinon.spy(events,"emit"); var eventEmitSpy = sinon.spy(events,"emit");
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir + "NestedDirectoryNode",true).then(function() { typeRegistry.load(resourcesDir + "NestedDirectoryNode",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -173,9 +181,10 @@ describe('NodeRegistry', function() {
}); });
it('rejects a duplicate node type registration', function(done) { it('rejects a duplicate node type registration', function(done) {
typeRegistry.init({
typeRegistry.init(stubSettings({
nodesDir:[resourcesDir + "TestNode1",resourcesDir + "DuplicateTestNode"] nodesDir:[resourcesDir + "TestNode1",resourcesDir + "DuplicateTestNode"]
}); },false));
typeRegistry.load("wontexist",true).then(function() { typeRegistry.load("wontexist",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
@ -191,7 +200,7 @@ describe('NodeRegistry', function() {
list[1].should.have.property("name","TestNode1.js"); list[1].should.have.property("name","TestNode1.js");
list[1].should.have.property("types",["test-node-1"]); list[1].should.have.property("types",["test-node-1"]);
list[1].should.have.property("enabled",false); list[1].should.have.property("enabled",true);
list[1].should.have.property("err"); list[1].should.have.property("err");
/already registered/.test(list[1].err).should.be.true; /already registered/.test(list[1].err).should.be.true;
@ -206,11 +215,10 @@ describe('NodeRegistry', function() {
}); });
it('handles nodesDir as a string', function(done) { it('handles nodesDir as a string', function(done) {
var settings = {
nodesDir :resourcesDir + "TestNode1"
}
typeRegistry.init(settings); typeRegistry.init(stubSettings({
nodesDir :resourcesDir + "TestNode1"
},false));
typeRegistry.load("wontexist",true).then(function(){ typeRegistry.load("wontexist",true).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -223,11 +231,10 @@ describe('NodeRegistry', function() {
}); });
it('handles invalid nodesDir',function(done) { it('handles invalid nodesDir',function(done) {
var settings = {
nodesDir : "wontexist"
}
typeRegistry.init(settings); typeRegistry.init(stubSettings({
nodesDir : "wontexist"
},false));
typeRegistry.load("wontexist",true).then(function(){ typeRegistry.load("wontexist",true).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty; list.should.be.an.Array.and.be.empty;
@ -238,7 +245,7 @@ describe('NodeRegistry', function() {
}); });
it('returns nothing for an unregistered type config', function() { it('returns nothing for an unregistered type config', function() {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load("wontexist",true).then(function(){ typeRegistry.load("wontexist",true).then(function(){
var config = typeRegistry.getNodeConfig("imaginary-shark"); var config = typeRegistry.getNodeConfig("imaginary-shark");
(config === null).should.be.true; (config === null).should.be.true;
@ -248,10 +255,10 @@ describe('NodeRegistry', function() {
}); });
it('excludes node files listed in nodesExcludes',function(done) { it('excludes node files listed in nodesExcludes',function(done) {
typeRegistry.init({ typeRegistry.init(stubSettings({
nodesExcludes: [ "TestNode1.js" ], nodesExcludes: [ "TestNode1.js" ],
nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2"] nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2"]
}); },false));
typeRegistry.load("wontexist",true).then(function() { typeRegistry.load("wontexist",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -263,9 +270,9 @@ describe('NodeRegistry', function() {
}); });
it('returns the node configurations', function(done) { it('returns the node configurations', function(done) {
typeRegistry.init({ typeRegistry.init(stubSettings({
nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2"] nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2"]
}); },false));
typeRegistry.load("wontexist",true).then(function() { typeRegistry.load("wontexist",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
@ -276,20 +283,59 @@ describe('NodeRegistry', function() {
var nodeId = list[0].id; var nodeId = list[0].id;
var nodeConfig = typeRegistry.getNodeConfig(nodeId); var nodeConfig = typeRegistry.getNodeConfig(nodeId);
nodeConfig.should.equal("<script type=\"text/x-red\" data-template-name=\"test-node-1\"></script>\n<script type=\"text/x-red\" data-help-name=\"test-node-1\"></script>\n<script type=\"text/javascript\">RED.nodes.registerType('test-node-1',{});</script>\n<style></style>\n<p>this should be filtered out</p>\n<script type=\"text/javascript\"></script>"); nodeConfig.should.equal("<script type=\"text/x-red\" data-template-name=\"test-node-1\"></script>\n<script type=\"text/x-red\" data-help-name=\"test-node-1\"></script>\n<script type=\"text/javascript\">RED.nodes.registerType('test-node-1',{});</script>\n<style></style>\n<p>this should be filtered out</p>\n");
done(); done();
}).catch(function(e) { }).catch(function(e) {
done(e); done(e);
}); });
}); });
it('stores the node list', function(done) {
var settings = {
nodesDir:[resourcesDir + "TestNode1",resourcesDir + "TestNode2",resourcesDir + "TestNode3"],
available: function() { return true; },
set: function(s,v) {},
get: function(s) { return null;}
}
var settingsSave = sinon.spy(settings,"set");
typeRegistry.init(settings);
typeRegistry.load("wontexist",true).then(function() {
var list = typeRegistry.getNodeList();
list.should.be.Array.and.have.length(3);
settingsSave.callCount.should.equal(1);
settingsSave.firstCall.args[0].should.be.equal("nodes");
var savedList = settingsSave.firstCall.args[1];
savedList[list[0].id].name == list[0].name;
savedList[list[1].id].name == list[1].name;
savedList[list[2].id].name == list[2].name;
savedList[list[0].id].should.not.have.property("err");
savedList[list[1].id].should.not.have.property("err");
savedList[list[2].id].should.not.have.property("err");
done();
}).catch(function(e) {
done(e);
}).finally(function() {
settingsSave.restore();
});
});
it('allows nodes to be added by filename', function(done) { it('allows nodes to be added by filename', function(done) {
typeRegistry.init({}); var settings = {
available: function() { return true; },
set: function(s,v) {},
get: function(s) { return null;}
}
typeRegistry.init(settings);
typeRegistry.load("wontexist",true).then(function(){ typeRegistry.load("wontexist",true).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty; list.should.be.an.Array.and.be.empty;
typeRegistry.addNode({file: resourcesDir + "TestNode1/TestNode1.js"}).then(function(node) { typeRegistry.addNode(resourcesDir + "TestNode1/TestNode1.js").then(function(node) {
list = typeRegistry.getNodeList(); list = typeRegistry.getNodeList();
list[0].should.have.property("id"); list[0].should.have.property("id");
list[0].should.have.property("name","TestNode1.js"); list[0].should.have.property("name","TestNode1.js");
@ -311,11 +357,11 @@ describe('NodeRegistry', function() {
}); });
it('fails to add non-existent filename', function(done) { it('fails to add non-existent filename', function(done) {
typeRegistry.init({}); typeRegistry.init(settingsWithStorage);
typeRegistry.load("wontexist",true).then(function(){ typeRegistry.load("wontexist",true).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty; list.should.be.an.Array.and.be.empty;
typeRegistry.addNode({file: resourcesDir + "DoesNotExist/DoesNotExist.js"}).then(function(node) { typeRegistry.addNode(resourcesDir + "DoesNotExist/DoesNotExist.js").then(function(node) {
done(new Error("ENOENT not thrown")); done(new Error("ENOENT not thrown"));
}).otherwise(function(e) { }).otherwise(function(e) {
e.code.should.eql("ENOENT"); e.code.should.eql("ENOENT");
@ -328,7 +374,7 @@ describe('NodeRegistry', function() {
}); });
it('returns node info by type or id', function(done) { it('returns node info by type or id', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir + "TestNode1",true).then(function() { typeRegistry.load(resourcesDir + "TestNode1",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -357,7 +403,7 @@ describe('NodeRegistry', function() {
it('rejects adding duplicate nodes', function(done) { it('rejects adding duplicate nodes', function(done) {
typeRegistry.init({}); typeRegistry.init(settingsWithStorage);
typeRegistry.load(resourcesDir + "TestNode1",true).then(function(){ typeRegistry.load(resourcesDir + "TestNode1",true).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -376,18 +422,23 @@ describe('NodeRegistry', function() {
}); });
it('removes nodes from the registry', function(done) { it('removes nodes from the registry', function(done) {
typeRegistry.init({}); typeRegistry.init(settingsWithStorage);
typeRegistry.load(resourcesDir + "TestNode1",true).then(function() { typeRegistry.load(resourcesDir + "TestNode1",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
list[0].should.have.property("id"); list[0].should.have.property("id");
list[0].should.have.property("name","TestNode1.js"); list[0].should.have.property("name","TestNode1.js");
list[0].should.have.property("types",["test-node-1"]); list[0].should.have.property("types",["test-node-1"]);
list[0].should.have.property("enabled",true);
list[0].should.have.property("loaded",true);
typeRegistry.getNodeConfigs().length.should.be.greaterThan(0); typeRegistry.getNodeConfigs().length.should.be.greaterThan(0);
var info = typeRegistry.removeNode(list[0].id); var info = typeRegistry.removeNode(list[0].id);
info.should.eql(list[0]);
info.should.have.property("id",list[0].id);
info.should.have.property("enabled",false);
info.should.have.property("loaded",false);
typeRegistry.getNodeList().should.be.an.Array.and.be.empty; typeRegistry.getNodeList().should.be.an.Array.and.be.empty;
typeRegistry.getNodeConfigs().length.should.equal(0); typeRegistry.getNodeConfigs().length.should.equal(0);
@ -403,7 +454,7 @@ describe('NodeRegistry', function() {
}); });
it('rejects removing unknown nodes from the registry', function(done) { it('rejects removing unknown nodes from the registry', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load("wontexist",true).then(function() { typeRegistry.load("wontexist",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty; list.should.be.an.Array.and.be.empty;
@ -451,7 +502,7 @@ describe('NodeRegistry', function() {
}); });
})(); })();
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load("wontexist",false).then(function(){ typeRegistry.load("wontexist",false).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(2); list.should.be.an.Array.and.have.lengthOf(2);
@ -464,7 +515,7 @@ describe('NodeRegistry', function() {
list[1].should.have.property("id"); list[1].should.have.property("id");
list[1].should.have.property("name","TestNodeModule:TestNodeMod2"); list[1].should.have.property("name","TestNodeModule:TestNodeMod2");
list[1].should.have.property("types",["test-node-mod-2"]); list[1].should.have.property("types",["test-node-mod-2"]);
list[1].should.have.property("enabled",false); list[1].should.have.property("enabled",true);
list[1].should.have.property("err"); list[1].should.have.property("err");
@ -516,13 +567,12 @@ describe('NodeRegistry', function() {
return result; return result;
}); });
})(); })();
typeRegistry.init(settingsWithStorage);
typeRegistry.init({});
typeRegistry.load("wontexist",true).then(function(){ typeRegistry.load("wontexist",true).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty; list.should.be.an.Array.and.be.empty;
typeRegistry.addNode({module: "TestNodeModule"}).then(function(node) { typeRegistry.addModule("TestNodeModule").then(function(node) {
list = typeRegistry.getNodeList(); list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(2); list.should.be.an.Array.and.have.lengthOf(2);
list[0].should.have.property("id"); list[0].should.have.property("id");
@ -534,7 +584,7 @@ describe('NodeRegistry', function() {
list[1].should.have.property("id"); list[1].should.have.property("id");
list[1].should.have.property("name","TestNodeModule:TestNodeMod2"); list[1].should.have.property("name","TestNodeModule:TestNodeMod2");
list[1].should.have.property("types",["test-node-mod-2"]); list[1].should.have.property("types",["test-node-mod-2"]);
list[1].should.have.property("enabled",false); list[1].should.have.property("enabled",true);
list[1].should.have.property("err"); list[1].should.have.property("err");
node.should.eql(list); node.should.eql(list);
@ -552,13 +602,62 @@ describe('NodeRegistry', function() {
}); });
}); });
it('rejects adding duplicate node modules', function(done) {
var fs = require("fs");
var path = require("path");
var pathJoin = (function() {
var _join = path.join;
return sinon.stub(path,"join",function() {
if (arguments.length == 3 && arguments[2] == "package.json") {
return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1],arguments[2]);
}
if (arguments.length == 2 && arguments[1] == "TestNodeModule") {
return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1]);
}
return _join.apply(this,arguments);
});
})();
var readdirSync = (function() {
var originalReaddirSync = fs.readdirSync;
var callCount = 0;
return sinon.stub(fs,"readdirSync",function(dir) {
var result = [];
if (callCount == 1) {
result = originalReaddirSync(resourcesDir + "TestNodeModule" + path.sep + "node_modules");
}
callCount++;
return result;
});
})();
typeRegistry.init(settingsWithStorage);
typeRegistry.load('wontexist',false).then(function(){
var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(2);
typeRegistry.addModule("TestNodeModule").then(function(node) {
done(new Error("addModule resolved"));
}).otherwise(function(err) {
done();
});
}).catch(function(e) {
done(e);
}).finally(function() {
readdirSync.restore();
pathJoin.restore();
});
});
it('fails to add non-existent module name', function(done) { it('fails to add non-existent module name', function(done) {
typeRegistry.init({}); typeRegistry.init(settingsWithStorage);
typeRegistry.load("wontexist",true).then(function(){ typeRegistry.load("wontexist",true).then(function(){
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty; list.should.be.an.Array.and.be.empty;
typeRegistry.addNode({module: "DoesNotExistModule"}).then(function(node) { typeRegistry.addModule("DoesNotExistModule").then(function(node) {
done(new Error("ENOENT not thrown")); done(new Error("ENOENT not thrown"));
}).otherwise(function(e) { }).otherwise(function(e) {
e.code.should.eql("MODULE_NOT_FOUND"); e.code.should.eql("MODULE_NOT_FOUND");
@ -570,9 +669,80 @@ describe('NodeRegistry', function() {
}); });
}); });
it('removes nodes from the registry by module', function(done) {
var fs = require("fs");
var path = require("path");
var pathJoin = (function() {
var _join = path.join;
return sinon.stub(path,"join",function() {
if (arguments.length == 3 && arguments[2] == "package.json") {
return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1],arguments[2]);
}
if (arguments.length == 2 && arguments[1] == "TestNodeModule") {
return _join(resourcesDir,"TestNodeModule" + path.sep + "node_modules" + path.sep,arguments[1]);
}
return _join.apply(this,arguments);
});
})();
var readdirSync = (function() {
var originalReaddirSync = fs.readdirSync;
var callCount = 0;
return sinon.stub(fs,"readdirSync",function(dir) {
var result = [];
if (callCount == 1) {
result = originalReaddirSync(resourcesDir + "TestNodeModule" + path.sep + "node_modules");
}
callCount++;
return result;
});
})();
typeRegistry.init(settingsWithStorage);
typeRegistry.load('wontexist',false).then(function(){
var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(2);
var res = typeRegistry.removeModule("TestNodeModule");
res.should.be.an.Array.and.have.lengthOf(2);
res[0].should.have.a.property("id",list[0].id);
res[1].should.have.a.property("id",list[1].id);
list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty;
done();
}).catch(function(e) {
done(e);
}).finally(function() {
readdirSync.restore();
pathJoin.restore();
});
});
it('fails to remove non-existent module name', function(done) {
typeRegistry.init(settings);
typeRegistry.load("wontexist",true).then(function(){
var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.be.empty;
/*jshint immed: false */
(function() {
typeRegistry.removeModule("DoesNotExistModule");
}).should.throw();
done();
}).catch(function(e) {
done(e);
});
});
it('allows nodes to be enabled and disabled', function(done) { it('allows nodes to be enabled and disabled', function(done) {
typeRegistry.init({}); typeRegistry.init(settingsWithStorage);
typeRegistry.load(resourcesDir+path.sep+"TestNode1",true).then(function() { typeRegistry.load(resourcesDir+path.sep+"TestNode1",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.have.lengthOf(1);
@ -583,7 +753,9 @@ describe('NodeRegistry', function() {
var nodeConfig = typeRegistry.getNodeConfigs(); var nodeConfig = typeRegistry.getNodeConfigs();
nodeConfig.length.should.be.greaterThan(0); nodeConfig.length.should.be.greaterThan(0);
typeRegistry.disableNode(list[0].id); var info = typeRegistry.disableNode(list[0].id);
info.should.have.property("id",list[0].id);
info.should.have.property("enabled",false);
var list2 = typeRegistry.getNodeList(); var list2 = typeRegistry.getNodeList();
list2.should.be.an.Array.and.have.lengthOf(1); list2.should.be.an.Array.and.have.lengthOf(1);
@ -591,7 +763,9 @@ describe('NodeRegistry', function() {
typeRegistry.getNodeConfigs().length.should.equal(0); typeRegistry.getNodeConfigs().length.should.equal(0);
typeRegistry.enableNode(list[0].id); var info2 = typeRegistry.enableNode(list[0].id);
info2.should.have.property("id",list[0].id);
info2.should.have.property("enabled",true);
var list3 = typeRegistry.getNodeList(); var list3 = typeRegistry.getNodeList();
list3.should.be.an.Array.and.have.lengthOf(1); list3.should.be.an.Array.and.have.lengthOf(1);
@ -606,27 +780,25 @@ describe('NodeRegistry', function() {
}); });
}); });
it('does not allow a node with error to be enabled', function(done) { it('fails to enable/disable non-existent nodes', function(done) {
typeRegistry.init({}); typeRegistry.init(settings);
typeRegistry.load(resourcesDir+path.sep+"TestNode3",true).then(function() { typeRegistry.load("wontexist",true).then(function() {
var list = typeRegistry.getNodeList(); var list = typeRegistry.getNodeList();
list.should.be.an.Array.and.have.lengthOf(1); list.should.be.an.Array.and.be.empty;
list[0].should.have.property("id");
list[0].should.have.property("name","TestNode3.js");
list[0].should.have.property("enabled",false);
list[0].should.have.property("err");
/*jshint immed: false */ /*jshint immed: false */
(function() { (function() {
typeRegistry.enable(list[0].id); typeRegistry.disableNode("123");
}).should.throw(); }).should.throw();
/*jshint immed: false */
(function() {
typeRegistry.enableNode("123");
}).should.throw();
done(); done();
}).catch(function(e) { }).catch(function(e) {
done(e); done(e);
}); });
}); });
}); });

109
test/red/settings_spec.js Normal file
View File

@ -0,0 +1,109 @@
/**
* 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 when = require("when");
var settings = require("../../red/settings");
describe("red/settings", function() {
it('wraps the user settings as read-only properties', function() {
var userSettings = {
a: 123,
b: "test",
c: [1,2,3]
}
settings.init(userSettings);
settings.available().should.be.false;
settings.a.should.equal(123);
settings.b.should.equal("test");
settings.c.should.be.an.Array.with.lengthOf(3);
settings.get("a").should.equal(123);
settings.get("b").should.equal("test");
settings.get("c").should.be.an.Array.with.lengthOf(3);
/*jshint immed: false */
(function() {
settings.a = 456;
}).should.throw();
settings.c.push(5);
settings.c.should.be.an.Array.with.lengthOf(4);
/*jshint immed: false */
(function() {
settings.set("a",456);
}).should.throw();
/*jshint immed: false */
(function() {
settings.set("a",456);
}).should.throw();
/*jshint immed: false */
(function() {
settings.get("unknown");
}).should.throw();
/*jshint immed: false */
(function() {
settings.set("unknown",456);
}).should.throw();
});
it('loads global settings from storage', function(done) {
var userSettings = {
a: 123,
b: "test",
c: [1,2,3]
}
var savedSettings = null;
var storage = {
getSettings: function() {
return when.resolve({globalA:789});
},
saveSettings: function(settings) {
savedSettings = settings;
return when.resolve();
}
}
settings.init(userSettings);
settings.available().should.be.false;
/*jshint immed: false */
(function() {
settings.get("unknown");
}).should.throw();
settings.load(storage).then(function() {
settings.available().should.be.true;
settings.get("globalA").should.equal(789);
settings.set("globalA","abc").then(function() {
savedSettings.globalA.should.equal("abc");
done();
});
}).otherwise(function(err) {
done(err);
});
});
});