node-red
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var util = require("util");
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var redUtil = require("@node-red/util").util;
|
||||
var Log = require("@node-red/util").log;
|
||||
var context = require("./context");
|
||||
var flows = require("../flows");
|
||||
const hooks = require("@node-red/util").hooks;
|
||||
|
||||
|
||||
const NOOP_SEND = function() {}
|
||||
|
||||
/**
|
||||
* The Node object is the heart of a Node-RED flow. It is the object that all
|
||||
* nodes extend.
|
||||
*
|
||||
* The Node object itself inherits from EventEmitter, although it provides
|
||||
* custom implementations of some of the EE functions in order to handle
|
||||
* `input` and `close` events properly.
|
||||
*/
|
||||
function Node(n) {
|
||||
this.id = n.id;
|
||||
this.type = n.type;
|
||||
this.z = n.z;
|
||||
this.g = n.g;
|
||||
this._closeCallbacks = [];
|
||||
this._inputCallback = null;
|
||||
this._inputCallbacks = null;
|
||||
this._expectedDoneCount = 0;
|
||||
|
||||
if (n.name) {
|
||||
this.name = n.name;
|
||||
}
|
||||
if (n._alias) {
|
||||
this._alias = n._alias;
|
||||
}
|
||||
if (n._flow) {
|
||||
// Make this a non-enumerable property as it may cause
|
||||
// circular references. Any existing code that tries to JSON serialise
|
||||
// the object (such as dashboard) will not like circular refs
|
||||
// The value must still be writable in the case that a node does:
|
||||
// Object.assign(this,config)
|
||||
// as part of its constructor - config._flow will overwrite this._flow
|
||||
// which we can tolerate as they are the same object.
|
||||
Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true })
|
||||
}
|
||||
if (n._module) {
|
||||
Object.defineProperty(this,'_module', {value: n._module, enumerable: false, writable: true })
|
||||
}
|
||||
if (n._path) {
|
||||
Object.defineProperty(this,'_path', {value: n._path, enumerable: false, writable: true })
|
||||
}
|
||||
this.updateWires(n.wires);
|
||||
}
|
||||
|
||||
util.inherits(Node, EventEmitter);
|
||||
|
||||
/**
|
||||
* Update the wiring configuration for this node.
|
||||
*
|
||||
* We try to optimise the message handling path. To do this there are three
|
||||
* cases to consider:
|
||||
* 1. this node is wired to nothing. In this case we replace node.send with a
|
||||
* NO-OP function.
|
||||
* 2. this node is wired to one other node. In this case we set `this._wire`
|
||||
* as a reference to the node it is wired to. This means we avoid unnecessary
|
||||
* iterations over what would otherwise be a 1-element array.
|
||||
* 3. this node is wired to multiple things. The normal node.send processing of
|
||||
* this.wires applies.
|
||||
*
|
||||
* @param {array} wires the new wiring configuration
|
||||
*/
|
||||
Node.prototype.updateWires = function(wires) {
|
||||
//console.log("UPDATE",this.id);
|
||||
this.wires = wires || [];
|
||||
delete this._wire;
|
||||
|
||||
var wc = 0;
|
||||
this.wires.forEach(function(w) {
|
||||
wc+=w.length;
|
||||
});
|
||||
this._wireCount = wc;
|
||||
if (wc === 0) {
|
||||
// With nothing wired to the node, no-op send
|
||||
this.send = NOOP_SEND
|
||||
} else {
|
||||
this.send = Node.prototype.send;
|
||||
if (this.wires.length === 1 && this.wires[0].length === 1) {
|
||||
// Single wire, so we can shortcut the send when
|
||||
// a single message is sent
|
||||
this._wire = this.wires[0][0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Get the context object for this node.
|
||||
*
|
||||
* As most nodes do not use context, this is a lazy function that will only
|
||||
* create a context instance for the node if it is needed.
|
||||
* @return {object} the context object
|
||||
*/
|
||||
Node.prototype.context = function() {
|
||||
if (!this._context) {
|
||||
this._context = context.get(this._alias||this.id,this.z);
|
||||
}
|
||||
return this._context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the complete event for a message
|
||||
*
|
||||
* @param {object} msg The message that has completed
|
||||
* @param {error} error (optional) an error hit whilst handling the message
|
||||
*/
|
||||
Node.prototype._complete = function(msg,error) {
|
||||
this.metric("done",msg);
|
||||
hooks.trigger("onComplete",{ msg: msg, error: error, node: { id: this.id, node: this }}, err => {
|
||||
if (err) {
|
||||
this.error(err);
|
||||
}
|
||||
})
|
||||
if (error) {
|
||||
// For now, delegate this to this.error
|
||||
// But at some point, the timeout handling will need to know about
|
||||
// this as well.
|
||||
this.error(error,msg);
|
||||
} else {
|
||||
this._flow.handleComplete(this,msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal reference to the original EventEmitter.on() function
|
||||
*/
|
||||
Node.prototype._on = Node.prototype.on;
|
||||
|
||||
/**
|
||||
* Register a callback function for a named event.
|
||||
* 'close' and 'input' events are handled locally, other events defer to EventEmitter.on()
|
||||
*/
|
||||
Node.prototype.on = function(event, callback) {
|
||||
var node = this;
|
||||
if (event == "close") {
|
||||
this._closeCallbacks.push(callback);
|
||||
} else if (event === "input") {
|
||||
if (callback.length === 3) {
|
||||
this._expectedDoneCount++
|
||||
}
|
||||
if (this._inputCallback) {
|
||||
this._inputCallbacks = [this._inputCallback, callback];
|
||||
this._inputCallback = null;
|
||||
} else if (this._inputCallbacks) {
|
||||
this._inputCallbacks.push(callback);
|
||||
} else {
|
||||
this._inputCallback = callback;
|
||||
}
|
||||
} else {
|
||||
this._on(event, callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An internal reference to the original EventEmitter.emit() function
|
||||
*/
|
||||
Node.prototype._emit = Node.prototype.emit;
|
||||
|
||||
/**
|
||||
* Emit an event to all registered listeners.
|
||||
*/
|
||||
Node.prototype.emit = function(event, ...args) {
|
||||
var node = this;
|
||||
if (event === "input") {
|
||||
this._emitInput.apply(this,args);
|
||||
} else {
|
||||
this._emit.apply(this,arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the 'input' event.
|
||||
*
|
||||
* This will call all registered handlers for the 'input' event.
|
||||
*/
|
||||
Node.prototype._emitInput = function(arg) {
|
||||
var node = this;
|
||||
this.metric("receive", arg);
|
||||
let receiveEvent = { msg:arg, destination: { id: this.id, node: this } }
|
||||
// onReceive - a node is about to receive a message
|
||||
hooks.trigger("onReceive",receiveEvent,(err) => {
|
||||
if (err) {
|
||||
node.error(err);
|
||||
return
|
||||
} else if (err !== false) {
|
||||
if (node._inputCallback) {
|
||||
// Just one callback registered.
|
||||
try {
|
||||
node._inputCallback(
|
||||
arg,
|
||||
function() { node.send.apply(node,arguments) },
|
||||
function(err) { node._complete(arg,err); }
|
||||
);
|
||||
} catch(err) {
|
||||
node.error(err,arg);
|
||||
}
|
||||
} else if (node._inputCallbacks) {
|
||||
// Multiple callbacks registered. Call each one, tracking eventual completion
|
||||
var c = node._inputCallbacks.length;
|
||||
let doneCount = 0
|
||||
for (var i=0;i<c;i++) {
|
||||
var cb = node._inputCallbacks[i];
|
||||
try {
|
||||
cb.call(
|
||||
node,
|
||||
arg,
|
||||
function() { node.send.apply(node,arguments) },
|
||||
function(err) {
|
||||
doneCount++;
|
||||
if (doneCount === node._expectedDoneCount) {
|
||||
node._complete(arg,err);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch(err) {
|
||||
node.error(err,arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// postReceive - the message has been passed to the node's input handler
|
||||
hooks.trigger("postReceive",receiveEvent,(err) => {if (err) { node.error(err) }});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal reference to the original EventEmitter.removeListener() function
|
||||
*/
|
||||
Node.prototype._removeListener = Node.prototype.removeListener;
|
||||
|
||||
/**
|
||||
* Remove a listener for an event
|
||||
*/
|
||||
Node.prototype.removeListener = function(name, listener) {
|
||||
var index;
|
||||
if (name === "input") {
|
||||
if (listener.length === 3) {
|
||||
this._expectedDoneCount--
|
||||
}
|
||||
if (this._inputCallback && this._inputCallback === listener) {
|
||||
// Removing the only callback
|
||||
this._inputCallback = null;
|
||||
} else if (this._inputCallbacks) {
|
||||
// Removing one of many callbacks
|
||||
index = this._inputCallbacks.indexOf(listener);
|
||||
if (index > -1) {
|
||||
this._inputCallbacks.splice(index,1);
|
||||
}
|
||||
// Check if we can optimise back to a single callback
|
||||
if (this._inputCallbacks.length === 1) {
|
||||
this._inputCallback = this._inputCallbacks[0];
|
||||
this._inputCallbacks = null;
|
||||
}
|
||||
}
|
||||
} else if (name === "close") {
|
||||
index = this._closeCallbacks.indexOf(listener);
|
||||
if (index > -1) {
|
||||
this._closeCallbacks.splice(index,1);
|
||||
}
|
||||
} else {
|
||||
this._removeListener(name, listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal reference to the original EventEmitter.removeAllListeners() function
|
||||
*/
|
||||
Node.prototype._removeAllListeners = Node.prototype.removeAllListeners;
|
||||
|
||||
/**
|
||||
* Remove all listeners for an event
|
||||
*/
|
||||
Node.prototype.removeAllListeners = function(name) {
|
||||
if (name === "input") {
|
||||
this._inputCallback = null;
|
||||
this._inputCallbacks = null;
|
||||
} else if (name === "close") {
|
||||
this._closeCallbacks = [];
|
||||
} else {
|
||||
this._removeAllListeners(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the node is being stopped
|
||||
* @param {boolean} removed Whether the node has been removed, or just being stopped
|
||||
* @return {Promise} resolves when the node has closed
|
||||
*/
|
||||
Node.prototype.close = function(removed) {
|
||||
//console.log(this.type,this.id,removed);
|
||||
var promises = [];
|
||||
var node = this;
|
||||
// Call all registered close callbacks.
|
||||
for (var i=0;i<this._closeCallbacks.length;i++) {
|
||||
var callback = this._closeCallbacks[i];
|
||||
if (callback.length > 0) {
|
||||
// The callback takes a 'done' callback and (maybe) the removed flag
|
||||
promises.push(
|
||||
new Promise((resolve) => {
|
||||
try {
|
||||
var args = [];
|
||||
if (callback.length === 2) {
|
||||
// The listener expects the removed flag
|
||||
args.push(!!removed);
|
||||
}
|
||||
args.push(() => {
|
||||
resolve();
|
||||
});
|
||||
callback.apply(node, args);
|
||||
} catch(err) {
|
||||
// TODO: error thrown in node async close callback
|
||||
// We've never logged this properly.
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// No done callback so handle synchronously
|
||||
try {
|
||||
callback.call(node);
|
||||
} catch(err) {
|
||||
// TODO: error thrown in node sync close callback
|
||||
// We've never logged this properly.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (promises.length > 0) {
|
||||
return Promise.all(promises).then(() => {
|
||||
this.removeAllListeners("input")
|
||||
if (this._context) {
|
||||
return context.delete(this._alias||this.id,this.z);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.removeAllListeners("input")
|
||||
if (this._context) {
|
||||
return context.delete(this._alias||this.id,this.z);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a message to the nodes wired.
|
||||
*
|
||||
*
|
||||
* @param {object} msg A message or array of messages to send
|
||||
*/
|
||||
Node.prototype.send = function(msg) {
|
||||
var msgSent = false;
|
||||
var node;
|
||||
|
||||
if (msg === null || typeof msg === "undefined") {
|
||||
return;
|
||||
} else if (!Array.isArray(msg)) {
|
||||
// A single message has been passed in
|
||||
if (typeof msg !== 'object') {
|
||||
this.error(Log._("nodes.flow.non-message-returned", { type: typeof msg }));
|
||||
return
|
||||
}
|
||||
if (this._wire) {
|
||||
// A single message and a single wire on output 0
|
||||
// TODO: pre-load flows.get calls - cannot do in constructor
|
||||
// as not all nodes are defined at that point
|
||||
if (!msg._msgid) {
|
||||
msg._msgid = redUtil.generateId();
|
||||
}
|
||||
this.metric("send",msg);
|
||||
this._flow.send([{
|
||||
msg: msg,
|
||||
source: {
|
||||
id: this.id,
|
||||
node: this,
|
||||
port: 0
|
||||
},
|
||||
destination: {
|
||||
id: this._wire,
|
||||
node: undefined
|
||||
},
|
||||
cloneMessage: false
|
||||
}]);
|
||||
return;
|
||||
} else {
|
||||
msg = [msg];
|
||||
}
|
||||
}
|
||||
|
||||
var numOutputs = this.wires.length;
|
||||
|
||||
// Build a list of send events so that all cloning is done before
|
||||
// any calls to node.receive
|
||||
var sendEvents = [];
|
||||
|
||||
var sentMessageId = null;
|
||||
var hasMissingIds = false;
|
||||
// for each output of node eg. [msgs to output 0, msgs to output 1, ...]
|
||||
for (var i = 0; i < numOutputs; i++) {
|
||||
var wires = this.wires[i]; // wires leaving output i
|
||||
/* istanbul ignore else */
|
||||
if (i < msg.length) {
|
||||
var msgs = msg[i]; // msgs going to output i
|
||||
if (msgs !== null && typeof msgs !== "undefined") {
|
||||
if (!Array.isArray(msgs)) {
|
||||
msgs = [msgs];
|
||||
}
|
||||
var k = 0;
|
||||
// for each recipent node of that output
|
||||
for (var j = 0; j < wires.length; j++) {
|
||||
// for each msg to send eg. [[m1, m2, ...], ...]
|
||||
for (k = 0; k < msgs.length; k++) {
|
||||
var m = msgs[k];
|
||||
if (m !== null && m !== undefined) {
|
||||
if (typeof m !== 'object') {
|
||||
this.error(Log._("nodes.flow.non-message-returned", { type: typeof m }));
|
||||
} else {
|
||||
if (!m._msgid) {
|
||||
hasMissingIds = true;
|
||||
}
|
||||
/* istanbul ignore else */
|
||||
if (!sentMessageId) {
|
||||
sentMessageId = m._msgid;
|
||||
}
|
||||
sendEvents.push({
|
||||
msg: m,
|
||||
source: {
|
||||
id: this.id,
|
||||
node: this,
|
||||
port: i
|
||||
},
|
||||
destination: {
|
||||
id: wires[j],
|
||||
node: undefined
|
||||
},
|
||||
cloneMessage: msgSent
|
||||
});
|
||||
msgSent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* istanbul ignore else */
|
||||
if (!sentMessageId) {
|
||||
sentMessageId = redUtil.generateId();
|
||||
}
|
||||
this.metric("send",{_msgid:sentMessageId});
|
||||
|
||||
if (hasMissingIds) {
|
||||
for (i=0;i<sendEvents.length;i++) {
|
||||
var ev = sendEvents[i];
|
||||
/* istanbul ignore else */
|
||||
if (!ev.msg._msgid) {
|
||||
ev.msg._msgid = sentMessageId;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._flow.send(sendEvents);
|
||||
};
|
||||
|
||||
/**
|
||||
* Receive a message.
|
||||
*
|
||||
* This will emit the `input` event with the provided message.
|
||||
*/
|
||||
Node.prototype.receive = function(msg) {
|
||||
if (!msg) {
|
||||
msg = {};
|
||||
}
|
||||
if (!msg._msgid) {
|
||||
msg._msgid = redUtil.generateId();
|
||||
}
|
||||
this.emit("input",msg);
|
||||
};
|
||||
|
||||
function log_helper(self, level, msg) {
|
||||
var o = {
|
||||
level: level,
|
||||
id: self.id,
|
||||
type: self.type,
|
||||
msg: msg
|
||||
};
|
||||
if (self._alias) {
|
||||
o._alias = self._alias;
|
||||
}
|
||||
|
||||
if (self.z) {
|
||||
o.z = self.z;
|
||||
}
|
||||
if (self.name) {
|
||||
o.name = self.name;
|
||||
}
|
||||
// See https://github.com/node-red/node-red/issues/3327
|
||||
// See https://github.com/node-red/node-red/issues/3389
|
||||
|
||||
let srcError;
|
||||
if (msg instanceof Error) {
|
||||
srcError = msg;//use existing err object for actual stack
|
||||
} else {
|
||||
srcError = new Error(msg);//generate a new error for generate a stack
|
||||
}
|
||||
try {
|
||||
if(self instanceof Node && self._flow) {
|
||||
self._flow.log(o);
|
||||
} else {
|
||||
//if self._flow is not present, this is not a node-red Node
|
||||
//Set info to "Node object is not a node-red Node" to point out the `Node type` problem in log
|
||||
logUnexpectedError(self, srcError, "Node object is not a node-red Node")
|
||||
}
|
||||
} catch(err) {
|
||||
//build an unexpected error report indicating using the original error (for better stack trace)
|
||||
logUnexpectedError(self, srcError, `An error occured attempting to make a log entry: ${err}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Log an INFO level message
|
||||
*/
|
||||
Node.prototype.log = function(msg) {
|
||||
log_helper(this, Log.INFO, msg);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log a WARN level message
|
||||
*/
|
||||
Node.prototype.warn = function(msg) {
|
||||
log_helper(this, Log.WARN, msg);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log an ERROR level message
|
||||
*/
|
||||
Node.prototype.error = function(logMessage,msg) {
|
||||
if (typeof logMessage != 'boolean') {
|
||||
logMessage = logMessage || "";
|
||||
}
|
||||
var handled = false;
|
||||
if (this._flow && msg && typeof msg === 'object') {
|
||||
handled = this._flow.handleError(this,logMessage,msg);
|
||||
}
|
||||
if (!handled) {
|
||||
log_helper(this, Log.ERROR, logMessage);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Log an DEBUG level message
|
||||
*/
|
||||
Node.prototype.debug = function(msg) {
|
||||
log_helper(this, Log.DEBUG, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an TRACE level message
|
||||
*/
|
||||
Node.prototype.trace = function(msg) {
|
||||
log_helper(this, Log.TRACE, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a metric event.
|
||||
* If called with no args, returns whether metric collection is enabled
|
||||
*/
|
||||
Node.prototype.metric = function(eventname, msg, metricValue) {
|
||||
if (typeof eventname === "undefined") {
|
||||
return Log.metric();
|
||||
}
|
||||
var metrics = {};
|
||||
metrics.level = Log.METRIC;
|
||||
metrics.nodeid = this.id;
|
||||
metrics.event = "node."+this.type+"."+eventname;
|
||||
metrics.msgid = msg._msgid;
|
||||
metrics.value = metricValue;
|
||||
Log.log(metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the node's status object
|
||||
*
|
||||
* status: { fill:"red|green", shape:"dot|ring", text:"blah" }
|
||||
* or
|
||||
* status: "simple text status"
|
||||
*/
|
||||
Node.prototype.status = function(status) {
|
||||
switch (typeof status) {
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
status = {text:""+status}
|
||||
}
|
||||
this._flow.handleStatus(this,status);
|
||||
};
|
||||
|
||||
|
||||
function inspectObject(flow) {
|
||||
try {
|
||||
let properties = new Set()
|
||||
let currentObj = flow
|
||||
do {
|
||||
if (!Object.getPrototypeOf(currentObj)) { break }
|
||||
Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
|
||||
} while ((currentObj = Object.getPrototypeOf(currentObj)))
|
||||
let propList = [...properties.keys()].map(item => `${item}[${(typeof flow[item])[0]}]`)
|
||||
propList.sort();
|
||||
let result = [];
|
||||
let line = "";
|
||||
while (propList.length > 0) {
|
||||
let prop = propList.shift()
|
||||
if (line.length+prop.length > 80) {
|
||||
result.push(line)
|
||||
line = "";
|
||||
} else {
|
||||
line += " "+prop
|
||||
}
|
||||
}
|
||||
if (line.length > 0) {
|
||||
result.push(line);
|
||||
}
|
||||
return result.join("\n ")
|
||||
|
||||
} catch(err) {
|
||||
return "Failed to capture object properties: "+err.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function logUnexpectedError(node, error, info) {
|
||||
const header = `
|
||||
********************************************************************
|
||||
Unexpected Node Error
|
||||
********************************************************************`;
|
||||
|
||||
const footer = `
|
||||
Please report this issue, including the information logged above:
|
||||
https://github.com/node-red/node-red/issues/
|
||||
********************************************************************`;
|
||||
|
||||
let detail = [`Info:\n ${info || 'No additional info'}`];
|
||||
|
||||
//Include Error info?
|
||||
if(error && error.stack){
|
||||
detail.push(`Stack:\n ${error.stack}`)
|
||||
}
|
||||
//Include Node info?
|
||||
if(node && (node._module || node.type)){
|
||||
const moduleInfo = node._module?`${node._module.module}@${node._module.version}`:"undefined";
|
||||
const id = node._alias||node.id||"undefined";
|
||||
detail.push(`Node:\n Type: ${node.type}\n Module: ${moduleInfo}\n ID: ${id}\n Properties:\n ${inspectObject(node)}`)
|
||||
}
|
||||
//Include Flow info?
|
||||
if(node && node._flow){
|
||||
detail.push(`Flow: ${node._flow.path}\n Type: ${node._flow.TYPE}\n Properties:\n ${inspectObject(node._flow)}`)
|
||||
}
|
||||
Log.error(`${header}\n${detail.join("\n")}\n${footer}`);
|
||||
}
|
||||
|
||||
module.exports = Node;
|
||||
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
const clone = require("clone");
|
||||
const log = require("@node-red/util").log;
|
||||
const util = require("@node-red/util").util;
|
||||
const memory = require("./memory");
|
||||
|
||||
let settings;
|
||||
|
||||
// A map of scope id to context instance
|
||||
let contexts = {};
|
||||
|
||||
// A map of store name to instance
|
||||
let stores = {};
|
||||
let storeList = [];
|
||||
let defaultStore;
|
||||
|
||||
// Whether there context storage has been configured or left as default
|
||||
let hasConfiguredStore = false;
|
||||
|
||||
// Unknown Stores
|
||||
let unknownStores = {};
|
||||
|
||||
function logUnknownStore(name) {
|
||||
if (name) {
|
||||
let count = unknownStores[name] || 0;
|
||||
if (count == 0) {
|
||||
log.warn(log._("context.unknown-store", {name: name}));
|
||||
count++;
|
||||
unknownStores[name] = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init(_settings) {
|
||||
settings = _settings;
|
||||
contexts = {};
|
||||
stores = {};
|
||||
storeList = [];
|
||||
hasConfiguredStore = false;
|
||||
initialiseGlobalContext();
|
||||
|
||||
// create a default memory store - used by the unit tests that skip the full
|
||||
// `load()` initialisation sequence.
|
||||
// If the user has any stores configured, this will be disgarded
|
||||
stores["_"] = new memory();
|
||||
defaultStore = "memory";
|
||||
}
|
||||
|
||||
function initialiseGlobalContext() {
|
||||
const seed = settings.functionGlobalContext || {};
|
||||
contexts['global'] = createContext("global",seed);
|
||||
}
|
||||
|
||||
function load() {
|
||||
return new Promise(function(resolve,reject) {
|
||||
// load & init plugins in settings.contextStorage
|
||||
var plugins = settings.contextStorage || {};
|
||||
var defaultIsAlias = false;
|
||||
var promises = [];
|
||||
if (plugins && Object.keys(plugins).length > 0) {
|
||||
var hasDefault = plugins.hasOwnProperty('default');
|
||||
var defaultName;
|
||||
for (var pluginName in plugins) {
|
||||
if (plugins.hasOwnProperty(pluginName)) {
|
||||
// "_" is a reserved name - do not allow it to be overridden
|
||||
if (pluginName === "_") {
|
||||
continue;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(pluginName)) {
|
||||
return reject(new Error(log._("context.error-invalid-module-name", {name:pluginName})));
|
||||
}
|
||||
|
||||
// Check if this is setting the 'default' context to be a named plugin
|
||||
if (pluginName === "default" && typeof plugins[pluginName] === "string") {
|
||||
// Check the 'default' alias exists before initialising anything
|
||||
if (!plugins.hasOwnProperty(plugins[pluginName])) {
|
||||
return reject(new Error(log._("context.error-invalid-default-module", {storage:plugins["default"]})));
|
||||
}
|
||||
defaultIsAlias = true;
|
||||
continue;
|
||||
}
|
||||
if (!hasDefault && !defaultName) {
|
||||
defaultName = pluginName;
|
||||
}
|
||||
var plugin;
|
||||
if (plugins[pluginName].hasOwnProperty("module")) {
|
||||
// Get the provided config and copy in the 'approved' top-level settings (eg userDir)
|
||||
var config = plugins[pluginName].config || {};
|
||||
copySettings(config, settings);
|
||||
|
||||
if (typeof plugins[pluginName].module === "string") {
|
||||
// This config identifies the module by name - assume it is a built-in one
|
||||
// TODO: check it exists locally, if not, try to require it as-is
|
||||
try {
|
||||
plugin = require("./"+plugins[pluginName].module);
|
||||
} catch(err) {
|
||||
return reject(new Error(log._("context.error-loading-module2", {module:plugins[pluginName].module,message:err.toString()})));
|
||||
}
|
||||
} else {
|
||||
// Assume `module` is an already-required module we can use
|
||||
plugin = plugins[pluginName].module;
|
||||
}
|
||||
try {
|
||||
// Create a new instance of the plugin by calling its module function
|
||||
stores[pluginName] = plugin(config);
|
||||
var moduleInfo = plugins[pluginName].module;
|
||||
if (typeof moduleInfo !== 'string') {
|
||||
if (moduleInfo.hasOwnProperty("toString")) {
|
||||
moduleInfo = moduleInfo.toString();
|
||||
} else {
|
||||
moduleInfo = "custom";
|
||||
}
|
||||
}
|
||||
log.info(log._("context.log-store-init", {name:pluginName, info:"module="+moduleInfo}));
|
||||
} catch(err) {
|
||||
return reject(new Error(log._("context.error-loading-module2",{module:pluginName,message:err.toString()})));
|
||||
}
|
||||
} else {
|
||||
// Plugin does not specify a 'module'
|
||||
return reject(new Error(log._("context.error-module-not-defined", {storage:pluginName})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open all of the configured contexts
|
||||
for (var plugin in stores) {
|
||||
if (stores.hasOwnProperty(plugin)) {
|
||||
promises.push(stores[plugin].open());
|
||||
}
|
||||
}
|
||||
// There is a 'default' listed in the configuration
|
||||
if (hasDefault) {
|
||||
// If 'default' is an alias, point it at the right module - we have already
|
||||
// checked that it exists. If it isn't an alias, then it will
|
||||
// already be set to a configured store
|
||||
if (defaultIsAlias) {
|
||||
stores["_"] = stores[plugins["default"]];
|
||||
defaultStore = plugins["default"];
|
||||
} else {
|
||||
stores["_"] = stores["default"];
|
||||
defaultStore = "default";
|
||||
}
|
||||
} else if (defaultName) {
|
||||
// No 'default' listed, so pick first in list as the default
|
||||
stores["_"] = stores[defaultName];
|
||||
defaultStore = defaultName;
|
||||
defaultIsAlias = true;
|
||||
} else {
|
||||
// else there were no stores list the config object - fall through
|
||||
// to below where we default to a memory store
|
||||
storeList = ["memory"];
|
||||
defaultStore = "memory";
|
||||
}
|
||||
hasConfiguredStore = true;
|
||||
storeList = Object.keys(stores).filter(n=>!(defaultIsAlias && n==="default") && n!== "_");
|
||||
} else {
|
||||
// No configured plugins
|
||||
log.info(log._("context.log-store-init", {name:"default", info:"module=memory"}));
|
||||
promises.push(stores["_"].open())
|
||||
storeList = ["memory"];
|
||||
defaultStore = "memory";
|
||||
}
|
||||
return resolve(Promise.all(promises));
|
||||
}).catch(function(err) {
|
||||
throw new Error(log._("context.error-loading-module",{message:err.toString()}));
|
||||
});
|
||||
}
|
||||
|
||||
function copySettings(config, settings){
|
||||
var copy = ["userDir"]
|
||||
config.settings = {};
|
||||
copy.forEach(function(setting){
|
||||
config.settings[setting] = clone(settings[setting]);
|
||||
});
|
||||
}
|
||||
|
||||
function getContextStorage(storage) {
|
||||
if (stores.hasOwnProperty(storage)) {
|
||||
// A known context
|
||||
return stores[storage];
|
||||
} else if (stores.hasOwnProperty("_")) {
|
||||
// Not known, but we have a default to fall back to
|
||||
if (storage !== defaultStore) {
|
||||
// It isn't the default store either, so log it
|
||||
logUnknownStore(storage);
|
||||
}
|
||||
return stores["_"];
|
||||
}
|
||||
}
|
||||
|
||||
function followParentContext(parent, key) {
|
||||
if (key === "$parent") {
|
||||
return [parent, undefined];
|
||||
}
|
||||
else if (key.startsWith("$parent.")) {
|
||||
var len = "$parent.".length;
|
||||
var new_key = key.substring(len);
|
||||
var ctx = parent;
|
||||
while (ctx && new_key.startsWith("$parent.")) {
|
||||
ctx = ctx.$parent;
|
||||
new_key = new_key.substring(len);
|
||||
}
|
||||
return [ctx, new_key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateContextKey(key) {
|
||||
try {
|
||||
const keys = Array.isArray(key) ? key : [key];
|
||||
if(!keys.length) { return false }; //no key to get/set
|
||||
for (let index = 0; index < keys.length; index++) {
|
||||
const k = keys[index];
|
||||
if (typeof k !== "string" || !k.length) {
|
||||
return false; //not string or zero-length
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function createContext(id,seed,parent) {
|
||||
// Seed is only set for global context - sourced from functionGlobalContext
|
||||
const scope = id;
|
||||
const obj = {};
|
||||
let seedKeys;
|
||||
let insertSeedValues;
|
||||
if (seed) {
|
||||
seedKeys = Object.keys(seed);
|
||||
seedKeys.forEach(key => {
|
||||
obj[key] = seed[key];
|
||||
})
|
||||
insertSeedValues = function(keys,values) {
|
||||
if (!Array.isArray(keys)) {
|
||||
if (values[0] === undefined) {
|
||||
try {
|
||||
values[0] = util.getObjectProperty(seed,keys);
|
||||
} catch(err) {
|
||||
if (err.code === "INVALID_EXPR") {
|
||||
throw err;
|
||||
}
|
||||
values[0] = undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i=0;i<keys.length;i++) {
|
||||
if (values[i] === undefined) {
|
||||
try {
|
||||
values[i] = util.getObjectProperty(seed,keys[i]);
|
||||
} catch(err) {
|
||||
if (err.code === "INVALID_EXPR") {
|
||||
throw err;
|
||||
}
|
||||
values[i] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(obj, {
|
||||
get: {
|
||||
value: function(key, storage, callback) {
|
||||
var context;
|
||||
if (!callback && typeof storage === 'function') {
|
||||
callback = storage;
|
||||
storage = undefined;
|
||||
}
|
||||
if (callback && typeof callback !== 'function'){
|
||||
throw new Error("Callback must be a function");
|
||||
}
|
||||
if (!validateContextKey(key)) {
|
||||
var err = Error("Invalid context key");
|
||||
if(callback) {
|
||||
return callback(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(key)) {
|
||||
var keyParts = util.parseContextStore(key);
|
||||
key = keyParts.key;
|
||||
if (!storage) {
|
||||
storage = keyParts.store || "_";
|
||||
}
|
||||
var result = followParentContext(parent, key);
|
||||
if (result) {
|
||||
var [ctx, new_key] = result;
|
||||
if (ctx && new_key) {
|
||||
return ctx.get(new_key, storage, callback);
|
||||
}
|
||||
else {
|
||||
if (callback) {
|
||||
return callback(undefined);
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!storage) {
|
||||
storage = "_";
|
||||
}
|
||||
}
|
||||
context = getContextStorage(storage);
|
||||
|
||||
if (callback) {
|
||||
if (!seed) {
|
||||
context.get(scope,key,callback);
|
||||
} else {
|
||||
context.get(scope,key,function() {
|
||||
if (arguments[0]) {
|
||||
callback(arguments[0]);
|
||||
return;
|
||||
}
|
||||
var results = Array.prototype.slice.call(arguments,[1]);
|
||||
try {
|
||||
insertSeedValues(key,results);
|
||||
} catch(err) {
|
||||
callback.apply(err);
|
||||
return
|
||||
}
|
||||
// Put the err arg back
|
||||
results.unshift(undefined);
|
||||
callback.apply(null,results);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No callback, attempt to do this synchronously
|
||||
var results = context.get(scope,key);
|
||||
if (seed) {
|
||||
if (Array.isArray(key)) {
|
||||
insertSeedValues(key,results);
|
||||
} else if (results === undefined){
|
||||
try {
|
||||
results = util.getObjectProperty(seed,key);
|
||||
} catch(err) {
|
||||
if (err.code === "INVALID_EXPR") {
|
||||
throw err;
|
||||
}
|
||||
results = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
},
|
||||
set: {
|
||||
value: function(key, value, storage, callback) {
|
||||
var context;
|
||||
if (!callback && typeof storage === 'function') {
|
||||
callback = storage;
|
||||
storage = undefined;
|
||||
}
|
||||
if (callback && typeof callback !== 'function'){
|
||||
throw new Error("Callback must be a function");
|
||||
}
|
||||
if (!validateContextKey(key)) {
|
||||
var err = Error("Invalid context key");
|
||||
if(callback) {
|
||||
return callback(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(key)) {
|
||||
var keyParts = util.parseContextStore(key);
|
||||
key = keyParts.key;
|
||||
if (!storage) {
|
||||
storage = keyParts.store || "_";
|
||||
}
|
||||
var result = followParentContext(parent, key);
|
||||
if (result) {
|
||||
var [ctx, new_key] = result;
|
||||
if (ctx && new_key) {
|
||||
return ctx.set(new_key, value, storage, callback);
|
||||
}
|
||||
else {
|
||||
if (callback) {
|
||||
return callback();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!storage) {
|
||||
storage = "_";
|
||||
}
|
||||
}
|
||||
context = getContextStorage(storage);
|
||||
|
||||
context.set(scope, key, value, callback);
|
||||
}
|
||||
},
|
||||
keys: {
|
||||
value: function(storage, callback) {
|
||||
var context;
|
||||
if (!storage && !callback) {
|
||||
context = stores["_"];
|
||||
} else {
|
||||
if (typeof storage === 'function') {
|
||||
callback = storage;
|
||||
storage = "_";
|
||||
}
|
||||
if (callback && typeof callback !== 'function') {
|
||||
throw new Error("Callback must be a function");
|
||||
}
|
||||
context = getContextStorage(storage);
|
||||
}
|
||||
if (seed && settings.exportGlobalContextKeys !== false) {
|
||||
if (callback) {
|
||||
context.keys(scope, function(err,keys) {
|
||||
callback(err,Array.from(new Set(seedKeys.concat(keys)).keys()));
|
||||
});
|
||||
} else {
|
||||
var keys = context.keys(scope);
|
||||
return Array.from(new Set(seedKeys.concat(keys)).keys())
|
||||
}
|
||||
} else {
|
||||
return context.keys(scope, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (parent) {
|
||||
Object.defineProperty(obj, "$parent", {
|
||||
value: parent
|
||||
});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function createRootContext() {
|
||||
var obj = {};
|
||||
Object.defineProperties(obj, {
|
||||
get: {
|
||||
value: function(key, storage, callback) {
|
||||
if (!callback && typeof storage === 'function') {
|
||||
callback = storage;
|
||||
storage = undefined;
|
||||
}
|
||||
if (callback) {
|
||||
callback()
|
||||
return;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
set: {
|
||||
value: function(key, value, storage, callback) {
|
||||
if (!callback && typeof storage === 'function') {
|
||||
callback = storage;
|
||||
storage = undefined;
|
||||
}
|
||||
if (callback) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
keys: {
|
||||
value: function(storage, callback) {
|
||||
if (!callback && typeof storage === 'function') {
|
||||
callback = storage;
|
||||
storage = undefined;
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a flow-level context object.
|
||||
* @param {string} flowId [description]
|
||||
* @param {string} parentFlowId the id of the parent flow. undefined
|
||||
* @return {object}} the context object
|
||||
*/
|
||||
function getFlowContext(flowId,parentFlowId) {
|
||||
if (contexts.hasOwnProperty(flowId)) {
|
||||
return contexts[flowId];
|
||||
}
|
||||
var parentContext = contexts[parentFlowId];
|
||||
if (!parentContext) {
|
||||
parentContext = createRootContext();
|
||||
contexts[parentFlowId] = parentContext;
|
||||
// throw new Error("Flow "+flowId+" is missing parent context "+parentFlowId);
|
||||
}
|
||||
var newContext = createContext(flowId,undefined,parentContext);
|
||||
contexts[flowId] = newContext;
|
||||
return newContext;
|
||||
|
||||
}
|
||||
|
||||
function getContext(nodeId, flowId) {
|
||||
var contextId = nodeId;
|
||||
if (flowId) {
|
||||
contextId = nodeId+":"+flowId;
|
||||
}
|
||||
if (contexts.hasOwnProperty(contextId)) {
|
||||
return contexts[contextId];
|
||||
}
|
||||
var newContext = createContext(contextId);
|
||||
|
||||
if (flowId) {
|
||||
var flowContext = contexts[flowId];
|
||||
if (!flowContext) {
|
||||
// This is most likely due to a unit test for a node which doesn't
|
||||
// initialise the flow properly.
|
||||
// To keep things working, initialise the missing context.
|
||||
// This *does not happen* in normal node-red operation
|
||||
flowContext = createContext(flowId,undefined,createRootContext());
|
||||
contexts[flowId] = flowContext;
|
||||
}
|
||||
Object.defineProperty(newContext, 'flow', {
|
||||
value: flowContext
|
||||
});
|
||||
}
|
||||
Object.defineProperty(newContext, 'global', {
|
||||
value: contexts['global']
|
||||
})
|
||||
contexts[contextId] = newContext;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the context of the given node/flow/global
|
||||
*
|
||||
* If the user has configured a context store, this
|
||||
* will no-op a request to delete node/flow context.
|
||||
*/
|
||||
function deleteContext(id,flowId) {
|
||||
if (id === "global") {
|
||||
// 1. delete global from all configured stores
|
||||
var promises = [];
|
||||
for(var plugin in stores){
|
||||
if(stores.hasOwnProperty(plugin)){
|
||||
promises.push(stores[plugin].delete('global'));
|
||||
}
|
||||
}
|
||||
return Promise.all(promises).then(function() {
|
||||
// 2. delete global context
|
||||
delete contexts['global'];
|
||||
// 3. reinitialise global context
|
||||
initialiseGlobalContext();
|
||||
})
|
||||
} else if (!hasConfiguredStore) {
|
||||
// only delete context if there's no configured storage.
|
||||
var contextId = id;
|
||||
if (flowId) {
|
||||
contextId = id+":"+flowId;
|
||||
}
|
||||
delete contexts[contextId];
|
||||
return stores["_"].delete(contextId);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any contexts that are no longer in use
|
||||
* @param flowConfig object includes allNodes as object of id->node
|
||||
*
|
||||
* If flowConfig is undefined, all flow/node contexts will be removed
|
||||
**/
|
||||
function clean(flowConfig) {
|
||||
flowConfig = flowConfig || { allNodes: {}, subflows: {} };
|
||||
const knownNodes = new Set(Object.keys(flowConfig.allNodes))
|
||||
|
||||
// We need to alias all of the subflow instance contents
|
||||
for (const subflow of Object.values(flowConfig.subflows || {})) {
|
||||
subflow.instances.forEach(instance => {
|
||||
for (const nodeId of Object.keys(subflow.nodes || {})) {
|
||||
knownNodes.add(`${instance.id}-${nodeId}`)
|
||||
}
|
||||
for (const nodeId of Object.keys(subflow.configs || {})) {
|
||||
knownNodes.add(`${instance.id}-${nodeId}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
var promises = [];
|
||||
for (const store of Object.values(stores)){
|
||||
promises.push(store.clean(Array.from(knownNodes)));
|
||||
}
|
||||
for (const id of Object.keys(contexts)) {
|
||||
if (id !== "global") {
|
||||
var idParts = id.split(":");
|
||||
if (!knownNodes.has(idParts[0])) {
|
||||
delete contexts[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all contexts, including global and reinitialises global to
|
||||
* initial state.
|
||||
*/
|
||||
function clear() {
|
||||
return clean().then(function() {
|
||||
return deleteContext('global')
|
||||
})
|
||||
}
|
||||
|
||||
function close() {
|
||||
var promises = [];
|
||||
for(var plugin in stores){
|
||||
if(stores.hasOwnProperty(plugin)){
|
||||
promises.push(stores[plugin].close());
|
||||
}
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function listStores() {
|
||||
return {default:defaultStore,stores:storeList};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
load: load,
|
||||
listStores: listStores,
|
||||
get: getContext,
|
||||
getFlowContext:getFlowContext,
|
||||
delete: deleteContext,
|
||||
clean: clean,
|
||||
clear: clear,
|
||||
close: close
|
||||
};
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
/**
|
||||
* Local file-system based context storage
|
||||
*
|
||||
* Configuration options:
|
||||
* {
|
||||
* base: "context", // the base directory to use
|
||||
* // default: "context"
|
||||
* dir: "/path/to/storage", // the directory to create the base directory in
|
||||
* // default: settings.userDir
|
||||
* cache: true, // whether to cache contents in memory
|
||||
* // default: true
|
||||
* flushInterval: 30 // if cache is enabled, the minimum interval
|
||||
* // between writes to storage, in seconds. This
|
||||
* can be used to reduce wear on underlying storage.
|
||||
* default: 30 seconds
|
||||
* }
|
||||
*
|
||||
*
|
||||
* $HOME/.node-red/contexts
|
||||
* ├── global
|
||||
* │ └── global_context.json
|
||||
* ├── <id of Flow 1>
|
||||
* │ ├── flow_context.json
|
||||
* │ ├── <id of Node a>.json
|
||||
* │ └── <id of Node b>.json
|
||||
* └── <id of Flow 2>
|
||||
* ├── flow_context.json
|
||||
* ├── <id of Node x>.json
|
||||
* └── <id of Node y>.json
|
||||
*/
|
||||
|
||||
var fs = require('fs-extra');
|
||||
var path = require("path");
|
||||
var util = require("@node-red/util").util;
|
||||
var log = require("@node-red/util").log;
|
||||
|
||||
var safeJSONStringify = require("json-stringify-safe");
|
||||
var MemoryStore = require("./memory");
|
||||
|
||||
|
||||
function getStoragePath(storageBaseDir, scope) {
|
||||
if(scope.indexOf(":") === -1){
|
||||
if(scope === "global"){
|
||||
return path.join(storageBaseDir,"global",scope);
|
||||
}else{ // scope:flow
|
||||
return path.join(storageBaseDir,scope,"flow");
|
||||
}
|
||||
}else{ // scope:local
|
||||
var ids = scope.split(":")
|
||||
return path.join(storageBaseDir,ids[1],ids[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function getBasePath(config) {
|
||||
var base = config.base || "context";
|
||||
var storageBaseDir;
|
||||
if (!config.dir) {
|
||||
if(config.settings && config.settings.userDir){
|
||||
storageBaseDir = path.join(config.settings.userDir, base);
|
||||
}else{
|
||||
try {
|
||||
fs.statSync(path.join(process.env.NODE_RED_HOME,".config.json"));
|
||||
storageBaseDir = path.join(process.env.NODE_RED_HOME, base);
|
||||
} catch(err) {
|
||||
try {
|
||||
// Consider compatibility for older versions
|
||||
if (process.env.HOMEPATH) {
|
||||
fs.statSync(path.join(process.env.HOMEPATH,".node-red",".config.json"));
|
||||
storageBaseDir = path.join(process.env.HOMEPATH, ".node-red", base);
|
||||
}
|
||||
} catch(err) {
|
||||
}
|
||||
if (!storageBaseDir) {
|
||||
storageBaseDir = path.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || process.env.NODE_RED_HOME,".node-red", base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
storageBaseDir = path.join(config.dir, base);
|
||||
}
|
||||
return storageBaseDir;
|
||||
}
|
||||
|
||||
function loadFile(storagePath){
|
||||
return fs.pathExists(storagePath).then(function(exists){
|
||||
if(exists === true){
|
||||
return fs.readFile(storagePath, "utf8");
|
||||
}else{
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function listFiles(storagePath) {
|
||||
var promises = [];
|
||||
return fs.readdir(storagePath).then(function(files) {
|
||||
files.forEach(function(file) {
|
||||
if (!/^\./.test(file)) {
|
||||
var fullPath = path.join(storagePath,file);
|
||||
var stats = fs.statSync(fullPath);
|
||||
if (stats.isDirectory()) {
|
||||
promises.push(fs.readdir(fullPath).then(function(subdirFiles) {
|
||||
var result = [];
|
||||
subdirFiles.forEach(subfile => {
|
||||
if (/\.json$/.test(subfile)) {
|
||||
result.push(path.join(file,subfile))
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}).then(dirs => dirs.reduce((acc, val) => acc.concat(val), []));
|
||||
}
|
||||
|
||||
function stringify(value) {
|
||||
var hasCircular;
|
||||
var result = safeJSONStringify(value,null,4,function(k,v){hasCircular = true})
|
||||
return { json: result, circular: hasCircular };
|
||||
}
|
||||
|
||||
async function writeFileAtomic(storagePath, content) {
|
||||
// To protect against file corruption, write to a tmp file first and then
|
||||
// rename to the destination file
|
||||
let finalFile = storagePath + ".json";
|
||||
let tmpFile = finalFile + "."+Date.now()+".tmp";
|
||||
await fs.outputFile(tmpFile, content, "utf8");
|
||||
return fs.rename(tmpFile,finalFile);
|
||||
}
|
||||
|
||||
function LocalFileSystem(config){
|
||||
this.config = config;
|
||||
this.storageBaseDir = getBasePath(this.config);
|
||||
this.writePromise = Promise.resolve();
|
||||
if (config.hasOwnProperty('cache')?config.cache:true) {
|
||||
this.cache = MemoryStore({});
|
||||
}
|
||||
this.pendingWrites = {};
|
||||
this.knownCircularRefs = {};
|
||||
|
||||
if (config.hasOwnProperty('flushInterval')) {
|
||||
this.flushInterval = Math.max(0,config.flushInterval) * 1000;
|
||||
} else {
|
||||
this.flushInterval = 30000;
|
||||
}
|
||||
}
|
||||
|
||||
LocalFileSystem.prototype.open = function(){
|
||||
var self = this;
|
||||
if (this.cache) {
|
||||
var scopes = [];
|
||||
var promises = [];
|
||||
var contextFiles = [];
|
||||
return listFiles(self.storageBaseDir).then(function(files) {
|
||||
files.forEach(function(file) {
|
||||
var parts = file.split(path.sep);
|
||||
if (parts[0] === 'global') {
|
||||
scopes.push("global");
|
||||
} else if (parts[1] === 'flow.json') {
|
||||
scopes.push(parts[0])
|
||||
} else {
|
||||
scopes.push(parts[1].substring(0,parts[1].length-5)+":"+parts[0]);
|
||||
}
|
||||
let contextFile = path.join(self.storageBaseDir,file);
|
||||
contextFiles.push(contextFile)
|
||||
promises.push(loadFile(contextFile));
|
||||
})
|
||||
return Promise.all(promises);
|
||||
}).then(function(res) {
|
||||
scopes.forEach(function(scope,i) {
|
||||
try {
|
||||
var data = res[i]?JSON.parse(res[i]):{};
|
||||
Object.keys(data).forEach(function(key) {
|
||||
self.cache.set(scope,key,data[key]);
|
||||
})
|
||||
} catch(err) {
|
||||
let error = new Error(log._("context.localfilesystem.invalid-json",{file: contextFiles[i]}))
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}).catch(function(err){
|
||||
if(err.code == 'ENOENT') {
|
||||
return fs.ensureDir(self.storageBaseDir);
|
||||
}else{
|
||||
throw err;
|
||||
}
|
||||
}).then(function() {
|
||||
self._flushPendingWrites = function() {
|
||||
var scopes = Object.keys(self.pendingWrites);
|
||||
self.pendingWrites = {};
|
||||
var promises = [];
|
||||
var newContext = self.cache._export();
|
||||
scopes.forEach(function(scope) {
|
||||
var storagePath = getStoragePath(self.storageBaseDir,scope);
|
||||
var context = newContext[scope] || {};
|
||||
var stringifiedContext = stringify(context);
|
||||
if (stringifiedContext.circular && !self.knownCircularRefs[scope]) {
|
||||
log.warn(log._("context.localfilesystem.error-circular",{scope:scope}));
|
||||
self.knownCircularRefs[scope] = true;
|
||||
} else {
|
||||
delete self.knownCircularRefs[scope];
|
||||
}
|
||||
log.debug("Flushing localfilesystem context scope "+scope);
|
||||
promises.push(writeFileAtomic(storagePath, stringifiedContext.json))
|
||||
});
|
||||
delete self._pendingWriteTimeout;
|
||||
return Promise.all(promises);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self._flushPendingWrites = function() { }
|
||||
return fs.ensureDir(self.storageBaseDir);
|
||||
}
|
||||
}
|
||||
|
||||
LocalFileSystem.prototype.close = function(){
|
||||
var self = this;
|
||||
if (this.cache && this._pendingWriteTimeout) {
|
||||
clearTimeout(this._pendingWriteTimeout);
|
||||
delete this._pendingWriteTimeout;
|
||||
this.flushInterval = 0;
|
||||
self.writePromise = self.writePromise.then(function(){
|
||||
return self._flushPendingWrites.call(self).catch(function(err) {
|
||||
log.error(log._("context.localfilesystem.error-write",{message:err.toString()}));
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
return this.writePromise;
|
||||
}
|
||||
|
||||
LocalFileSystem.prototype.get = function(scope, key, callback) {
|
||||
if (this.cache) {
|
||||
return this.cache.get(scope,key,callback);
|
||||
}
|
||||
if(typeof callback !== "function"){
|
||||
throw new Error("File Store cache disabled - only asynchronous access supported");
|
||||
}
|
||||
var storagePath = getStoragePath(this.storageBaseDir ,scope);
|
||||
loadFile(storagePath + ".json").then(function(data){
|
||||
var value;
|
||||
if(data){
|
||||
data = JSON.parse(data);
|
||||
if (!Array.isArray(key)) {
|
||||
try {
|
||||
value = util.getObjectProperty(data,key);
|
||||
} catch(err) {
|
||||
if (err.code === "INVALID_EXPR") {
|
||||
throw err;
|
||||
}
|
||||
value = undefined;
|
||||
}
|
||||
callback(null, value);
|
||||
} else {
|
||||
var results = [undefined];
|
||||
for (var i=0;i<key.length;i++) {
|
||||
try {
|
||||
value = util.getObjectProperty(data,key[i]);
|
||||
} catch(err) {
|
||||
if (err.code === "INVALID_EXPR") {
|
||||
throw err;
|
||||
}
|
||||
value = undefined;
|
||||
}
|
||||
results.push(value)
|
||||
}
|
||||
callback.apply(null,results);
|
||||
}
|
||||
}else{
|
||||
callback(null, undefined);
|
||||
}
|
||||
}).catch(function(err){
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
LocalFileSystem.prototype.set = function(scope, key, value, callback) {
|
||||
var self = this;
|
||||
var storagePath = getStoragePath(this.storageBaseDir ,scope);
|
||||
if (this.cache) {
|
||||
this.cache.set(scope,key,value,callback);
|
||||
this.pendingWrites[scope] = true;
|
||||
if (this._pendingWriteTimeout) {
|
||||
// there's a pending write which will handle this
|
||||
return;
|
||||
} else {
|
||||
this._pendingWriteTimeout = setTimeout(function() {
|
||||
self.writePromise = self.writePromise.then(function(){
|
||||
return self._flushPendingWrites.call(self).catch(function(err) {
|
||||
log.error(log._("context.localfilesystem.error-write",{message:err.toString()}));
|
||||
});
|
||||
});
|
||||
}, this.flushInterval);
|
||||
}
|
||||
} else if (callback && typeof callback !== 'function') {
|
||||
throw new Error("File Store cache disabled - only asynchronous access supported");
|
||||
} else {
|
||||
self.writePromise = self.writePromise.then(function() { return loadFile(storagePath + ".json") }).then(function(data){
|
||||
var obj = data ? JSON.parse(data) : {}
|
||||
if (!Array.isArray(key)) {
|
||||
key = [key];
|
||||
value = [value];
|
||||
} else if (!Array.isArray(value)) {
|
||||
// key is an array, but value is not - wrap it as an array
|
||||
value = [value];
|
||||
}
|
||||
for (var i=0;i<key.length;i++) {
|
||||
var v = null;
|
||||
if (i<value.length) {
|
||||
v = value[i];
|
||||
}
|
||||
util.setObjectProperty(obj,key[i],v);
|
||||
}
|
||||
var stringifiedContext = stringify(obj);
|
||||
if (stringifiedContext.circular && !self.knownCircularRefs[scope]) {
|
||||
log.warn(log._("context.localfilesystem.error-circular",{scope:scope}));
|
||||
self.knownCircularRefs[scope] = true;
|
||||
} else {
|
||||
delete self.knownCircularRefs[scope];
|
||||
}
|
||||
return writeFileAtomic(storagePath, stringifiedContext.json);
|
||||
}).then(function(){
|
||||
if(typeof callback === "function"){
|
||||
callback(null);
|
||||
}
|
||||
}).catch(function(err){
|
||||
if(typeof callback === "function"){
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
LocalFileSystem.prototype.keys = function(scope, callback){
|
||||
if (this.cache) {
|
||||
return this.cache.keys(scope,callback);
|
||||
}
|
||||
if(typeof callback !== "function"){
|
||||
throw new Error("Callback must be a function");
|
||||
}
|
||||
var storagePath = getStoragePath(this.storageBaseDir ,scope);
|
||||
loadFile(storagePath + ".json").then(function(data){
|
||||
if(data){
|
||||
callback(null, Object.keys(JSON.parse(data)));
|
||||
}else{
|
||||
callback(null, []);
|
||||
}
|
||||
}).catch(function(err){
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
LocalFileSystem.prototype.delete = function(scope){
|
||||
var cachePromise;
|
||||
if (this.cache) {
|
||||
cachePromise = this.cache.delete(scope);
|
||||
} else {
|
||||
cachePromise = Promise.resolve();
|
||||
}
|
||||
var that = this;
|
||||
delete this.pendingWrites[scope];
|
||||
return cachePromise.then(function() {
|
||||
var storagePath = getStoragePath(that.storageBaseDir,scope);
|
||||
return fs.remove(storagePath + ".json");
|
||||
});
|
||||
}
|
||||
|
||||
LocalFileSystem.prototype.clean = function(_activeNodes) {
|
||||
var activeNodes = {};
|
||||
_activeNodes.forEach(function(node) { activeNodes[node] = true });
|
||||
var self = this;
|
||||
var cachePromise;
|
||||
if (this.cache) {
|
||||
cachePromise = this.cache.clean(_activeNodes);
|
||||
} else {
|
||||
cachePromise = Promise.resolve();
|
||||
}
|
||||
this.knownCircularRefs = {};
|
||||
return cachePromise.then(() => listFiles(self.storageBaseDir)).then(function(files) {
|
||||
var promises = [];
|
||||
files.forEach(function(file) {
|
||||
var parts = file.split(path.sep);
|
||||
var removePromise;
|
||||
if (parts[0] === 'global') {
|
||||
// never clean global
|
||||
return;
|
||||
} else if (!activeNodes[parts[0]]) {
|
||||
// Flow removed - remove the whole dir
|
||||
removePromise = fs.remove(path.join(self.storageBaseDir,parts[0]));
|
||||
} else if (parts[1] !== 'flow.json' && !activeNodes[parts[1].substring(0,parts[1].length-5)]) {
|
||||
// Node removed - remove the context file
|
||||
removePromise = fs.remove(path.join(self.storageBaseDir,file));
|
||||
}
|
||||
if (removePromise) {
|
||||
promises.push(removePromise);
|
||||
}
|
||||
});
|
||||
return Promise.all(promises)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function(config){
|
||||
return new LocalFileSystem(config);
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var util = require("@node-red/util").util;
|
||||
|
||||
function Memory(config){
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
Memory.prototype.open = function(){
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
Memory.prototype.close = function(){
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
Memory.prototype._getOne = function(scope, key) {
|
||||
var value;
|
||||
var error;
|
||||
if(this.data[scope]){
|
||||
try {
|
||||
value = util.getObjectProperty(this.data[scope], key);
|
||||
} catch(err) {
|
||||
if (err.code === "INVALID_EXPR") {
|
||||
throw err;
|
||||
}
|
||||
value = undefined;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Memory.prototype.get = function(scope, key, callback) {
|
||||
var value;
|
||||
var error;
|
||||
if (!Array.isArray(key)) {
|
||||
try {
|
||||
value = this._getOne(scope,key);
|
||||
} catch(err) {
|
||||
if (!callback) {
|
||||
throw err;
|
||||
}
|
||||
error = err;
|
||||
}
|
||||
if (callback) {
|
||||
callback(error,value);
|
||||
return;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
value = [];
|
||||
for (var i=0; i<key.length; i++) {
|
||||
try {
|
||||
value.push(this._getOne(scope,key[i]));
|
||||
} catch(err) {
|
||||
if (!callback) {
|
||||
throw err;
|
||||
} else {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback.apply(null, [undefined].concat(value));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
Memory.prototype.set = function(scope, key, value, callback) {
|
||||
if(!this.data[scope]){
|
||||
this.data[scope] = {};
|
||||
}
|
||||
var error;
|
||||
if (!Array.isArray(key)) {
|
||||
key = [key];
|
||||
value = [value];
|
||||
} else if (!Array.isArray(value)) {
|
||||
// key is an array, but value is not - wrap it as an array
|
||||
value = [value];
|
||||
}
|
||||
try {
|
||||
for (var i=0; i<key.length; i++) {
|
||||
var v = null;
|
||||
if (i < value.length) {
|
||||
v = value[i];
|
||||
}
|
||||
util.setObjectProperty(this.data[scope],key[i],v);
|
||||
}
|
||||
} catch(err) {
|
||||
if (callback) {
|
||||
error = err;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if(callback){
|
||||
callback(error);
|
||||
}
|
||||
};
|
||||
|
||||
Memory.prototype.keys = function(scope, callback){
|
||||
var values = [];
|
||||
var error;
|
||||
try{
|
||||
if(this.data[scope]){
|
||||
if (scope !== "global") {
|
||||
values = Object.keys(this.data[scope]);
|
||||
} else {
|
||||
values = Object.keys(this.data[scope]).filter(function (key) {
|
||||
return key !== "set" && key !== "get" && key !== "keys";
|
||||
});
|
||||
}
|
||||
}
|
||||
}catch(err){
|
||||
if(callback){
|
||||
error = err;
|
||||
}else{
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if(callback){
|
||||
if(error){
|
||||
callback(error);
|
||||
} else {
|
||||
callback(null, values);
|
||||
}
|
||||
} else {
|
||||
return values;
|
||||
}
|
||||
};
|
||||
|
||||
Memory.prototype.delete = function(scope){
|
||||
delete this.data[scope];
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
Memory.prototype.clean = function(activeNodes){
|
||||
for(var id in this.data){
|
||||
if(this.data.hasOwnProperty(id) && id !== "global"){
|
||||
var idParts = id.split(":");
|
||||
if(activeNodes.indexOf(idParts[0]) === -1){
|
||||
delete this.data[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
Memory.prototype._export = function() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
module.exports = function(config){
|
||||
return new Memory(config);
|
||||
};
|
||||
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var crypto = require('crypto');
|
||||
var runtime;
|
||||
var settings;
|
||||
var log;
|
||||
|
||||
|
||||
var encryptedCredentials = null;
|
||||
var credentialCache = {};
|
||||
var credentialsDef = {};
|
||||
var dirty = false;
|
||||
|
||||
var removeDefaultKey = false;
|
||||
var encryptionEnabled = null;
|
||||
var encryptionKeyType; // disabled, system, user, project
|
||||
var encryptionAlgorithm = "aes-256-ctr";
|
||||
var encryptionKey;
|
||||
|
||||
function decryptCredentials(key,credentials) {
|
||||
var creds = credentials["$"];
|
||||
var initVector = Buffer.from(creds.substring(0, 32),'hex');
|
||||
creds = creds.substring(32);
|
||||
var decipher = crypto.createDecipheriv(encryptionAlgorithm, key, initVector);
|
||||
var decrypted = decipher.update(creds, 'base64', 'utf8') + decipher.final('utf8');
|
||||
return JSON.parse(decrypted);
|
||||
}
|
||||
function encryptCredentials(key,credentials) {
|
||||
var initVector = crypto.randomBytes(16);
|
||||
var cipher = crypto.createCipheriv(encryptionAlgorithm, key, initVector);
|
||||
return {"$":initVector.toString('hex') + cipher.update(JSON.stringify(credentials), 'utf8', 'base64') + cipher.final('base64')};
|
||||
}
|
||||
|
||||
var api = module.exports = {
|
||||
init: function(_runtime) {
|
||||
runtime = _runtime;
|
||||
log = runtime.log;
|
||||
settings = runtime.settings;
|
||||
dirty = false;
|
||||
credentialCache = {};
|
||||
credentialsDef = {};
|
||||
encryptionEnabled = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the credentials from storage.
|
||||
*/
|
||||
load: async function (credentials) {
|
||||
dirty = false;
|
||||
var credentialsEncrypted = credentials.hasOwnProperty("$") && Object.keys(credentials).length === 1;
|
||||
|
||||
// Case 1: Active Project in place
|
||||
// - use whatever its config says
|
||||
|
||||
// Case 2: _credentialSecret unset, credentialSecret unset
|
||||
// - generate _credentialSecret and encrypt
|
||||
|
||||
// Case 3: _credentialSecret set, credentialSecret set
|
||||
// - migrate from _credentialSecret to credentialSecret
|
||||
// - delete _credentialSecret
|
||||
|
||||
// Case 4: credentialSecret set
|
||||
// - use it
|
||||
|
||||
var setupEncryptionPromise = Promise.resolve();
|
||||
|
||||
var projectKey = false;
|
||||
var activeProject;
|
||||
encryptionKeyType = "";
|
||||
|
||||
if (runtime.storage && runtime.storage.projects) {
|
||||
// projects enabled
|
||||
activeProject = runtime.storage.projects.getActiveProject();
|
||||
if (activeProject) {
|
||||
projectKey = activeProject.credentialSecret;
|
||||
if (!projectKey) {
|
||||
log.debug("red/runtime/nodes/credentials.load : using active project key - disabled");
|
||||
encryptionKeyType = "disabled";
|
||||
encryptionEnabled = false;
|
||||
} else {
|
||||
log.debug("red/runtime/nodes/credentials.load : using active project key");
|
||||
encryptionKeyType = "project";
|
||||
encryptionKey = crypto.createHash('sha256').update(projectKey).digest();
|
||||
encryptionEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (encryptionKeyType === '') {
|
||||
var defaultKey;
|
||||
try {
|
||||
defaultKey = settings.get('_credentialSecret');
|
||||
} catch(err) {
|
||||
}
|
||||
if (defaultKey) {
|
||||
defaultKey = crypto.createHash('sha256').update(defaultKey).digest();
|
||||
encryptionKeyType = "system";
|
||||
}
|
||||
var userKey;
|
||||
try {
|
||||
userKey = settings.get('credentialSecret');
|
||||
} catch(err) {
|
||||
userKey = false;
|
||||
}
|
||||
|
||||
if (userKey === false) {
|
||||
encryptionKeyType = "disabled";
|
||||
log.debug("red/runtime/nodes/credentials.load : user disabled encryption");
|
||||
// User has disabled encryption
|
||||
encryptionEnabled = false;
|
||||
// Check if we have a generated _credSecret to decrypt with and remove
|
||||
if (defaultKey) {
|
||||
log.debug("red/runtime/nodes/credentials.load : default key present. Will migrate");
|
||||
if (credentialsEncrypted) {
|
||||
try {
|
||||
credentials = decryptCredentials(defaultKey,credentials)
|
||||
} catch(err) {
|
||||
credentials = {};
|
||||
log.warn(log._("nodes.credentials.error",{message:err.toString()}))
|
||||
var error = new Error("Failed to decrypt credentials");
|
||||
error.code = "credentials_load_failed";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
dirty = true;
|
||||
removeDefaultKey = true;
|
||||
}
|
||||
} else if (typeof userKey === 'string') {
|
||||
if (!projectKey) {
|
||||
log.debug("red/runtime/nodes/credentials.load : user provided key");
|
||||
}
|
||||
if (encryptionKeyType !== 'project') {
|
||||
encryptionKeyType = 'user';
|
||||
}
|
||||
// User has provided own encryption key, get the 32-byte hash of it
|
||||
encryptionKey = crypto.createHash('sha256').update(userKey).digest();
|
||||
encryptionEnabled = true;
|
||||
|
||||
if (encryptionKeyType !== 'project' && defaultKey) {
|
||||
log.debug("red/runtime/nodes/credentials.load : default key present. Will migrate");
|
||||
// User has provided their own key, but we already have a default key
|
||||
// Decrypt using default key
|
||||
if (credentialsEncrypted) {
|
||||
try {
|
||||
credentials = decryptCredentials(defaultKey,credentials)
|
||||
} catch(err) {
|
||||
credentials = {};
|
||||
log.warn(log._("nodes.credentials.error",{message:err.toString()}))
|
||||
var error = new Error("Failed to decrypt credentials");
|
||||
error.code = "credentials_load_failed";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
dirty = true;
|
||||
removeDefaultKey = true;
|
||||
}
|
||||
} else {
|
||||
log.debug("red/runtime/nodes/credentials.load : no user key present");
|
||||
// User has not provide their own key
|
||||
if (encryptionKeyType !== 'project') {
|
||||
encryptionKeyType = 'system';
|
||||
}
|
||||
encryptionKey = defaultKey;
|
||||
encryptionEnabled = true;
|
||||
if (encryptionKey === undefined) {
|
||||
log.debug("red/runtime/nodes/credentials.load : no default key present - generating one");
|
||||
// No user-provided key, no generated key
|
||||
// Generate a new key
|
||||
defaultKey = crypto.randomBytes(32).toString('hex');
|
||||
try {
|
||||
setupEncryptionPromise = settings.set('_credentialSecret',defaultKey);
|
||||
encryptionKey = crypto.createHash('sha256').update(defaultKey).digest();
|
||||
} catch(err) {
|
||||
log.debug("red/runtime/nodes/credentials.load : settings unavailable - disabling encryption");
|
||||
// Settings unavailable
|
||||
encryptionEnabled = false;
|
||||
encryptionKey = null;
|
||||
}
|
||||
dirty = true;
|
||||
} else {
|
||||
log.debug("red/runtime/nodes/credentials.load : using default key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (encryptionEnabled && !dirty) {
|
||||
encryptedCredentials = credentials;
|
||||
}
|
||||
log.debug("red/runtime/nodes/credentials.load : keyType="+encryptionKeyType);
|
||||
if (encryptionKeyType === 'system') {
|
||||
log.warn(log._("nodes.credentials.system-key-warning"));
|
||||
}
|
||||
return setupEncryptionPromise.then(function() {
|
||||
var clearInvalidFlag = false;
|
||||
if (credentials.hasOwnProperty("$")) {
|
||||
if (encryptionEnabled === false) {
|
||||
// The credentials appear to be encrypted, but our config
|
||||
// thinks they are not.
|
||||
var error = new Error("Failed to decrypt credentials");
|
||||
error.code = "credentials_load_failed";
|
||||
if (activeProject) {
|
||||
// This is a project with a bad key. Mark it as invalid
|
||||
// TODO: this delves too deep into Project structure
|
||||
activeProject.credentialSecretInvalid = true;
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
// These are encrypted credentials
|
||||
try {
|
||||
credentialCache = decryptCredentials(encryptionKey,credentials)
|
||||
clearInvalidFlag = true;
|
||||
} catch(err) {
|
||||
credentialCache = {};
|
||||
dirty = true;
|
||||
log.warn(log._("nodes.credentials.error",{message:err.toString()}))
|
||||
var error = new Error("Failed to decrypt credentials");
|
||||
error.code = "credentials_load_failed";
|
||||
if (activeProject) {
|
||||
// This is a project with a bad key. Mark it as invalid
|
||||
// TODO: this delves too deep into Project structure
|
||||
activeProject.credentialSecretInvalid = true;
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
if (encryptionEnabled) {
|
||||
// Our config expects the credentials to be encrypted but the encrypted object is not found
|
||||
log.warn(log._("nodes.credentials.encryptedNotFound"))
|
||||
credentialCache = credentials;
|
||||
} else {
|
||||
// credentialSecret is set to False
|
||||
log.warn(log._("nodes.credentials.unencrypted"))
|
||||
credentialCache = credentials;
|
||||
}
|
||||
}
|
||||
if (clearInvalidFlag) {
|
||||
// TODO: this delves too deep into Project structure
|
||||
if (activeProject) {
|
||||
delete activeProject.credentialSecretInvalid;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a set of credentials for the given node id.
|
||||
* @param id the node id for the credentials
|
||||
* @param creds an object of credential key/value pairs
|
||||
* @return a promise for backwards compatibility TODO: can this be removed?
|
||||
*/
|
||||
add: async function (id, creds) {
|
||||
if (!credentialCache.hasOwnProperty(id) || JSON.stringify(creds) !== JSON.stringify(credentialCache[id])) {
|
||||
credentialCache[id] = creds;
|
||||
dirty = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the credentials for the given node id.
|
||||
* @param id the node id for the credentials
|
||||
* @return the credentials
|
||||
*/
|
||||
get: function (id) {
|
||||
return credentialCache[id];
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the credentials for the given node id.
|
||||
* @param id the node id for the credentials
|
||||
* @return a promise for the saving of credentials to storage
|
||||
*/
|
||||
delete: function (id) {
|
||||
delete credentialCache[id];
|
||||
dirty = true;
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
credentialCache = {};
|
||||
dirty = true;
|
||||
},
|
||||
/**
|
||||
* Deletes any credentials for nodes that no longer exist
|
||||
* @param config a flow config
|
||||
* @return a promise for the saving of credentials to storage
|
||||
*/
|
||||
clean: async function (config) {
|
||||
var existingIds = {};
|
||||
config.forEach(function(n) {
|
||||
existingIds[n.id] = true;
|
||||
if (n.credentials) {
|
||||
api.extract(n);
|
||||
}
|
||||
});
|
||||
var deletedCredentials = false;
|
||||
for (var c in credentialCache) {
|
||||
if (credentialCache.hasOwnProperty(c)) {
|
||||
if (!existingIds[c]) {
|
||||
deletedCredentials = true;
|
||||
delete credentialCache[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deletedCredentials) {
|
||||
dirty = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a node credential definition.
|
||||
* @param type the node type
|
||||
* @param definition the credential definition
|
||||
*/
|
||||
register: function (type, definition) {
|
||||
var dashedType = type.replace(/\s+/g, '-');
|
||||
credentialsDef[dashedType] = definition;
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts and stores any credential updates in the provided node.
|
||||
* The provided node may have a .credentials property that contains
|
||||
* new credentials for the node.
|
||||
* This function loops through the credentials in the definition for
|
||||
* the node-type and applies any of the updates provided in the node.
|
||||
*
|
||||
* This function does not save the credentials to disk as it is expected
|
||||
* to be called multiple times when a new flow is deployed.
|
||||
*
|
||||
* @param node the node to extract credentials from
|
||||
*/
|
||||
extract: function(node) {
|
||||
var nodeID = node.id;
|
||||
var nodeType = node.type;
|
||||
var cred;
|
||||
var newCreds = node.credentials;
|
||||
if (newCreds) {
|
||||
delete node.credentials;
|
||||
var savedCredentials = credentialCache[nodeID] || {};
|
||||
// Need to check the type of constructor for this node.
|
||||
// - Function : regular node
|
||||
// - !Function: subflow module
|
||||
|
||||
if (/^subflow(:|$)/.test(nodeType) || typeof runtime.nodes.getType(nodeType) !== 'function') {
|
||||
for (cred in newCreds) {
|
||||
if (newCreds.hasOwnProperty(cred)) {
|
||||
if (newCreds[cred] === "__PWRD__") {
|
||||
continue;
|
||||
}
|
||||
if (0 === newCreds[cred].length || /^\s*$/.test(newCreds[cred])) {
|
||||
delete savedCredentials[cred];
|
||||
dirty = true;
|
||||
continue;
|
||||
}
|
||||
if (!savedCredentials.hasOwnProperty(cred) || JSON.stringify(savedCredentials[cred]) !== JSON.stringify(newCreds[cred])) {
|
||||
savedCredentials[cred] = newCreds[cred];
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (/^subflow(:|$)/.test(nodeType)) {
|
||||
for (cred in savedCredentials) {
|
||||
if (savedCredentials.hasOwnProperty(cred)) {
|
||||
if (!newCreds.hasOwnProperty(cred)) {
|
||||
delete savedCredentials[cred];
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (nodeType === "global-config") {
|
||||
savedCredentials.map = savedCredentials.map || {}
|
||||
const existingCredentialKeys = Object.keys(savedCredentials.map)
|
||||
const newCredentialKeys = Object.keys(newCreds?.map || [])
|
||||
existingCredentialKeys.forEach(key => {
|
||||
if (!newCreds.map?.[key]) {
|
||||
// This key doesn't exist in the new credentials list - remove
|
||||
delete savedCredentials.map[key]
|
||||
delete savedCredentials.map[`has_${key}`]
|
||||
dirty = true
|
||||
}
|
||||
})
|
||||
newCredentialKeys.forEach(key => {
|
||||
if (!/^has_/.test(key)) {
|
||||
if (!savedCredentials.map[key] || newCreds.map[key] !== '__PWRD__') {
|
||||
// This key either doesn't exist in current saved, or the
|
||||
// value has been changed
|
||||
savedCredentials.map[key] = newCreds.map[key]
|
||||
savedCredentials.map[`has_${key}`] = newCreds.map[`has_${key}`]
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
var dashedType = nodeType.replace(/\s+/g, '-');
|
||||
var definition = credentialsDef[dashedType];
|
||||
if (!definition) {
|
||||
log.warn(log._("nodes.credentials.not-registered",{type:nodeType}));
|
||||
return;
|
||||
}
|
||||
|
||||
for (cred in definition) {
|
||||
if (definition.hasOwnProperty(cred)) {
|
||||
if (newCreds[cred] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (definition[cred].type == "password" && newCreds[cred] == '__PWRD__') {
|
||||
continue;
|
||||
}
|
||||
if (0 === newCreds[cred].length || /^\s*$/.test(newCreds[cred])) {
|
||||
delete savedCredentials[cred];
|
||||
dirty = true;
|
||||
continue;
|
||||
}
|
||||
if (!savedCredentials.hasOwnProperty(cred) || JSON.stringify(savedCredentials[cred]) !== JSON.stringify(newCreds[cred])) {
|
||||
savedCredentials[cred] = newCreds[cred];
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
credentialCache[nodeID] = savedCredentials;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the credential definition for the given node type
|
||||
* @param type the node type
|
||||
* @return the credential definition
|
||||
*/
|
||||
getDefinition: function (type) {
|
||||
return credentialsDef[type];
|
||||
},
|
||||
|
||||
dirty: function() {
|
||||
return dirty;
|
||||
},
|
||||
setKey: function(key) {
|
||||
if (key) {
|
||||
encryptionKey = crypto.createHash('sha256').update(key).digest();
|
||||
encryptionEnabled = true;
|
||||
dirty = true;
|
||||
encryptionKeyType = "project";
|
||||
} else {
|
||||
encryptionKey = null;
|
||||
encryptionEnabled = false;
|
||||
dirty = true;
|
||||
encryptionKeyType = "disabled";
|
||||
}
|
||||
},
|
||||
getKeyType: function() {
|
||||
return encryptionKeyType;
|
||||
},
|
||||
export: async function() {
|
||||
var result = credentialCache;
|
||||
|
||||
if (encryptionEnabled) {
|
||||
if (dirty) {
|
||||
try {
|
||||
log.debug("red/runtime/nodes/credentials.export : encrypting");
|
||||
result = encryptCredentials(encryptionKey, credentialCache);
|
||||
} catch(err) {
|
||||
log.warn(log._("nodes.credentials.error-saving",{message:err.toString()}))
|
||||
}
|
||||
} else {
|
||||
result = encryptedCredentials;
|
||||
}
|
||||
}
|
||||
dirty = false;
|
||||
if (removeDefaultKey) {
|
||||
log.debug("red/runtime/nodes/credentials.export : removing unused default key");
|
||||
return settings.delete('_credentialSecret').then(function() {
|
||||
removeDefaultKey = false;
|
||||
return result;
|
||||
})
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
|
||||
const jsonClone = require("rfdc")();
|
||||
var util = require("util");
|
||||
|
||||
var registry = require("@node-red/registry");
|
||||
|
||||
var credentials = require("./credentials");
|
||||
var flows = require("../flows");
|
||||
var flowUtil = require("../flows/util")
|
||||
var context = require("./context");
|
||||
var Node = require("./Node");
|
||||
var log;
|
||||
|
||||
const events = require("@node-red/util").events;
|
||||
|
||||
var settings;
|
||||
|
||||
/**
|
||||
* Registers a node constructor
|
||||
* @param nodeSet - the nodeSet providing the node (module/set)
|
||||
* @param type - the string type name
|
||||
* @param constructor - the constructor function for this node type
|
||||
* @param opts - optional additional options for the node
|
||||
*/
|
||||
function registerType(nodeSet,type,constructor,opts) {
|
||||
if (typeof type !== "string") {
|
||||
// This is someone calling the api directly, rather than via the
|
||||
// RED object provided to a node. Log a warning
|
||||
log.warn("["+nodeSet+"] Deprecated call to RED.runtime.nodes.registerType - node-set name must be provided as first argument");
|
||||
opts = constructor;
|
||||
constructor = type;
|
||||
type = nodeSet;
|
||||
nodeSet = "";
|
||||
}
|
||||
if (opts) {
|
||||
if (opts.credentials) {
|
||||
credentials.register(type,opts.credentials);
|
||||
}
|
||||
if (opts.settings) {
|
||||
try {
|
||||
settings.registerNodeSettings(type,opts.settings);
|
||||
} catch(err) {
|
||||
log.warn("["+type+"] "+err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!(constructor.prototype instanceof Node)) {
|
||||
if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
|
||||
util.inherits(constructor,Node);
|
||||
} else {
|
||||
var proto = constructor.prototype;
|
||||
while(Object.getPrototypeOf(proto) !== Object.prototype) {
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
//TODO: This is a partial implementation of util.inherits >= node v5.0.0
|
||||
// which should be changed when support for node < v5.0.0 is dropped
|
||||
// see: https://github.com/nodejs/node/pull/3455
|
||||
proto.constructor.super_ = Node;
|
||||
if(Object.setPrototypeOf) {
|
||||
Object.setPrototypeOf(proto, Node.prototype);
|
||||
} else {
|
||||
// hack for node v0.10
|
||||
proto.__proto__ = Node.prototype;
|
||||
}
|
||||
}
|
||||
}
|
||||
registry.registerType(nodeSet,type,constructor,opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from a Node's constructor function, invokes the super-class
|
||||
* constructor and attaches any credentials to the node.
|
||||
* @param node the node object being created
|
||||
* @param def the instance definition for the node
|
||||
*/
|
||||
function createNode(node,def) {
|
||||
Node.call(node,def);
|
||||
var id = node.id;
|
||||
if (def._alias) {
|
||||
id = def._alias;
|
||||
}
|
||||
var creds = credentials.get(id);
|
||||
if (creds) {
|
||||
creds = jsonClone(creds);
|
||||
//console.log("Attaching credentials to ",node.id);
|
||||
// allow $(foo) syntax to substitute env variables for credentials also...
|
||||
for (var p in creds) {
|
||||
if (creds.hasOwnProperty(p)) {
|
||||
flowUtil.mapEnvVarProperties(creds,p,node._flow,node);
|
||||
}
|
||||
}
|
||||
node.credentials = creds;
|
||||
} else if (credentials.getDefinition(node.type)) {
|
||||
node.credentials = {};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
credentials.init(runtime);
|
||||
flows.init(runtime);
|
||||
registry.init(runtime);
|
||||
context.init(runtime.settings);
|
||||
}
|
||||
|
||||
function disableNode(id) {
|
||||
flows.checkTypeInUse(id);
|
||||
return registry.disableNode(id).then(function(info) {
|
||||
reportNodeStateChange(info,false);
|
||||
return info;
|
||||
});
|
||||
}
|
||||
|
||||
function enableNode(id) {
|
||||
return registry.enableNode(id).then(function(info) {
|
||||
reportNodeStateChange(info,true);
|
||||
return info;
|
||||
});
|
||||
}
|
||||
|
||||
function reportNodeStateChange(info,enabled) {
|
||||
if (info.enabled === enabled && !info.err) {
|
||||
events.emit("runtime-event",{id:"node/"+(enabled?"enabled":"disabled"),retain:false,payload:info});
|
||||
log.info(" "+log._("api.nodes."+(enabled?"enabled":"disabled")));
|
||||
for (var i=0;i<info.types.length;i++) {
|
||||
log.info(" - "+info.types[i]);
|
||||
}
|
||||
} else if (enabled && info.err) {
|
||||
log.warn(log._("api.nodes.error-enable"));
|
||||
log.warn(" - "+info.name+" : "+info.err);
|
||||
}
|
||||
}
|
||||
|
||||
function installModule(module,version,url) {
|
||||
return registry.installModule(module,version,url).then(function(info) {
|
||||
if (info.pending_version) {
|
||||
events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:info.name,version:info.pending_version}});
|
||||
} else {
|
||||
if (!info.nodes.length && info.plugins.length) {
|
||||
events.emit("runtime-event",{id:"plugin/added",retain:false,payload:info.plugins});
|
||||
} else {
|
||||
events.emit("runtime-event",{id:"node/added",retain:false,payload:info.nodes});
|
||||
}
|
||||
}
|
||||
return info;
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallModule(module) {
|
||||
var info = registry.getModuleInfo(module);
|
||||
if (!info || !info.user) {
|
||||
throw new Error(log._("nodes.index.unrecognised-module", {module:module}));
|
||||
} else {
|
||||
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});
|
||||
return list;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// Lifecycle
|
||||
init: init,
|
||||
load: registry.load,
|
||||
|
||||
// Node registry
|
||||
createNode: createNode,
|
||||
getNode: flows.get,
|
||||
eachNode: flows.eachNode,
|
||||
getContext: context.get,
|
||||
|
||||
clearContext: context.clear,
|
||||
|
||||
installerEnabled: registry.installerEnabled,
|
||||
installModule: installModule,
|
||||
uninstallModule: uninstallModule,
|
||||
|
||||
enableNode: enableNode,
|
||||
disableNode: disableNode,
|
||||
|
||||
// Node type registry
|
||||
registerType: registerType,
|
||||
registerSubflow: registerSubflow,
|
||||
getType: registry.get,
|
||||
|
||||
getNodeInfo: registry.getNodeInfo,
|
||||
getNodeList: registry.getNodeList,
|
||||
|
||||
getModuleInfo: registry.getModuleInfo,
|
||||
|
||||
getNodeConfigs: registry.getNodeConfigs,
|
||||
getNodeConfig: registry.getNodeConfig,
|
||||
getNodeIconPath: registry.getNodeIconPath,
|
||||
getNodeIcons: registry.getNodeIcons,
|
||||
getNodeExampleFlows: registry.getNodeExampleFlows,
|
||||
getNodeExampleFlowPath: registry.getNodeExampleFlowPath,
|
||||
getModuleResource: registry.getModuleResource,
|
||||
|
||||
clearRegistry: registry.clear,
|
||||
cleanModuleList: registry.cleanModuleList,
|
||||
|
||||
// Flow handling
|
||||
loadFlows: flows.load,
|
||||
startFlows: flows.startFlows,
|
||||
stopFlows: flows.stopFlows,
|
||||
setFlows: flows.setFlows,
|
||||
getFlows: flows.getFlows,
|
||||
|
||||
addFlow: flows.addFlow,
|
||||
getFlow: flows.getFlow,
|
||||
updateFlow: flows.updateFlow,
|
||||
removeFlow: flows.removeFlow,
|
||||
// disableFlow: flows.disableFlow,
|
||||
// enableFlow: flows.enableFlow,
|
||||
|
||||
// Credentials
|
||||
addCredentials: credentials.add,
|
||||
getCredentials: credentials.get,
|
||||
deleteCredentials: credentials.delete,
|
||||
getCredentialDefinition: credentials.getDefinition,
|
||||
setCredentialSecret: credentials.setKey,
|
||||
clearCredentials: credentials.clear,
|
||||
exportCredentials: credentials.export,
|
||||
getCredentialKeyType: credentials.getKeyType,
|
||||
|
||||
// Contexts
|
||||
loadContextsPlugin: context.load,
|
||||
closeContextsPlugin: context.close,
|
||||
listContextStores: context.listStores,
|
||||
};
|
||||
Reference in New Issue
Block a user