mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Merge pull request #2690 from node-red/sf-module
[sf-modules] Support npm subflow modules
This commit is contained in:
@@ -194,7 +194,7 @@ var api = module.exports = {
|
||||
}
|
||||
if (opts.module) {
|
||||
var existingModule = runtime.nodes.getModuleInfo(opts.module);
|
||||
if (existingModule) {
|
||||
if (existingModule && existingModule.user) {
|
||||
if (!opts.version || existingModule.version === opts.version) {
|
||||
runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"}, opts.req);
|
||||
var err = new Error("Module already loaded");
|
||||
|
@@ -93,7 +93,7 @@ class Flow {
|
||||
* @param {[type]} msg [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
log(msg) {
|
||||
info(msg) {
|
||||
Log.log({
|
||||
id: this.id||"global",
|
||||
level: Log.INFO,
|
||||
@@ -116,6 +116,17 @@ class Flow {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* [log description]
|
||||
* @param {[type]} msg [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
log(msg) {
|
||||
if (!msg.path) {
|
||||
msg.path = this.path;
|
||||
}
|
||||
this.parent.log(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start this flow.
|
||||
@@ -303,21 +314,13 @@ class Flow {
|
||||
if (node) {
|
||||
delete this.activeNodes[stopList[i]];
|
||||
if (this.subflowInstanceNodes[stopList[i]]) {
|
||||
try {
|
||||
(function(subflow) {
|
||||
promises.push(stopNode(node,false).then(() => subflow.stop()));
|
||||
})(this.subflowInstanceNodes[stopList[i]]);
|
||||
} catch(err) {
|
||||
node.error(err);
|
||||
}
|
||||
delete this.subflowInstanceNodes[stopList[i]];
|
||||
} else {
|
||||
try {
|
||||
var removed = removedMap[stopList[i]];
|
||||
promises.push(stopNode(node,removed).catch(()=>{}));
|
||||
} catch(err) {
|
||||
node.error(err);
|
||||
}
|
||||
}
|
||||
try {
|
||||
var removed = removedMap[stopList[i]];
|
||||
promises.push(stopNode(node,removed).catch(()=>{}));
|
||||
} catch(err) {
|
||||
node.error(err);
|
||||
}
|
||||
if (removedMap[stopList[i]]) {
|
||||
events.emit("node-status",{
|
||||
|
@@ -208,7 +208,12 @@ class Subflow extends Flow {
|
||||
|
||||
this.node = new Node(subflowInstanceConfig);
|
||||
this.node.on("input", function(msg) { this.send(msg);});
|
||||
this.node.on("close", function() { this.status({}); })
|
||||
// Called when the subflow instance node is being stopped
|
||||
this.node.on("close", function(done) {
|
||||
this.status({});
|
||||
// Stop the complete subflow
|
||||
self.stop().finally(done)
|
||||
})
|
||||
this.node.status = status => this.parent.handleStatus(this.node,status);
|
||||
// Create a context instance
|
||||
// console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z)
|
||||
@@ -499,11 +504,49 @@ function remapSubflowNodes(nodes,nodeMap) {
|
||||
}
|
||||
}
|
||||
|
||||
class SubflowModule extends Subflow {
|
||||
/**
|
||||
* Create a Subflow Module object.
|
||||
* This is a node that has been published as a subflow.
|
||||
* @param {[type]} parent [description]
|
||||
* @param {[type]} globalFlow [description]
|
||||
* @param {[type]} subflowDef [description]
|
||||
* @param {[type]} subflowInstance [description]
|
||||
*/
|
||||
constructor(type, parent,globalFlow,subflowDef,subflowInstance) {
|
||||
super(parent,globalFlow,subflowDef,subflowInstance);
|
||||
this.TYPE = `module:${type}`;
|
||||
this.subflowType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* [log description]
|
||||
* @param {[type]} msg [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
log(msg) {
|
||||
if (msg.id) {
|
||||
msg.id = this.id
|
||||
}
|
||||
if (msg.type) {
|
||||
msg.type = this.subflowType
|
||||
}
|
||||
super.log(msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
|
||||
return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
|
||||
}
|
||||
|
||||
|
||||
function createModuleInstance(type, parent,globalFlow,subflowDef,subflowInstance) {
|
||||
return new SubflowModule(type, parent,globalFlow,subflowDef,subflowInstance);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function(runtime) {},
|
||||
create: createSubflow
|
||||
create: createSubflow,
|
||||
createModuleInstance: createModuleInstance
|
||||
}
|
||||
|
@@ -698,7 +698,8 @@ const flowAPI = {
|
||||
getNode: getNode,
|
||||
handleError: () => false,
|
||||
handleStatus: () => false,
|
||||
getSetting: k => flowUtil.getEnvVar(k)
|
||||
getSetting: k => flowUtil.getEnvVar(k),
|
||||
log: m => log.log(m)
|
||||
}
|
||||
|
||||
|
||||
|
@@ -19,6 +19,7 @@ var Log = require("@node-red/util").log;
|
||||
var subflowInstanceRE = /^subflow:(.+)$/;
|
||||
var typeRegistry = require("@node-red/registry");
|
||||
|
||||
|
||||
var envVarExcludes = {};
|
||||
|
||||
function diffNodes(oldNode,newNode) {
|
||||
@@ -66,6 +67,195 @@ function mapEnvVarProperties(obj,prop,flow) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createNode(flow,config) {
|
||||
var newNode = null;
|
||||
var type = config.type;
|
||||
try {
|
||||
var nodeTypeConstructor = typeRegistry.get(type);
|
||||
if (typeof nodeTypeConstructor === "function") {
|
||||
var conf = clone(config);
|
||||
delete conf.credentials;
|
||||
for (var p in conf) {
|
||||
if (conf.hasOwnProperty(p)) {
|
||||
mapEnvVarProperties(conf,p,flow);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
|
||||
newNode = new nodeTypeConstructor(conf);
|
||||
} catch (err) {
|
||||
Log.log({
|
||||
level: Log.ERROR,
|
||||
id:conf.id,
|
||||
type: type,
|
||||
msg: err
|
||||
});
|
||||
}
|
||||
} else if (nodeTypeConstructor) {
|
||||
// console.log(nodeTypeConstructor)
|
||||
var subflowConfig = parseConfig([nodeTypeConstructor.subflow].concat(nodeTypeConstructor.subflow.flow));
|
||||
var instanceConfig = clone(config);
|
||||
instanceConfig.env = clone(nodeTypeConstructor.subflow.env);
|
||||
|
||||
instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => {
|
||||
var nodePropType;
|
||||
var nodePropValue = config[nodeProp.name];
|
||||
if (nodeProp.type === "cred") {
|
||||
nodePropType = "cred";
|
||||
} else {
|
||||
switch(typeof config[nodeProp.name]) {
|
||||
case "string": nodePropType = "str"; break;
|
||||
case "number": nodePropType = "num"; break;
|
||||
case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
|
||||
default:
|
||||
nodePropType = config[nodeProp.name].type;
|
||||
nodePropValue = config[nodeProp.name].value;
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: nodeProp.name,
|
||||
type: nodePropType,
|
||||
value: nodePropValue
|
||||
}
|
||||
})
|
||||
|
||||
var subflow = require("./Subflow").createModuleInstance(
|
||||
nodeTypeConstructor.type,
|
||||
flow,
|
||||
flow.global,
|
||||
subflowConfig.subflows[nodeTypeConstructor.subflow.id],
|
||||
instanceConfig
|
||||
);
|
||||
subflow.start();
|
||||
return subflow.node;
|
||||
|
||||
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
|
||||
}
|
||||
} catch(err) {
|
||||
Log.error(err);
|
||||
}
|
||||
return newNode;
|
||||
}
|
||||
|
||||
function parseConfig(config) {
|
||||
var flow = {};
|
||||
flow.allNodes = {};
|
||||
flow.subflows = {};
|
||||
flow.configs = {};
|
||||
flow.flows = {};
|
||||
flow.groups = {};
|
||||
flow.missingTypes = [];
|
||||
|
||||
config.forEach(function(n) {
|
||||
flow.allNodes[n.id] = clone(n);
|
||||
if (n.type === 'tab') {
|
||||
flow.flows[n.id] = n;
|
||||
flow.flows[n.id].subflows = {};
|
||||
flow.flows[n.id].configs = {};
|
||||
flow.flows[n.id].nodes = {};
|
||||
}
|
||||
if (n.type === 'group') {
|
||||
flow.groups[n.id] = n;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: why a separate forEach? this can be merged with above
|
||||
config.forEach(function(n) {
|
||||
if (n.type === 'subflow') {
|
||||
flow.subflows[n.id] = n;
|
||||
flow.subflows[n.id].configs = {};
|
||||
flow.subflows[n.id].nodes = {};
|
||||
flow.subflows[n.id].instances = [];
|
||||
}
|
||||
});
|
||||
var linkWires = {};
|
||||
var linkOutNodes = [];
|
||||
config.forEach(function(n) {
|
||||
if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') {
|
||||
var subflowDetails = subflowInstanceRE.exec(n.type);
|
||||
|
||||
if ( (subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type)) ) {
|
||||
if (flow.missingTypes.indexOf(n.type) === -1) {
|
||||
flow.missingTypes.push(n.type);
|
||||
}
|
||||
}
|
||||
var container = null;
|
||||
if (flow.flows[n.z]) {
|
||||
container = flow.flows[n.z];
|
||||
} else if (flow.subflows[n.z]) {
|
||||
container = flow.subflows[n.z];
|
||||
}
|
||||
if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) {
|
||||
if (subflowDetails) {
|
||||
var subflowType = subflowDetails[1]
|
||||
n.subflow = subflowType;
|
||||
flow.subflows[subflowType].instances.push(n)
|
||||
}
|
||||
if (container) {
|
||||
container.nodes[n.id] = n;
|
||||
}
|
||||
} else {
|
||||
if (container) {
|
||||
container.configs[n.id] = n;
|
||||
} else {
|
||||
flow.configs[n.id] = n;
|
||||
flow.configs[n.id]._users = [];
|
||||
}
|
||||
}
|
||||
if (n.type === 'link in' && n.links) {
|
||||
// Ensure wires are present in corresponding link out nodes
|
||||
n.links.forEach(function(id) {
|
||||
linkWires[id] = linkWires[id]||{};
|
||||
linkWires[id][n.id] = true;
|
||||
})
|
||||
} else if (n.type === 'link out' && n.links) {
|
||||
linkWires[n.id] = linkWires[n.id]||{};
|
||||
n.links.forEach(function(id) {
|
||||
linkWires[n.id][id] = true;
|
||||
})
|
||||
linkOutNodes.push(n);
|
||||
}
|
||||
}
|
||||
});
|
||||
linkOutNodes.forEach(function(n) {
|
||||
var links = linkWires[n.id];
|
||||
var targets = Object.keys(links);
|
||||
n.wires = [targets];
|
||||
});
|
||||
|
||||
|
||||
var addedTabs = {};
|
||||
config.forEach(function(n) {
|
||||
if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') {
|
||||
for (var prop in n) {
|
||||
if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) {
|
||||
// This property references a global config node
|
||||
flow.configs[n[prop]]._users.push(n.id)
|
||||
}
|
||||
}
|
||||
if (n.z && !flow.subflows[n.z]) {
|
||||
|
||||
if (!flow.flows[n.z]) {
|
||||
flow.flows[n.z] = {type:'tab',id:n.z};
|
||||
flow.flows[n.z].subflows = {};
|
||||
flow.flows[n.z].configs = {};
|
||||
flow.flows[n.z].nodes = {};
|
||||
addedTabs[n.z] = flow.flows[n.z];
|
||||
}
|
||||
if (addedTabs[n.z]) {
|
||||
if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) {
|
||||
addedTabs[n.z].nodes[n.id] = n;
|
||||
} else {
|
||||
addedTabs[n.z].configs[n.id] = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return flow;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: function(runtime) {
|
||||
envVarExcludes = {};
|
||||
@@ -79,123 +269,7 @@ module.exports = {
|
||||
diffNodes: diffNodes,
|
||||
mapEnvVarProperties: mapEnvVarProperties,
|
||||
|
||||
parseConfig: function(config) {
|
||||
var flow = {};
|
||||
flow.allNodes = {};
|
||||
flow.subflows = {};
|
||||
flow.configs = {};
|
||||
flow.flows = {};
|
||||
flow.groups = {};
|
||||
flow.missingTypes = [];
|
||||
|
||||
config.forEach(function(n) {
|
||||
flow.allNodes[n.id] = clone(n);
|
||||
if (n.type === 'tab') {
|
||||
flow.flows[n.id] = n;
|
||||
flow.flows[n.id].subflows = {};
|
||||
flow.flows[n.id].configs = {};
|
||||
flow.flows[n.id].nodes = {};
|
||||
}
|
||||
if (n.type === 'group') {
|
||||
flow.groups[n.id] = n;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: why a separate forEach? this can be merged with above
|
||||
config.forEach(function(n) {
|
||||
if (n.type === 'subflow') {
|
||||
flow.subflows[n.id] = n;
|
||||
flow.subflows[n.id].configs = {};
|
||||
flow.subflows[n.id].nodes = {};
|
||||
flow.subflows[n.id].instances = [];
|
||||
}
|
||||
});
|
||||
var linkWires = {};
|
||||
var linkOutNodes = [];
|
||||
config.forEach(function(n) {
|
||||
if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') {
|
||||
var subflowDetails = subflowInstanceRE.exec(n.type);
|
||||
|
||||
if ( (subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type)) ) {
|
||||
if (flow.missingTypes.indexOf(n.type) === -1) {
|
||||
flow.missingTypes.push(n.type);
|
||||
}
|
||||
}
|
||||
var container = null;
|
||||
if (flow.flows[n.z]) {
|
||||
container = flow.flows[n.z];
|
||||
} else if (flow.subflows[n.z]) {
|
||||
container = flow.subflows[n.z];
|
||||
}
|
||||
if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) {
|
||||
if (subflowDetails) {
|
||||
var subflowType = subflowDetails[1]
|
||||
n.subflow = subflowType;
|
||||
flow.subflows[subflowType].instances.push(n)
|
||||
}
|
||||
if (container) {
|
||||
container.nodes[n.id] = n;
|
||||
}
|
||||
} else {
|
||||
if (container) {
|
||||
container.configs[n.id] = n;
|
||||
} else {
|
||||
flow.configs[n.id] = n;
|
||||
flow.configs[n.id]._users = [];
|
||||
}
|
||||
}
|
||||
if (n.type === 'link in' && n.links) {
|
||||
// Ensure wires are present in corresponding link out nodes
|
||||
n.links.forEach(function(id) {
|
||||
linkWires[id] = linkWires[id]||{};
|
||||
linkWires[id][n.id] = true;
|
||||
})
|
||||
} else if (n.type === 'link out' && n.links) {
|
||||
linkWires[n.id] = linkWires[n.id]||{};
|
||||
n.links.forEach(function(id) {
|
||||
linkWires[n.id][id] = true;
|
||||
})
|
||||
linkOutNodes.push(n);
|
||||
}
|
||||
}
|
||||
});
|
||||
linkOutNodes.forEach(function(n) {
|
||||
var links = linkWires[n.id];
|
||||
var targets = Object.keys(links);
|
||||
n.wires = [targets];
|
||||
});
|
||||
|
||||
|
||||
var addedTabs = {};
|
||||
config.forEach(function(n) {
|
||||
if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') {
|
||||
for (var prop in n) {
|
||||
if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) {
|
||||
// This property references a global config node
|
||||
flow.configs[n[prop]]._users.push(n.id)
|
||||
}
|
||||
}
|
||||
if (n.z && !flow.subflows[n.z]) {
|
||||
|
||||
if (!flow.flows[n.z]) {
|
||||
flow.flows[n.z] = {type:'tab',id:n.z};
|
||||
flow.flows[n.z].subflows = {};
|
||||
flow.flows[n.z].configs = {};
|
||||
flow.flows[n.z].nodes = {};
|
||||
addedTabs[n.z] = flow.flows[n.z];
|
||||
}
|
||||
if (addedTabs[n.z]) {
|
||||
if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) {
|
||||
addedTabs[n.z].nodes[n.id] = n;
|
||||
} else {
|
||||
addedTabs[n.z].configs[n.id] = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return flow;
|
||||
},
|
||||
parseConfig: parseConfig,
|
||||
|
||||
diffConfigs: function(oldConfig, newConfig) {
|
||||
var id;
|
||||
@@ -475,36 +549,5 @@ module.exports = {
|
||||
* @param {object} config The node configuration object
|
||||
* @return {Node} The instance of the node
|
||||
*/
|
||||
createNode: function(flow,config) {
|
||||
var newNode = null;
|
||||
var type = config.type;
|
||||
try {
|
||||
var nodeTypeConstructor = typeRegistry.get(type);
|
||||
if (nodeTypeConstructor) {
|
||||
var conf = clone(config);
|
||||
delete conf.credentials;
|
||||
for (var p in conf) {
|
||||
if (conf.hasOwnProperty(p)) {
|
||||
mapEnvVarProperties(conf,p,flow);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
|
||||
newNode = new nodeTypeConstructor(conf);
|
||||
} catch (err) {
|
||||
Log.log({
|
||||
level: Log.ERROR,
|
||||
id:conf.id,
|
||||
type: type,
|
||||
msg: err
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
|
||||
}
|
||||
} catch(err) {
|
||||
Log.error(err);
|
||||
}
|
||||
return newNode;
|
||||
}
|
||||
createNode: createNode
|
||||
}
|
||||
|
@@ -486,16 +486,14 @@ function log_helper(self, level, msg) {
|
||||
if (self._alias) {
|
||||
o._alias = self._alias;
|
||||
}
|
||||
if (self._flow) {
|
||||
o.path = self._flow.path;
|
||||
}
|
||||
|
||||
if (self.z) {
|
||||
o.z = self.z;
|
||||
}
|
||||
if (self.name) {
|
||||
o.name = self.name;
|
||||
}
|
||||
Log.log(o);
|
||||
self._flow.log(o);
|
||||
}
|
||||
/**
|
||||
* Log an INFO level message
|
||||
|
@@ -112,6 +112,25 @@ function createNode(node,def) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerSubflow(nodeSet, subflow) {
|
||||
// TODO: extract credentials definition from subflow properties
|
||||
var registeredType = registry.registerSubflow(nodeSet,subflow);
|
||||
|
||||
if (subflow.env) {
|
||||
var creds = {};
|
||||
var hasCreds = false;
|
||||
subflow.env.forEach(e => {
|
||||
if (e.type === "cred") {
|
||||
creds[e.name] = {type: "password"};
|
||||
hasCreds = true;
|
||||
}
|
||||
})
|
||||
if (hasCreds) {
|
||||
credentials.register(registeredType.type,creds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init(runtime) {
|
||||
settings = runtime.settings;
|
||||
log = runtime.log;
|
||||
@@ -162,11 +181,12 @@ function installModule(module,version,url) {
|
||||
|
||||
function uninstallModule(module) {
|
||||
var info = registry.getModuleInfo(module);
|
||||
if (!info) {
|
||||
if (!info || !info.user) {
|
||||
throw new Error(log._("nodes.index.unrecognised-module", {module:module}));
|
||||
} else {
|
||||
for (var i=0;i<info.nodes.length;i++) {
|
||||
flows.checkTypeInUse(module+"/"+info.nodes[i].name);
|
||||
var nodeTypesToCheck = info.nodes.map(n => `${module}/${n.name}`);
|
||||
for (var i=0;i<nodeTypesToCheck.length;i++) {
|
||||
flows.checkTypeInUse(nodeTypesToCheck[i]);
|
||||
}
|
||||
return registry.uninstallModule(module).then(function(list) {
|
||||
events.emit("runtime-event",{id:"node/removed",retain:false,payload:list});
|
||||
@@ -196,6 +216,7 @@ module.exports = {
|
||||
|
||||
// Node type registry
|
||||
registerType: registerType,
|
||||
registerSubflow: registerSubflow,
|
||||
getType: registry.get,
|
||||
|
||||
getNodeInfo: registry.getNodeInfo,
|
||||
|
Reference in New Issue
Block a user