Merge pull request #2690 from node-red/sf-module

[sf-modules] Support npm subflow modules
This commit is contained in:
Nick O'Leary
2021-01-08 21:16:05 +00:00
committed by GitHub
34 changed files with 1289 additions and 402 deletions

View File

@@ -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");

View File

@@ -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",{

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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,