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

Merge branch '0.19' of https://github.com/node-red/node-red into api-access-token

This commit is contained in:
Hideki Nakamura 2018-07-10 09:33:25 -07:00
commit 42188b9f49
24 changed files with 1207 additions and 579 deletions

View File

@ -24,6 +24,10 @@ module.exports = function(grunt) {
nodemonArgs.push(flowFile); nodemonArgs.push(flowFile);
} }
var nonHeadless = grunt.option('non-headless');
if (nonHeadless) {
process.env.NODE_RED_NON_HEADLESS = 'true';
}
grunt.initConfig({ grunt.initConfig({
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
paths: { paths: {

View File

@ -15,16 +15,11 @@
**/ **/
(function($) { (function($) {
var contextParse = function(v) { var contextParse = function(v) {
var parts = {}; var parts = RED.utils.parseContextKey(v);
var m = /^#:\((\S+?)\)::(.*)$/.exec(v); return {
if (m) { option: parts.store,
parts.option = m[1]; value: parts.key
parts.value = m[2];
} else {
parts.value = v;
parts.option = RED.settings.context.default;
} }
return parts;
} }
var contextExport = function(v,opt) { var contextExport = function(v,opt) {
if (!opt) { if (!opt) {

View File

@ -828,6 +828,20 @@ RED.utils = (function() {
return payload; return payload;
} }
function parseContextKey(key) {
var parts = {};
var m = /^#:\((\S+?)\)::(.*)$/.exec(key);
if (m) {
parts.store = m[1];
parts.key = m[2];
} else {
parts.key = key;
if (RED.settings.context) {
parts.store = RED.settings.context.default;
}
}
return parts;
}
return { return {
createObjectElement: buildMessageElement, createObjectElement: buildMessageElement,
@ -839,6 +853,7 @@ RED.utils = (function() {
getNodeIcon: getNodeIcon, getNodeIcon: getNodeIcon,
getNodeLabel: getNodeLabel, getNodeLabel: getNodeLabel,
addSpinnerOverlay: addSpinnerOverlay, addSpinnerOverlay: addSpinnerOverlay,
decodeObject: decodeObject decodeObject: decodeObject,
parseContextKey: parseContextKey
} }
})(); })();

View File

@ -63,21 +63,33 @@ module.exports = function(RED) {
} }
this.on("input",function(msg) { this.on("input",function(msg) {
try { msg.topic = this.topic;
msg.topic = this.topic; if (this.payloadType !== 'flow' && this.payloadType !== 'global') {
if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") { try {
msg.payload = Date.now(); if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") {
} else if (this.payloadType == null) { msg.payload = Date.now();
msg.payload = this.payload; } else if (this.payloadType == null) {
} else if (this.payloadType === 'none') { msg.payload = this.payload;
msg.payload = ""; } else if (this.payloadType === 'none') {
} else { msg.payload = "";
msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg); } else {
msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg);
}
this.send(msg);
msg = null;
} catch(err) {
this.error(err,msg);
} }
this.send(msg); } else {
msg = null; RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg, function(err,res) {
} catch(err) { if (err) {
this.error(err,msg); node.error(err,msg);
} else {
msg.payload = res;
node.send(msg);
}
});
} }
}); });
} }

View File

@ -76,8 +76,43 @@ module.exports = function(RED) {
var node = this; var node = this;
node.topics = {}; node.topics = {};
this.on("input", function(msg) { var pendingMessages = [];
var activeMessagePromise = null;
var processMessageQueue = function(msg) {
if (msg) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
if (activeMessagePromise !== null) {
// The node is currently processing a message, so do nothing
// more with this message
return;
}
}
if (pendingMessages.length === 0) {
// There are no more messages to process, clear the active flag
// and return
activeMessagePromise = null;
return;
}
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
activeMessagePromise = processMessage(nextMsg)
.then(processMessageQueue)
.catch((err) => {
node.error(err,nextMsg);
return processMessageQueue();
});
}
this.on('input', function(msg) {
processMessageQueue(msg);
});
var processMessage = function(msg) {
var topic = msg.topic || "_none"; var topic = msg.topic || "_none";
var promise;
if (node.bytopic === "all") { topic = "_none"; } if (node.bytopic === "all") { topic = "_none"; }
node.topics[topic] = node.topics[topic] || {}; node.topics[topic] = node.topics[topic] || {};
if (msg.hasOwnProperty("reset") || ((node.reset !== '') && msg.hasOwnProperty("payload") && (msg.payload !== null) && msg.payload.toString && (msg.payload.toString() == node.reset)) ) { if (msg.hasOwnProperty("reset") || ((node.reset !== '') && msg.hasOwnProperty("payload") && (msg.payload !== null) && msg.payload.toString && (msg.payload.toString() == node.reset)) ) {
@ -88,48 +123,88 @@ module.exports = function(RED) {
} }
else { else {
if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) { if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) {
promise = Promise.resolve();
if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); } else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); }
else if (node.op2type !== "nul") { else if (node.op2type !== "nul") {
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); promise = new Promise((resolve,reject) => {
} RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) {
if (node.op1type === "pay") { } reject(err);
else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); } } else {
else if (node.op1type !== "nul") { node.topics[topic].m2 = value;
msg.payload = RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg); resolve();
}
if (node.duration === 0) { node.topics[topic].tout = 0; }
else if (node.loop === true) {
/* istanbul ignore else */
if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); }
/* istanbul ignore else */
if (node.op1type !== "nul") {
var msg2 = RED.util.cloneMessage(msg);
node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration);
}
}
else {
if (!node.topics[topic].tout) {
node.topics[topic].tout = setTimeout(function() {
var msg2 = null;
if (node.op2type !== "nul") {
msg2 = RED.util.cloneMessage(msg);
if (node.op2type === "flow" || node.op2type === "global") {
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg);
}
msg2.payload = node.topics[topic].m2;
delete node.topics[topic];
node.send(msg2);
} }
else { delete node.topics[topic]; } });
node.status({}); });
}, node.duration);
}
} }
node.status({fill:"blue",shape:"dot",text:" "});
if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } return promise.then(() => {
promise = Promise.resolve();
if (node.op1type === "pay") { }
else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); }
else if (node.op1type !== "nul") {
promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg,(err,value) => {
if (err) {
reject(err);
} else {
msg.payload = value;
resolve();
}
});
});
}
return promise.then(() => {
if (node.duration === 0) { node.topics[topic].tout = 0; }
else if (node.loop === true) {
/* istanbul ignore else */
if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); }
/* istanbul ignore else */
if (node.op1type !== "nul") {
var msg2 = RED.util.cloneMessage(msg);
node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration);
}
}
else {
if (!node.topics[topic].tout) {
node.topics[topic].tout = setTimeout(function() {
var msg2 = null;
if (node.op2type !== "nul") {
var promise = Promise.resolve();
msg2 = RED.util.cloneMessage(msg);
if (node.op2type === "flow" || node.op2type === "global") {
promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) {
reject(err);
} else {
node.topics[topic].m2 = value;
resolve();
}
});
});
}
promise.then(() => {
msg2.payload = node.topics[topic].m2;
delete node.topics[topic];
node.send(msg2);
node.status({});
}).catch(err => {
node.error(err);
});
} else {
delete node.topics[topic];
node.status({});
}
}, node.duration);
}
}
node.status({fill:"blue",shape:"dot",text:" "});
if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); }
});
});
} }
else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) { else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) {
/* istanbul ignore else */ /* istanbul ignore else */
@ -138,25 +213,43 @@ module.exports = function(RED) {
if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); } if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); }
node.topics[topic].tout = setTimeout(function() { node.topics[topic].tout = setTimeout(function() {
var msg2 = null; var msg2 = null;
var promise = Promise.resolve();
if (node.op2type !== "nul") { if (node.op2type !== "nul") {
if (node.op2type === "flow" || node.op2type === "global") { if (node.op2type === "flow" || node.op2type === "global") {
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); promise = new Promise((resolve,reject) => {
} RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (node.topics[topic] !== undefined) { if (err) {
msg2 = RED.util.cloneMessage(msg); reject(err);
msg2.payload = node.topics[topic].m2; } else {
node.topics[topic].m2 = value;
resolve();
}
});
});
} }
} }
delete node.topics[topic]; promise.then(() => {
node.status({}); if (node.op2type !== "nul") {
node.send(msg2); if (node.topics[topic] !== undefined) {
msg2 = RED.util.cloneMessage(msg);
msg2.payload = node.topics[topic].m2;
}
}
delete node.topics[topic];
node.status({});
node.send(msg2);
}).catch(err => {
node.error(err);
});
}, node.duration); }, node.duration);
} }
else { else {
if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
} }
} }
}); return Promise.resolve();
}
this.on("close", function() { this.on("close", function() {
for (var t in node.topics) { for (var t in node.topics) {
/* istanbul ignore else */ /* istanbul ignore else */

View File

@ -23,10 +23,6 @@ from time import sleep
bounce = 25; bounce = 25;
if sys.version_info >= (3,0):
print("Sorry - currently only configured to work with python 2.x")
sys.exit(1)
if len(sys.argv) > 2: if len(sys.argv) > 2:
cmd = sys.argv[1].lower() cmd = sys.argv[1].lower()
pin = int(sys.argv[2]) pin = int(sys.argv[2])
@ -34,7 +30,7 @@ if len(sys.argv) > 2:
GPIO.setwarnings(False) GPIO.setwarnings(False)
if cmd == "pwm": if cmd == "pwm":
#print "Initialised pin "+str(pin)+" to PWM" #print("Initialised pin "+str(pin)+" to PWM")
try: try:
freq = int(sys.argv[3]) freq = int(sys.argv[3])
except: except:
@ -54,10 +50,10 @@ if len(sys.argv) > 2:
GPIO.cleanup(pin) GPIO.cleanup(pin)
sys.exit(0) sys.exit(0)
except Exception as ex: except Exception as ex:
print "bad data: "+data print("bad data: "+data)
elif cmd == "buzz": elif cmd == "buzz":
#print "Initialised pin "+str(pin)+" to Buzz" #print("Initialised pin "+str(pin)+" to Buzz")
GPIO.setup(pin,GPIO.OUT) GPIO.setup(pin,GPIO.OUT)
p = GPIO.PWM(pin, 100) p = GPIO.PWM(pin, 100)
p.stop() p.stop()
@ -76,10 +72,10 @@ if len(sys.argv) > 2:
GPIO.cleanup(pin) GPIO.cleanup(pin)
sys.exit(0) sys.exit(0)
except Exception as ex: except Exception as ex:
print "bad data: "+data print("bad data: "+data)
elif cmd == "out": elif cmd == "out":
#print "Initialised pin "+str(pin)+" to OUT" #print("Initialised pin "+str(pin)+" to OUT")
GPIO.setup(pin,GPIO.OUT) GPIO.setup(pin,GPIO.OUT)
if len(sys.argv) == 4: if len(sys.argv) == 4:
GPIO.output(pin,int(sys.argv[3])) GPIO.output(pin,int(sys.argv[3]))
@ -103,11 +99,11 @@ if len(sys.argv) > 2:
GPIO.output(pin,data) GPIO.output(pin,data)
elif cmd == "in": elif cmd == "in":
#print "Initialised pin "+str(pin)+" to IN" #print("Initialised pin "+str(pin)+" to IN")
bounce = float(sys.argv[4]) bounce = float(sys.argv[4])
def handle_callback(chan): def handle_callback(chan):
sleep(bounce/1000.0) sleep(bounce/1000.0)
print GPIO.input(chan) print(GPIO.input(chan))
if sys.argv[3].lower() == "up": if sys.argv[3].lower() == "up":
GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP) GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP)
@ -116,7 +112,7 @@ if len(sys.argv) > 2:
else: else:
GPIO.setup(pin,GPIO.IN) GPIO.setup(pin,GPIO.IN)
print GPIO.input(pin) print(GPIO.input(pin))
GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=int(bounce)) GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=int(bounce))
while True: while True:
@ -129,7 +125,7 @@ if len(sys.argv) > 2:
sys.exit(0) sys.exit(0)
elif cmd == "byte": elif cmd == "byte":
#print "Initialised BYTE mode - "+str(pin)+ #print("Initialised BYTE mode - "+str(pin)+)
list = [7,11,13,12,15,16,18,22] list = [7,11,13,12,15,16,18,22]
GPIO.setup(list,GPIO.OUT) GPIO.setup(list,GPIO.OUT)
@ -152,7 +148,7 @@ if len(sys.argv) > 2:
GPIO.output(list[bit], data & mask) GPIO.output(list[bit], data & mask)
elif cmd == "borg": elif cmd == "borg":
#print "Initialised BORG mode - "+str(pin)+ #print("Initialised BORG mode - "+str(pin)+)
GPIO.setup(11,GPIO.OUT) GPIO.setup(11,GPIO.OUT)
GPIO.setup(13,GPIO.OUT) GPIO.setup(13,GPIO.OUT)
GPIO.setup(15,GPIO.OUT) GPIO.setup(15,GPIO.OUT)
@ -190,7 +186,7 @@ if len(sys.argv) > 2:
button = ord( buf[0] ) & pin # mask out just the required button(s) button = ord( buf[0] ) & pin # mask out just the required button(s)
if button != oldbutt: # only send if changed if button != oldbutt: # only send if changed
oldbutt = button oldbutt = button
print button print(button)
while True: while True:
try: try:
@ -215,7 +211,7 @@ if len(sys.argv) > 2:
# type,code,value # type,code,value
print("%u,%u" % (code, value)) print("%u,%u" % (code, value))
event = file.read(EVENT_SIZE) event = file.read(EVENT_SIZE)
print "0,0" print("0,0")
file.close() file.close()
sys.exit(0) sys.exit(0)
except: except:
@ -225,14 +221,14 @@ if len(sys.argv) > 2:
elif len(sys.argv) > 1: elif len(sys.argv) > 1:
cmd = sys.argv[1].lower() cmd = sys.argv[1].lower()
if cmd == "rev": if cmd == "rev":
print GPIO.RPI_REVISION print(GPIO.RPI_REVISION)
elif cmd == "ver": elif cmd == "ver":
print GPIO.VERSION print(GPIO.VERSION)
elif cmd == "info": elif cmd == "info":
print GPIO.RPI_INFO print(GPIO.RPI_INFO)
else: else:
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}")
print " only ver (gpio version) and info (board information) accept no pin parameter." print(" only ver (gpio version) and info (board information) accept no pin parameter.")
else: else:
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}")

View File

@ -697,7 +697,9 @@
"errors": { "errors": {
"dropped-object": "Ignored non-object payload", "dropped-object": "Ignored non-object payload",
"dropped": "Ignored unsupported payload type", "dropped": "Ignored unsupported payload type",
"dropped-error": "Failed to convert payload" "dropped-error": "Failed to convert payload",
"schema-error": "JSON Schema error",
"schema-error-compile": "JSON Schema error: failed to compile schema"
}, },
"label": { "label": {
"o2j": "Object to JSON options", "o2j": "Object to JSON options",
@ -924,8 +926,8 @@
"ascending" : "ascending", "ascending" : "ascending",
"descending" : "descending", "descending" : "descending",
"as-number" : "as number", "as-number" : "as number",
"invalid-exp" : "invalid JSONata expression in sort node", "invalid-exp" : "Invalid JSONata expression in sort node: __message__",
"too-many" : "too many pending messages in sort node", "too-many" : "Too many pending messages in sort node",
"clear" : "clear pending message in sort node" "clear" : "clear pending message in sort node"
}, },
"batch" : { "batch" : {

View File

@ -99,11 +99,17 @@
} }
return v; return v;
} }
function prop2name(key) {
var result = RED.utils.parseContextKey(key);
return result.key;
}
function getValueLabel(t,v) { function getValueLabel(t,v) {
if (t === 'str') { if (t === 'str') {
return '"'+clipValueLength(v)+'"'; return '"'+clipValueLength(v)+'"';
} else if (t === 'msg' || t==='flow' || t==='global') { } else if (t === 'msg') {
return t+"."+clipValueLength(v); return t+"."+clipValueLength(v);
} else if (t === 'flow' || t === 'global') {
return t+"."+clipValueLength(prop2name(v));
} }
return clipValueLength(v); return clipValueLength(v);
} }

View File

@ -59,21 +59,157 @@ module.exports = function(RED) {
'else': function(a) { return a === true; } 'else': function(a) { return a === true; }
}; };
var _max_kept_msgs_count = undefined; var _maxKeptCount;
function max_kept_msgs_count(node) { function getMaxKeptCount() {
if (_max_kept_msgs_count === undefined) { if (_maxKeptCount === undefined) {
var name = "nodeMessageBufferMaxLength"; var name = "nodeMessageBufferMaxLength";
if (RED.settings.hasOwnProperty(name)) { if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name]; _maxKeptCount = RED.settings[name];
} }
else { else {
_max_kept_msgs_count = 0; _maxKeptCount = 0;
} }
} }
return _max_kept_msgs_count; return _maxKeptCount;
} }
function getProperty(node,msg) {
return new Promise((resolve,reject) => {
if (node.propertyType === 'jsonata') {
RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => {
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else {
RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
}
});
}
function getV1(node,msg,rule,hasParts) {
return new Promise( (resolve,reject) => {
if (rule.vt === 'prev') {
resolve(node.previousValue);
} else if (rule.vt === 'jsonata') {
var exp = rule.v;
if (rule.t === 'jsonata_exp') {
if (hasParts) {
exp.assign("I", msg.parts.index);
exp.assign("N", msg.parts.count);
}
}
RED.util.evaluateJSONataExpression(exp,msg,(err,value) => {
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else if (rule.vt === 'json') {
resolve("json");
} else if (rule.vt === 'null') {
resolve("null");
} else {
RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
}
});
}
function getV2(node,msg,rule) {
return new Promise((resolve,reject) => {
var v2 = rule.v2;
if (rule.v2t === 'prev') {
resolve(node.previousValue);
} else if (rule.v2t === 'jsonata') {
RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => {
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else if (typeof v2 !== 'undefined') {
RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
} else {
resolve(v2);
}
})
}
function applyRule(node, msg, property, state) {
return new Promise((resolve,reject) => {
var rule = node.rules[state.currentRule];
var v1,v2;
getV1(node,msg,rule,state.hasParts).then(value => {
v1 = value;
}).then(()=>getV2(node,msg,rule)).then(value => {
v2 = value;
}).then(() => {
if (rule.t == "else") {
property = state.elseflag;
state.elseflag = true;
}
if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) {
state.onward.push(msg);
state.elseflag = false;
if (node.checkall == "false") {
return resolve(false);
}
} else {
state.onward.push(null);
}
resolve(state.currentRule < node.rules.length - 1);
});
})
}
function applyRules(node, msg, property,state) {
if (!state) {
state = {
currentRule: 0,
elseflag: true,
onward: [],
hasParts: msg.hasOwnProperty("parts") &&
msg.parts.hasOwnProperty("id") &&
msg.parts.hasOwnProperty("index")
}
}
return applyRule(node,msg,property,state).then(hasMore => {
if (hasMore) {
state.currentRule++;
return applyRules(node,msg,property,state);
} else {
node.previousValue = property;
return state.onward;
}
});
}
function SwitchNode(n) { function SwitchNode(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
this.rules = n.rules || []; this.rules = n.rules || [];
@ -94,10 +230,10 @@ module.exports = function(RED) {
var node = this; var node = this;
var valid = true; var valid = true;
var repair = n.repair; var repair = n.repair;
var needs_count = repair; var needsCount = repair;
for (var i=0; i<this.rules.length; i+=1) { for (var i=0; i<this.rules.length; i+=1) {
var rule = this.rules[i]; var rule = this.rules[i];
needs_count = needs_count || ((rule.t === "tail") || (rule.t === "jsonata_exp")); needsCount = needsCount || ((rule.t === "tail") || (rule.t === "jsonata_exp"));
if (!rule.vt) { if (!rule.vt) {
if (!isNaN(Number(rule.v))) { if (!isNaN(Number(rule.v))) {
rule.vt = 'num'; rule.vt = 'num';
@ -142,26 +278,26 @@ module.exports = function(RED) {
return; return;
} }
var pending_count = 0; var pendingCount = 0;
var pending_id = 0; var pendingId = 0;
var pending_in = {}; var pendingIn = {};
var pending_out = {}; var pendingOut = {};
var received = {}; var received = {};
function add2group_in(id, msg, parts) { function addMessageToGroup(id, msg, parts) {
if (!(id in pending_in)) { if (!(id in pendingIn)) {
pending_in[id] = { pendingIn[id] = {
count: undefined, count: undefined,
msgs: [], msgs: [],
seq_no: pending_id++ seq_no: pendingId++
}; };
} }
var group = pending_in[id]; var group = pendingIn[id];
group.msgs.push(msg); group.msgs.push(msg);
pending_count++; pendingCount++;
var max_msgs = max_kept_msgs_count(node); var max_msgs = getMaxKeptCount();
if ((max_msgs > 0) && (pending_count > max_msgs)) { if ((max_msgs > 0) && (pendingCount > max_msgs)) {
clear_pending(); clearPending();
node.error(RED._("switch.errors.too-many"), msg); node.error(RED._("switch.errors.too-many"), msg);
} }
if (parts.hasOwnProperty("count")) { if (parts.hasOwnProperty("count")) {
@ -170,32 +306,29 @@ module.exports = function(RED) {
return group; return group;
} }
function del_group_in(id, group) {
pending_count -= group.msgs.length;
delete pending_in[id];
}
function add2pending_in(msg) { function addMessageToPending(msg) {
var parts = msg.parts; var parts = msg.parts;
if (parts.hasOwnProperty("id") && // We've already checked the msg.parts has the require bits
parts.hasOwnProperty("index")) { var group = addMessageToGroup(parts.id, msg, parts);
var group = add2group_in(parts.id, msg, parts); var msgs = group.msgs;
var msgs = group.msgs; var count = group.count;
var count = group.count; if (count === msgs.length) {
if (count === msgs.length) { // We have a complete group - send the individual parts
for (var i = 0; i < msgs.length; i++) { return msgs.reduce((promise, msg) => {
var msg = msgs[i]; return promise.then((result) => {
msg.parts.count = count; msg.parts.count = count;
process_msg(msg, false); return processMessage(msg, false);
} })
del_group_in(parts.id, group); }, Promise.resolve()).then( () => {
} pendingCount -= group.msgs.length;
return true; delete pendingIn[parts.id];
});
} }
return false; return Promise.resolve();
} }
function send_group(onwards, port_count) { function sendGroup(onwards, port_count) {
var counts = new Array(port_count).fill(0); var counts = new Array(port_count).fill(0);
for (var i = 0; i < onwards.length; i++) { for (var i = 0; i < onwards.length; i++) {
var onward = onwards[i]; var onward = onwards[i];
@ -230,141 +363,104 @@ module.exports = function(RED) {
} }
} }
function send2ports(onward, msg) { function sendGroupMessages(onward, msg) {
var parts = msg.parts; var parts = msg.parts;
var gid = parts.id; var gid = parts.id;
received[gid] = ((gid in received) ? received[gid] : 0) +1; received[gid] = ((gid in received) ? received[gid] : 0) +1;
var send_ok = (received[gid] === parts.count); var send_ok = (received[gid] === parts.count);
if (!(gid in pending_out)) { if (!(gid in pendingOut)) {
pending_out[gid] = { pendingOut[gid] = {
onwards: [] onwards: []
}; };
} }
var group = pending_out[gid]; var group = pendingOut[gid];
var onwards = group.onwards; var onwards = group.onwards;
onwards.push(onward); onwards.push(onward);
pending_count++; pendingCount++;
if (send_ok) { if (send_ok) {
send_group(onwards, onward.length, msg); sendGroup(onwards, onward.length, msg);
pending_count -= onward.length; pendingCount -= onward.length;
delete pending_out[gid]; delete pendingOut[gid];
delete received[gid]; delete received[gid];
} }
var max_msgs = max_kept_msgs_count(node); var max_msgs = getMaxKeptCount();
if ((max_msgs > 0) && (pending_count > max_msgs)) { if ((max_msgs > 0) && (pendingCount > max_msgs)) {
clear_pending(); clearPending();
node.error(RED._("switch.errors.too-many"), msg); node.error(RED._("switch.errors.too-many"), msg);
} }
} }
function msg_has_parts(msg) {
if (msg.hasOwnProperty("parts")) {
var parts = msg.parts;
return (parts.hasOwnProperty("id") &&
parts.hasOwnProperty("index")); function processMessage(msg, checkParts) {
var hasParts = msg.hasOwnProperty("parts") &&
msg.parts.hasOwnProperty("id") &&
msg.parts.hasOwnProperty("index");
if (needsCount && checkParts && hasParts) {
return addMessageToPending(msg);
} }
return false; return getProperty(node,msg)
.then(property => applyRules(node,msg,property))
.then(onward => {
if (!repair || !hasParts) {
node.send(onward);
}
else {
sendGroupMessages(onward, msg);
}
}).catch(err => {
node.warn(err);
});
} }
function process_msg(msg, check_parts) { function clearPending() {
var has_parts = msg_has_parts(msg); pendingCount = 0;
if (needs_count && check_parts && has_parts && pendingId = 0;
add2pending_in(msg)) { pendingIn = {};
return; pendingOut = {};
}
var onward = [];
try {
var prop;
if (node.propertyType === 'jsonata') {
prop = RED.util.evaluateJSONataExpression(node.property,msg);
} else {
prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg);
}
var elseflag = true;
for (var i=0; i<node.rules.length; i+=1) {
var rule = node.rules[i];
var test = prop;
var v1,v2;
if (rule.vt === 'prev') {
v1 = node.previousValue;
} else if (rule.vt === 'jsonata') {
try {
var exp = rule.v;
if (rule.t === 'jsonata_exp') {
if (has_parts) {
exp.assign("I", msg.parts.index);
exp.assign("N", msg.parts.count);
}
}
v1 = RED.util.evaluateJSONataExpression(exp,msg);
} catch(err) {
node.error(RED._("switch.errors.invalid-expr",{error:err.message}));
return;
}
} else if (rule.vt === 'json') {
v1 = "json";
} else if (rule.vt === 'null') {
v1 = "null";
} else {
try {
v1 = RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg);
} catch(err) {
v1 = undefined;
}
}
v2 = rule.v2;
if (rule.v2t === 'prev') {
v2 = node.previousValue;
} else if (rule.v2t === 'jsonata') {
try {
v2 = RED.util.evaluateJSONataExpression(rule.v2,msg);
} catch(err) {
node.error(RED._("switch.errors.invalid-expr",{error:err.message}));
return;
}
} else if (typeof v2 !== 'undefined') {
try {
v2 = RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg);
} catch(err) {
v2 = undefined;
}
}
if (rule.t == "else") { test = elseflag; elseflag = true; }
if (operators[rule.t](test,v1,v2,rule.case,msg.parts)) {
onward.push(msg);
elseflag = false;
if (node.checkall == "false") { break; }
} else {
onward.push(null);
}
}
node.previousValue = prop;
if (!repair || !has_parts) {
node.send(onward);
}
else {
send2ports(onward, msg);
}
} catch(err) {
node.warn(err);
}
}
function clear_pending() {
pending_count = 0;
pending_id = 0;
pending_in = {};
pending_out = {};
received = {}; received = {};
} }
var pendingMessages = [];
var activeMessagePromise = null;
var processMessageQueue = function(msg) {
if (msg) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
if (activeMessagePromise !== null) {
// The node is currently processing a message, so do nothing
// more with this message
return;
}
}
if (pendingMessages.length === 0) {
// There are no more messages to process, clear the active flag
// and return
activeMessagePromise = null;
return;
}
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
activeMessagePromise = processMessage(nextMsg,true)
.then(processMessageQueue)
.catch((err) => {
node.error(err,nextMsg);
return processMessageQueue();
});
}
this.on('input', function(msg) { this.on('input', function(msg) {
process_msg(msg, true); processMessageQueue(msg);
}); });
this.on('close', function() { this.on('close', function() {
clear_pending(); clearPending();
}); });
} }

View File

@ -54,6 +54,10 @@
outputs: 1, outputs: 1,
icon: "swap.png", icon: "swap.png",
label: function() { label: function() {
function prop2name(type, key) {
var result = RED.utils.parseContextKey(key);
return type +"." +result.key;
}
if (this.name) { if (this.name) {
return this.name; return this.name;
} }
@ -70,13 +74,13 @@
} else { } else {
if (this.rules.length == 1) { if (this.rules.length == 1) {
if (this.rules[0].t === "set") { if (this.rules[0].t === "set") {
return this._("change.label.set",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.set",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} else if (this.rules[0].t === "change") { } else if (this.rules[0].t === "change") {
return this._("change.label.change",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.change",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} else if (this.rules[0].t === "move") { } else if (this.rules[0].t === "move") {
return this._("change.label.move",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.move",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} else { } else {
return this._("change.label.delete",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.delete",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} }
} else { } else {
return this._("change.label.changeCount",{count:this.rules.length}); return this._("change.label.changeCount",{count:this.rules.length});

View File

@ -98,44 +98,53 @@ module.exports = function(RED) {
} }
} }
function applyRule(msg,rule) { function getToValue(msg,rule) {
try { var value = rule.to;
var property = rule.p; if (rule.tot === 'json') {
var value = rule.to; value = JSON.parse(rule.to);
if (rule.tot === 'json') { } else if (rule.tot === 'bin') {
value = JSON.parse(rule.to); value = Buffer.from(JSON.parse(rule.to))
} else if (rule.tot === 'bin') { }
value = Buffer.from(JSON.parse(rule.to)) if (rule.tot === "msg") {
} value = RED.util.getMessageProperty(msg,rule.to);
var current; } else if (rule.tot === 'flow') {
var fromValue; value = node.context().flow.get(rule.to);
var fromType; } else if (rule.tot === 'global') {
var fromRE; value = node.context().global.get(rule.to);
if (rule.tot === "msg") { } else if (rule.tot === 'date') {
value = RED.util.getMessageProperty(msg,rule.to); value = Date.now();
} else if (rule.tot === 'flow') { } else if (rule.tot === 'jsonata') {
value = node.context().flow.get(rule.to); return new Promise((resolve,reject) => {
} else if (rule.tot === 'global') { RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
value = node.context().global.get(rule.to); if (err) {
} else if (rule.tot === 'date') { reject(RED._("change.errors.invalid-expr",{error:err.message}))
value = Date.now(); } else {
} else if (rule.tot === 'jsonata') { resolve(value);
try{
value = RED.util.evaluateJSONataExpression(rule.to,msg);
} catch(err) {
node.error(RED._("change.errors.invalid-expr",{error:err.message}),msg);
return;
}
}
if (rule.t === 'change') {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
if (rule.fromt === "msg") {
fromValue = RED.util.getMessageProperty(msg,rule.from);
} else if (rule.fromt === 'flow') {
fromValue = node.context().flow.get(rule.from);
} else if (rule.fromt === 'global') {
fromValue = node.context().global.get(rule.from);
} }
});
});
}
return Promise.resolve(value);
}
function getFromValue(msg,rule) {
var fromValue;
var fromType;
var fromRE;
if (rule.t === 'change') {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
return new Promise((resolve,reject) => {
if (rule.fromt === "msg") {
resolve(RED.util.getMessageProperty(msg,rule.from));
} else if (rule.fromt === 'flow' || rule.fromt === 'global') {
node.context()[rule.fromt].get(rule.from,(err,fromValue) => {
if (err) {
reject(err);
} else {
resolve(fromValue);
}
});
}
}).then(fromValue => {
if (typeof fromValue === 'number' || fromValue instanceof Number) { if (typeof fromValue === 'number' || fromValue instanceof Number) {
fromType = 'num'; fromType = 'num';
} else if (typeof fromValue === 'boolean') { } else if (typeof fromValue === 'boolean') {
@ -149,108 +158,161 @@ module.exports = function(RED) {
try { try {
fromRE = new RegExp(fromRE, "g"); fromRE = new RegExp(fromRE, "g");
} catch (e) { } catch (e) {
valid = false; reject(new Error(RED._("change.errors.invalid-from",{error:e.message})));
node.error(RED._("change.errors.invalid-from",{error:e.message}),msg);
return; return;
} }
} else { } else {
node.error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}),msg); reject(new Error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)})));
return return;
} }
return {
fromType,
fromValue,
fromRE
}
});
} else {
fromType = rule.fromt;
fromValue = rule.from;
fromRE = rule.fromRE;
}
}
return Promise.resolve({
fromType,
fromValue,
fromRE
});
}
function applyRule(msg,rule) {
var property = rule.p;
var current;
var fromValue;
var fromType;
var fromRE;
try {
return getToValue(msg,rule).then(value => {
return getFromValue(msg,rule).then(fromParts => {
fromValue = fromParts.fromValue;
fromType = fromParts.fromType;
fromRE = fromParts.fromRE;
if (rule.pt === 'msg') {
try {
if (rule.t === 'delete') {
RED.util.setMessageProperty(msg,property,undefined);
} else if (rule.t === 'set') {
RED.util.setMessageProperty(msg,property,value);
} else if (rule.t === 'change') {
current = RED.util.getMessageProperty(msg,property);
if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
RED.util.setMessageProperty(msg,property,value);
} else {
current = current.replace(fromRE,value);
RED.util.setMessageProperty(msg,property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
RED.util.setMessageProperty(msg,property,value);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
RED.util.setMessageProperty(msg,property,value);
}
}
}
} catch(err) {}
return msg;
} else if (rule.pt === 'flow' || rule.pt === 'global') {
return new Promise((resolve,reject) => {
var target = node.context()[rule.pt];
var callback = err => {
if (err) {
reject(err);
} else {
resolve(msg);
}
}
if (rule.t === 'delete') {
target.set(property,undefined,callback);
} else if (rule.t === 'set') {
target.set(property,value,callback);
} else if (rule.t === 'change') {
target.get(property,(err,current) => {
if (err) {
reject(err);
return;
}
if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
target.set(property,value,callback);
} else {
current = current.replace(fromRE,value);
target.set(property,current,callback);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
target.set(property,value,callback);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
target.set(property,value,callback);
}
}
});
}
});
}
});
}).catch(err => {
node.error(err, msg);
return null;
});
} catch(err) {
return Promise.resolve(msg);
}
}
function applyRules(msg, currentRule) {
var r = node.rules[currentRule];
var rulePromise;
if (r.t === "move") {
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) {
rulePromise = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt}).then(
msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt})
);
}
else { // 2 step move if we are moving from a child
rulePromise = applyRule(msg,{t:"set", p:"_temp_move", pt:r.tot, to:r.p, tot:r.pt}).then(
msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt})
).then(
msg => applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:"_temp_move", tot:r.pt})
).then(
msg => applyRule(msg,{t:"delete", p:"_temp_move", pt:r.pt})
)
}
} else {
rulePromise = applyRule(msg,r);
}
return rulePromise.then(
msg => {
if (!msg) {
return
} else if (currentRule === node.rules.length - 1) {
return msg;
} else { } else {
fromType = rule.fromt; return applyRules(msg, currentRule+1);
fromValue = rule.from;
fromRE = rule.fromRE;
} }
} }
if (rule.pt === 'msg') { );
if (rule.t === 'delete') {
RED.util.setMessageProperty(msg,property,undefined);
} else if (rule.t === 'set') {
RED.util.setMessageProperty(msg,property,value);
} else if (rule.t === 'change') {
current = RED.util.getMessageProperty(msg,property);
if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
RED.util.setMessageProperty(msg,property,value);
} else {
current = current.replace(fromRE,value);
RED.util.setMessageProperty(msg,property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
RED.util.setMessageProperty(msg,property,value);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
RED.util.setMessageProperty(msg,property,value);
}
}
}
}
else {
var target;
if (rule.pt === 'flow') {
target = node.context().flow;
} else if (rule.pt === 'global') {
target = node.context().global;
}
if (target) {
if (rule.t === 'delete') {
target.set(property,undefined);
} else if (rule.t === 'set') {
target.set(property,value);
} else if (rule.t === 'change') {
current = target.get(property);
if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
target.set(property,value);
} else {
current = current.replace(fromRE,value);
target.set(property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
target.set(property,value);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
target.set(property,value);
}
}
}
}
}
} catch(err) {/*console.log(err.stack)*/}
return msg;
} }
if (valid) { if (valid) {
this.on('input', function(msg) { this.on('input', function(msg) {
for (var i=0; i<this.rules.length; i++) { applyRules(msg, 0)
if (this.rules[i].t === "move") { .then( msg => { if (msg) { node.send(msg) }} )
var r = this.rules[i]; .catch( err => node.error(err, msg))
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) {
msg = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt});
applyRule(msg,{t:"delete", p:r.p, pt:r.pt});
}
else { // 2 step move if we are moving from a child
msg = applyRule(msg,{t:"set", p:"_temp_move", pt:r.tot, to:r.p, tot:r.pt});
applyRule(msg,{t:"delete", p:r.p, pt:r.pt});
msg = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:"_temp_move", tot:r.pt});
applyRule(msg,{t:"delete", p:"_temp_move", pt:r.pt});
}
} else {
msg = applyRule(msg,this.rules[i]);
}
if (msg === null) {
return;
}
}
node.send(msg);
}); });
} }
} }

View File

@ -233,7 +233,7 @@ module.exports = function(RED) {
RED.nodes.registerType("split",SplitNode); RED.nodes.registerType("split",SplitNode);
var _max_kept_msgs_count = undefined; var _max_kept_msgs_count;
function max_kept_msgs_count(node) { function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) { if (_max_kept_msgs_count === undefined) {
@ -252,7 +252,15 @@ module.exports = function(RED) {
exp.assign("I", index); exp.assign("I", index);
exp.assign("N", count); exp.assign("N", count);
exp.assign("A", accum); exp.assign("A", accum);
return RED.util.evaluateJSONataExpression(exp, msg); return new Promise((resolve,reject) => {
RED.util.evaluateJSONataExpression(exp, msg, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
} }
function apply_f(exp, accum, count) { function apply_f(exp, accum, count) {
@ -269,32 +277,37 @@ module.exports = function(RED) {
return exp return exp
} }
function reduce_and_send_group(node, group) { function reduceAndSendGroup(node, group) {
var is_right = node.reduce_right; var is_right = node.reduce_right;
var flag = is_right ? -1 : 1; var flag = is_right ? -1 : 1;
var msgs = group.msgs; var msgs = group.msgs;
var accum = eval_exp(node, node.exp_init, node.exp_init_type); return getInitialReduceValue(node, node.exp_init, node.exp_init_type).then(accum => {
var reduce_exp = node.reduce_exp; var reduce_exp = node.reduce_exp;
var reduce_fixup = node.reduce_fixup; var reduce_fixup = node.reduce_fixup;
var count = group.count; var count = group.count;
msgs.sort(function(x,y) { msgs.sort(function(x,y) {
var ix = x.parts.index; var ix = x.parts.index;
var iy = y.parts.index; var iy = y.parts.index;
if (ix < iy) return -flag; if (ix < iy) {return -flag;}
if (ix > iy) return flag; if (ix > iy) {return flag;}
return 0; return 0;
});
return msgs.reduce((promise, msg) => promise.then(accum => apply_r(reduce_exp, accum, msg, msg.parts.index, count)), Promise.resolve(accum))
.then(accum => {
if(reduce_fixup !== undefined) {
accum = apply_f(reduce_fixup, accum, count);
}
node.send({payload: accum});
});
}).catch(err => {
throw new Error(RED._("join.errors.invalid-expr",{error:e.message}));
}); });
for(var msg of msgs) {
accum = apply_r(reduce_exp, accum, msg, msg.parts.index, count);
}
if(reduce_fixup !== undefined) {
accum = apply_f(reduce_fixup, accum, count);
}
node.send({payload: accum});
} }
function reduce_msg(node, msg) { function reduce_msg(node, msg) {
if(msg.hasOwnProperty('parts')) { var promise;
if (msg.hasOwnProperty('parts')) {
var parts = msg.parts; var parts = msg.parts;
var pending = node.pending; var pending = node.pending;
var pending_count = node.pending_count; var pending_count = node.pending_count;
@ -312,65 +325,82 @@ module.exports = function(RED) {
var group = pending[gid]; var group = pending[gid];
var msgs = group.msgs; var msgs = group.msgs;
if(parts.hasOwnProperty('count') && if(parts.hasOwnProperty('count') &&
(group.count === undefined)) { (group.count === undefined)) {
group.count = count; group.count = count;
} }
msgs.push(msg); msgs.push(msg);
pending_count++; pending_count++;
var completeProcess = function() {
node.pending_count = pending_count;
var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) {
node.pending = {};
node.pending_count = 0;
var promise = Promise.reject(RED._("join.too-many"));
promise.catch(()=>{});
return promise;
}
return Promise.resolve();
}
if(msgs.length === group.count) { if(msgs.length === group.count) {
delete pending[gid]; delete pending[gid];
try { pending_count -= msgs.length;
pending_count -= msgs.length; promise = reduceAndSendGroup(node, group).then(completeProcess);
reduce_and_send_group(node, group); } else {
} catch(e) { promise = completeProcess();
node.error(RED._("join.errors.invalid-expr",{error:e.message})); }
} }
node.pending_count = pending_count; } else {
var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) {
node.pending = {};
node.pending_count = 0;
node.error(RED._("join.too-many"), msg);
}
}
else {
node.send(msg); node.send(msg);
} }
if (!promise) {
promise = Promise.resolve();
}
return promise;
} }
function eval_exp(node, exp, exp_type) { function getInitialReduceValue(node, exp, exp_type) {
if(exp_type === "flow") { return new Promise((resolve,reject) => {
return node.context().flow.get(exp); if(exp_type === "flow" || exp_type === "global") {
} node.context()[exp_type].get(exp,(err,value) => {
else if(exp_type === "global") { if (err) {
return node.context().global.get(exp); reject(err);
} } else {
else if(exp_type === "str") { resolve(value);
return exp; }
} });
else if(exp_type === "num") { return;
return Number(exp); } else if(exp_type === "jsonata") {
} var jexp = RED.util.prepareJSONataExpression(exp, node);
else if(exp_type === "bool") { RED.util.evaluateJSONataExpression(jexp, {},(err,value) => {
if (exp === 'true') { if (err) {
return true; reject(err);
} else {
resolve(value);
}
});
return;
} }
else if (exp === 'false') { var result;
return false; if(exp_type === "str") {
result = exp;
} else if(exp_type === "num") {
result = Number(exp);
} else if(exp_type === "bool") {
if (exp === 'true') {
result = true;
} else if (exp === 'false') {
result = false;
}
} else if ((exp_type === "bin") || (exp_type === "json")) {
result = JSON.parse(exp);
} else if(exp_type === "date") {
result = Date.now();
} else {
reject(new Error("unexpected initial value type"));
return;
} }
} resolve(result);
else if ((exp_type === "bin") || });
(exp_type === "json")) {
return JSON.parse(exp);
}
else if(exp_type === "date") {
return Date.now();
}
else if(exp_type === "jsonata") {
var jexp = RED.util.prepareJSONataExpression(exp, node);
return RED.util.evaluateJSONataExpression(jexp, {});
}
throw new Error("unexpected initial value type");
} }
function JoinNode(n) { function JoinNode(n) {
@ -437,7 +467,8 @@ module.exports = function(RED) {
newArray = newArray.concat(n); newArray = newArray.concat(n);
}) })
group.payload = newArray; group.payload = newArray;
} else if (group.type === 'buffer') { }
else if (group.type === 'buffer') {
var buffers = []; var buffers = [];
var bufferLen = 0; var bufferLen = 0;
if (group.joinChar !== undefined) { if (group.joinChar !== undefined) {
@ -450,7 +481,8 @@ module.exports = function(RED) {
buffers.push(group.payload[i]); buffers.push(group.payload[i]);
bufferLen += group.payload[i].length; bufferLen += group.payload[i].length;
} }
} else { }
else {
bufferLen = group.bufferLen; bufferLen = group.bufferLen;
buffers = group.payload; buffers = group.payload;
} }
@ -463,7 +495,8 @@ module.exports = function(RED) {
groupJoinChar = group.joinChar.toString(); groupJoinChar = group.joinChar.toString();
} }
RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar)); RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar));
} else { }
else {
if (node.propertyType === 'full') { if (node.propertyType === 'full') {
group.msg = RED.util.cloneMessage(group.msg); group.msg = RED.util.cloneMessage(group.msg);
} }
@ -471,13 +504,48 @@ module.exports = function(RED) {
} }
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) { if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
group.msg.parts = group.msg.parts.parts; group.msg.parts = group.msg.parts.parts;
} else { }
else {
delete group.msg.parts; delete group.msg.parts;
} }
delete group.msg.complete; delete group.msg.complete;
node.send(group.msg); node.send(group.msg);
} }
var pendingMessages = [];
var activeMessagePromise = null;
// In reduce mode, we must process messages fully in order otherwise
// groups may overlap and cause unexpected results. The use of JSONata
// means some async processing *might* occur if flow/global context is
// accessed.
var processReduceMessageQueue = function(msg) {
if (msg) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
if (activeMessagePromise !== null) {
// The node is currently processing a message, so do nothing
// more with this message
return;
}
}
if (pendingMessages.length === 0) {
// There are no more messages to process, clear the active flag
// and return
activeMessagePromise = null;
return;
}
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
activeMessagePromise = reduce_msg(node, nextMsg)
.then(processReduceMessageQueue)
.catch((err) => {
node.error(err,nextMsg);
return processReduceMessageQueue();
});
}
this.on("input", function(msg) { this.on("input", function(msg) {
try { try {
var property; var property;
@ -516,8 +584,7 @@ module.exports = function(RED) {
propertyIndex = msg.parts.index; propertyIndex = msg.parts.index;
} }
else if (node.mode === 'reduce') { else if (node.mode === 'reduce') {
reduce_msg(node, msg); return processReduceMessageQueue(msg);
return;
} }
else { else {
// Use the node configuration to identify all of the group information // Use the node configuration to identify all of the group information
@ -525,7 +592,7 @@ module.exports = function(RED) {
payloadType = node.build; payloadType = node.build;
targetCount = node.count; targetCount = node.count;
joinChar = node.joiner; joinChar = node.joiner;
if (targetCount === 0 && msg.hasOwnProperty('parts')) { if (n.count === "" && msg.hasOwnProperty('parts')) {
targetCount = msg.parts.count || 0; targetCount = msg.parts.count || 0;
} }
if (node.build === 'object') { if (node.build === 'object') {
@ -554,7 +621,7 @@ module.exports = function(RED) {
payload:{}, payload:{},
targetCount:targetCount, targetCount:targetCount,
type:"object", type:"object",
msg:msg msg:RED.util.cloneMessage(msg)
}; };
} }
else if (node.accumulate === true) { else if (node.accumulate === true) {
@ -564,7 +631,7 @@ module.exports = function(RED) {
payload:{}, payload:{},
targetCount:targetCount, targetCount:targetCount,
type:payloadType, type:payloadType,
msg:msg msg:RED.util.cloneMessage(msg)
} }
if (payloadType === 'string' || payloadType === 'array' || payloadType === 'buffer') { if (payloadType === 'string' || payloadType === 'array' || payloadType === 'buffer') {
inflight[partId].payload = []; inflight[partId].payload = [];
@ -576,7 +643,7 @@ module.exports = function(RED) {
payload:[], payload:[],
targetCount:targetCount, targetCount:targetCount,
type:payloadType, type:payloadType,
msg:msg msg:RED.util.cloneMessage(msg)
}; };
if (payloadType === 'string') { if (payloadType === 'string') {
inflight[partId].joinChar = joinChar; inflight[partId].joinChar = joinChar;
@ -627,14 +694,14 @@ module.exports = function(RED) {
} }
} }
} }
// TODO: currently reuse the last received - add option to pick first received group.msg = Object.assign(group.msg, msg);
group.msg = msg;
var tcnt = group.targetCount; var tcnt = group.targetCount;
if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; } if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; }
if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) { if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) {
completeSend(partId); completeSend(partId);
} }
} catch(err) { }
catch(err) {
console.log(err.stack); console.log(err.stack);
} }
}); });

View File

@ -17,7 +17,7 @@
module.exports = function(RED) { module.exports = function(RED) {
"use strict"; "use strict";
var _max_kept_msgs_count = undefined; var _max_kept_msgs_count;
function max_kept_msgs_count(node) { function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) { if (_max_kept_msgs_count === undefined) {
@ -32,30 +32,20 @@ module.exports = function(RED) {
return _max_kept_msgs_count; return _max_kept_msgs_count;
} }
function eval_jsonata(node, code, val) { // function get_context_val(node, name, dval) {
try { // var context = node.context();
return RED.util.evaluateJSONataExpression(code, val); // var val = context.get(name);
} // if (val === undefined) {
catch (e) { // context.set(name, dval);
node.error(RED._("sort.invalid-exp")); // return dval;
throw e; // }
} // return val;
} // }
function get_context_val(node, name, dval) {
var context = node.context();
var val = context.get(name);
if (val === undefined) {
context.set(name, dval);
return dval;
}
return val;
}
function SortNode(n) { function SortNode(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
var node = this; var node = this;
var pending = get_context_val(node, 'pending', {}) var pending = {};//get_context_val(node, 'pending', {})
var pending_count = 0; var pending_count = 0;
var pending_id = 0; var pending_id = 0;
var order = n.order || "ascending"; var order = n.order || "ascending";
@ -71,16 +61,15 @@ module.exports = function(RED) {
key_exp = RED.util.prepareJSONataExpression(key_exp, this); key_exp = RED.util.prepareJSONataExpression(key_exp, this);
} }
catch (e) { catch (e) {
node.error(RED._("sort.invalid-exp")); node.error(RED._("sort.invalid-exp",{message:e.toString()}));
return; return;
} }
} }
var dir = (order === "ascending") ? 1 : -1; var dir = (order === "ascending") ? 1 : -1;
var conv = as_num var conv = as_num ? function(x) { return Number(x); }
? function(x) { return Number(x); } : function(x) { return x; };
: function(x) { return x; };
function gen_comp(key) { function generateComparisonFunction(key) {
return function(x, y) { return function(x, y) {
var xp = conv(key(x)); var xp = conv(key(x));
var yp = conv(key(y)); var yp = conv(key(y));
@ -90,74 +79,105 @@ module.exports = function(RED) {
}; };
} }
function send_group(group) { function sortMessageGroup(group) {
var key = key_is_exp var promise;
? function(msg) {
return eval_jsonata(node, key_exp, msg);
}
: function(msg) {
return RED.util.getMessageProperty(msg, key_prop);
};
var comp = gen_comp(key);
var msgs = group.msgs; var msgs = group.msgs;
try { if (key_is_exp) {
msgs.sort(comp); var evaluatedDataPromises = msgs.map(msg => {
} return new Promise((resolve,reject) => {
catch (e) { RED.util.evaluateJSONataExpression(key_exp, msg, (err, result) => {
return; // not send when error if (err) {
} reject(RED._("sort.invalid-exp",{message:err.toString()}));
for (var i = 0; i < msgs.length; i++) { } else {
var msg = msgs[i]; resolve({
msg.parts.index = i; item: msg,
node.send(msg); sortValue: result
} })
} }
});
function sort_payload(msg) { })
var data = RED.util.getMessageProperty(msg, target_prop); });
if (Array.isArray(data)) { promise = Promise.all(evaluatedDataPromises).then(evaluatedElements => {
var key = key_is_exp // Once all of the sort keys are evaluated, sort by them
? function(elem) { var comp = generateComparisonFunction(elem=>elem.sortValue);
return eval_jsonata(node, key_exp, elem); return evaluatedElements.sort(comp).map(elem=>elem.item);
} });
: function(elem) { return elem; }; } else {
var comp = gen_comp(key); var key = function(msg) {
return ;
}
var comp = generateComparisonFunction(msg => RED.util.getMessageProperty(msg, key_prop));
try { try {
data.sort(comp); msgs.sort(comp);
} }
catch (e) { catch (e) {
return false; return; // not send when error
} }
return true; promise = Promise.resolve(msgs);
} }
return false; return promise.then(msgs => {
for (var i = 0; i < msgs.length; i++) {
var msg = msgs[i];
msg.parts.index = i;
node.send(msg);
}
});
} }
function check_parts(parts) { function sortMessageProperty(msg) {
if (parts.hasOwnProperty("id") && var data = RED.util.getMessageProperty(msg, target_prop);
parts.hasOwnProperty("index")) { if (Array.isArray(data)) {
return true; if (key_is_exp) {
// key is an expression. Evaluated the expression for each item
// to get its sort value. As this could be async, need to do
// it first.
var evaluatedDataPromises = data.map(elem => {
return new Promise((resolve,reject) => {
RED.util.evaluateJSONataExpression(key_exp, elem, (err, result) => {
if (err) {
reject(RED._("sort.invalid-exp",{message:err.toString()}));
} else {
resolve({
item: elem,
sortValue: result
})
}
});
})
})
return Promise.all(evaluatedDataPromises).then(evaluatedElements => {
// Once all of the sort keys are evaluated, sort by them
// and reconstruct the original message item with the newly
// sorted values.
var comp = generateComparisonFunction(elem=>elem.sortValue);
data = evaluatedElements.sort(comp).map(elem=>elem.item);
RED.util.setMessageProperty(msg, target_prop,data);
return true;
})
} else {
var comp = generateComparisonFunction(elem=>elem);
try {
data.sort(comp);
} catch (e) {
return Promise.resolve(false);
}
return Promise.resolve(true);
}
} }
return false; return Promise.resolve(false);
} }
function clear_pending() { function removeOldestPending() {
var oldest;
var oldest_key;
for(var key in pending) { for(var key in pending) {
node.log(RED._("sort.clear"), pending[key].msgs[0]); if (pending.hasOwnProperty(key)) {
delete pending[key]; var item = pending[key];
} if((oldest === undefined) ||
pending_count = 0; (oldest.seq_no > item.seq_no)) {
} oldest = item;
oldest_key = key;
function remove_oldest_pending() { }
var oldest = undefined;
var oldest_key = undefined;
for(var key in pending) {
var item = pending[key];
if((oldest === undefined) ||
(oldest.seq_no > item.seq_no)) {
oldest = item;
oldest_key = key;
} }
} }
if(oldest !== undefined) { if(oldest !== undefined) {
@ -166,16 +186,19 @@ module.exports = function(RED) {
} }
return 0; return 0;
} }
function process_msg(msg) { function processMessage(msg) {
if (target_is_prop) { if (target_is_prop) {
if (sort_payload(msg)) { sortMessageProperty(msg).then(send => {
node.send(msg); if (send) {
} node.send(msg);
return; }
}).catch(err => {
node.error(err,msg);
});
} }
var parts = msg.parts; var parts = msg.parts;
if (!check_parts(parts)) { if (!parts.hasOwnProperty("id") || !parts.hasOwnProperty("index")) {
return; return;
} }
var gid = parts.id; var gid = parts.id;
@ -195,23 +218,31 @@ module.exports = function(RED) {
pending_count++; pending_count++;
if (group.count === msgs.length) { if (group.count === msgs.length) {
delete pending[gid] delete pending[gid]
send_group(group); sortMessageGroup(group).catch(err => {
node.error(err,msg);
});
pending_count -= msgs.length; pending_count -= msgs.length;
} } else {
var max_msgs = max_kept_msgs_count(node); var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) { if ((max_msgs > 0) && (pending_count > max_msgs)) {
pending_count -= remove_oldest_pending(); pending_count -= removeOldestPending();
node.error(RED._("sort.too-many"), msg); node.error(RED._("sort.too-many"), msg);
}
} }
} }
this.on("input", function(msg) { this.on("input", function(msg) {
process_msg(msg); processMessage(msg);
}); });
this.on("close", function() { this.on("close", function() {
clear_pending(); for(var key in pending) {
}) if (pending.hasOwnProperty(key)) {
node.log(RED._("sort.clear"), pending[key].msgs[0]);
delete pending[key];
}
}
pending_count = 0; })
} }
RED.nodes.registerType("sort", SortNode); RED.nodes.registerType("sort", SortNode);

View File

@ -31,6 +31,8 @@
<dl class="message-properties"> <dl class="message-properties">
<dt>payload<span class="property-type">object | string</span></dt> <dt>payload<span class="property-type">object | string</span></dt>
<dd>A JavaScript object or JSON string.</dd> <dd>A JavaScript object or JSON string.</dd>
<dt>schema<span class="property-type">object</span></dt>
<dd>An optional JSON Schema object to validate the payload against.</dd>
</dl> </dl>
<h3>Outputs</h3> <h3>Outputs</h3>
<dl class="message-properties"> <dl class="message-properties">
@ -41,6 +43,9 @@
<li>If the input is a JavaScript object it creates a JSON string. The string can optionally be well-formatted.</li> <li>If the input is a JavaScript object it creates a JSON string. The string can optionally be well-formatted.</li>
</ul> </ul>
</dd> </dd>
<dt>schemaError<span class="property-type">array</span></dt>
<dd>If JSON schema validation fails, the catch node will have a <code>schemaError</code> property
containing an array of errors.</dd>
</dl> </dl>
<h3>Details</h3> <h3>Details</h3>
<p>By default, the node operates on <code>msg.payload</code>, but can be configured <p>By default, the node operates on <code>msg.payload</code>, but can be configured
@ -53,6 +58,8 @@
receives a String, no further checks will be made of the property. It will receives a String, no further checks will be made of the property. It will
not check the String is valid JSON nor will it reformat it if the format option not check the String is valid JSON nor will it reformat it if the format option
is selected.</p> is selected.</p>
<p>For more details about JSON Schema you can consult the specification
<a href="http://json-schema.org/latest/json-schema-validation.html">here</a>.</p>
</script> </script>
<script type="text/javascript"> <script type="text/javascript">

View File

@ -16,21 +16,52 @@
module.exports = function(RED) { module.exports = function(RED) {
"use strict"; "use strict";
const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true, schemaId: 'auto'});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
function JSONNode(n) { function JSONNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
this.indent = n.pretty ? 4 : 0; this.indent = n.pretty ? 4 : 0;
this.action = n.action||""; this.action = n.action||"";
this.property = n.property||"payload"; this.property = n.property||"payload";
this.schema = null;
this.compiledSchema = null;
var node = this; var node = this;
this.on("input", function(msg) { this.on("input", function(msg) {
var validate = false;
if (msg.schema) {
// If input schema is different, re-compile it
if (JSON.stringify(this.schema) != JSON.stringify(msg.schema)) {
try {
this.compiledSchema = ajv.compile(msg.schema);
this.schema = msg.schema;
} catch(e) {
this.schema = null;
this.compiledSchema = null;
node.error(RED._("json.errors.schema-error-compile"), msg);
return;
}
}
validate = true;
}
var value = RED.util.getMessageProperty(msg,node.property); var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) { if (value !== undefined) {
if (typeof value === "string") { if (typeof value === "string") {
if (node.action === "" || node.action === "obj") { if (node.action === "" || node.action === "obj") {
try { try {
RED.util.setMessageProperty(msg,node.property,JSON.parse(value)); RED.util.setMessageProperty(msg,node.property,JSON.parse(value));
node.send(msg); if (validate) {
if (this.compiledSchema(msg[node.property])) {
node.send(msg);
} else {
msg.schemaError = this.compiledSchema.errors;
node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg);
}
} else {
node.send(msg);
}
} }
catch(e) { node.error(e.message,msg); } catch(e) { node.error(e.message,msg); }
} else { } else {
@ -41,8 +72,19 @@ module.exports = function(RED) {
if (node.action === "" || node.action === "str") { if (node.action === "" || node.action === "str") {
if (!Buffer.isBuffer(value)) { if (!Buffer.isBuffer(value)) {
try { try {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent)); if (validate) {
node.send(msg); if (this.compiledSchema(value)) {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
node.send(msg);
} else {
msg.schemaError = this.compiledSchema.errors;
node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg);
}
} else {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
node.send(msg);
}
} }
catch(e) { node.error(RED._("json.errors.dropped-error")); } catch(e) { node.error(RED._("json.errors.dropped-error")); }
} }

View File

@ -33,6 +33,7 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"ajv": "6.5.1",
"basic-auth": "2.0.0", "basic-auth": "2.0.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.18.3", "body-parser": "1.18.3",

View File

@ -143,7 +143,13 @@ LocalFileSystem.prototype.open = function(){
self.cache.set(scope,key,data[key]); self.cache.set(scope,key,data[key]);
}) })
}); });
}) }).catch(function(err){
if(err.code == 'ENOENT') {
return fs.mkdir(self.storageBaseDir);
}else{
return Promise.reject(err);
}
});
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }

View File

@ -335,7 +335,7 @@ var parseContextStore = function(key) {
} }
function evaluateNodeProperty(value, type, node, msg, callback) { function evaluateNodeProperty(value, type, node, msg, callback) {
var result; var result = value;
if (type === 'str') { if (type === 'str') {
result = ""+value; result = ""+value;
} else if (type === 'num') { } else if (type === 'num') {
@ -350,7 +350,16 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
var data = JSON.parse(value); var data = JSON.parse(value);
result = Buffer.from(data); result = Buffer.from(data);
} else if (type === 'msg' && msg) { } else if (type === 'msg' && msg) {
result = getMessageProperty(msg,value); try {
result = getMessageProperty(msg,value);
} catch(err) {
if (callback) {
callback(err);
} else {
throw err;
}
return;
}
} else if ((type === 'flow' || type === 'global') && node) { } else if ((type === 'flow' || type === 'global') && node) {
var contextKey = parseContextStore(value); var contextKey = parseContextStore(value);
result = node.context()[type].get(contextKey.key,contextKey.store,callback); result = node.context()[type].get(contextKey.key,contextKey.store,callback);
@ -366,9 +375,9 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
result = evaluteEnvProperty(value); result = evaluteEnvProperty(value);
} }
if (callback) { if (callback) {
callback(result); callback(null,result);
} else { } else {
return value; return result;
} }
} }
@ -385,15 +394,44 @@ function prepareJSONataExpression(value,node) {
}) })
expr.registerFunction('clone', cloneMessage, '<(oa)-:o>'); expr.registerFunction('clone', cloneMessage, '<(oa)-:o>');
expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value); expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value);
expr._node = node;
return expr; return expr;
} }
function evaluateJSONataExpression(expr,msg) { function evaluateJSONataExpression(expr,msg,callback) {
var context = msg; var context = msg;
if (expr._legacyMode) { if (expr._legacyMode) {
context = {msg:msg}; context = {msg:msg};
} }
return expr.evaluate(context); var bindings = {};
if (callback) {
// If callback provided, need to override the pre-assigned sync
// context functions to be their async variants
bindings.flowContext = function(val) {
return new Promise((resolve,reject) => {
expr._node.context().flow.get(val, function(err,value) {
if (err) {
reject(err);
} else {
resolve(value);
}
})
});
}
bindings.globalContext = function(val) {
return new Promise((resolve,reject) => {
expr._node.context().global.get(val, function(err,value) {
if (err) {
reject(err);
} else {
resolve(value);
}
})
});
}
}
return expr.evaluate(context, bindings, callback);
} }

View File

@ -62,10 +62,11 @@ exports.config = {
// //
browserName: 'chrome', browserName: 'chrome',
chromeOptions: { chromeOptions: {
// Runs tests without opening a broser. args: process.env.NODE_RED_NON_HEADLESS
args: ['--headless', '--disable-gpu', 'window-size=1920,1080'], // Runs tests with opening a browser.
// Runs tests with opening a broser. ? ['--disable-gpu']
// args: ['--disable-gpu'], // Runs tests without opening a browser.
: ['--headless', '--disable-gpu', 'window-size=1920,1080']
}, },
}], }],
// //

View File

@ -288,7 +288,7 @@ describe('trigger node', function() {
it('should be able to return things from flow and global context variables', function(done) { it('should be able to return things from flow and global context variables', function(done) {
var spy = sinon.stub(RED.util, 'evaluateNodeProperty', var spy = sinon.stub(RED.util, 'evaluateNodeProperty',
function(arg1, arg2, arg3, arg4) { return arg1; } function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } }
); );
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"flow", op2:"bar", op2type:"global", duration:"20", wires:[["n2"]] }, var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"flow", op2:"bar", op2type:"global", duration:"20", wires:[["n2"]] },
{id:"n2", type:"helper"} ]; {id:"n2", type:"helper"} ];
@ -386,7 +386,7 @@ describe('trigger node', function() {
it('should be able to extend the delay', function(done) { it('should be able to extend the delay', function(done) {
this.timeout(5000); // add extra time for flake this.timeout(5000); // add extra time for flake
var spy = sinon.stub(RED.util, 'evaluateNodeProperty', var spy = sinon.stub(RED.util, 'evaluateNodeProperty',
function(arg1, arg2, arg3, arg4) { return arg1; } function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } }
); );
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"flow", op1:"foo", op2:"bar", op2type:"global", duration:"100", wires:[["n2"]] }, var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"flow", op1:"foo", op2:"bar", op2type:"global", duration:"100", wires:[["n2"]] },
{id:"n2", type:"helper"} ]; {id:"n2", type:"helper"} ];
@ -428,12 +428,10 @@ describe('trigger node', function() {
n2.on("input", function(msg) { n2.on("input", function(msg) {
try { try {
if (c === 0) { if (c === 0) {
console.log(c,Date.now() - ss,msg);
msg.should.have.a.property("payload", "Hello"); msg.should.have.a.property("payload", "Hello");
c += 1; c += 1;
} }
else { else {
console.log(c,Date.now() - ss,msg);
msg.should.have.a.property("payload", "World"); msg.should.have.a.property("payload", "World");
(Date.now() - ss).should.be.greaterThan(150); (Date.now() - ss).should.be.greaterThan(150);
done(); done();

View File

@ -460,7 +460,7 @@ describe('switch Node', function() {
} catch(err) { } catch(err) {
done(err); done(err);
} }
},100) },500)
}); });
}); });
@ -599,7 +599,7 @@ describe('switch Node', function() {
it('should take head of message sequence (w. context)', function(done) { it('should take head of message sequence (w. context)', function(done) {
var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"head","v":"count",vt:"global"}],checkall:false,repair:true,outputs:1,wires:[["helperNode1"]]}, var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"head","v":"count",vt:"global"}],checkall:false,repair:true,outputs:1,wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}]; {id:"helperNode1", type:"helper", wires:[]}];
customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [0, 1, 2], true, customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [0, 1, 2], true,
function(node) { function(node) {
node.context().global.set("count", 3); node.context().global.set("count", 3);
}, done); }, done);
@ -642,7 +642,7 @@ describe('switch Node', function() {
{id:"helperNode1", type:"helper", wires:[]}]; {id:"helperNode1", type:"helper", wires:[]}];
customFlowSwitchTest(flow, true, 9, done); customFlowSwitchTest(flow, true, 9, done);
}); });
it('should be able to use $I in JSONata expression', function(done) { it('should be able to use $I in JSONata expression', function(done) {
var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"jsonata_exp","v":"$I % 2 = 1",vt:"jsonata"}],checkall:true,repair:true,outputs:1,wires:[["helperNode1"]]}, var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"jsonata_exp","v":"$I % 2 = 1",vt:"jsonata"}],checkall:true,repair:true,outputs:1,wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}]; {id:"helperNode1", type:"helper", wires:[]}];
@ -821,4 +821,24 @@ describe('switch Node', function() {
n1.receive({payload:1, parts:{index:0, count:4, id:222}}); n1.receive({payload:1, parts:{index:0, count:4, id:222}});
}); });
}); });
it('should handle invalid jsonata expression', function(done) {
var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"$invalidExpression(payload)",propertyType:"jsonata",rules:[{"t":"btwn","v":"$sqrt(16)","vt":"jsonata","v2":"$sqrt(36)","v2t":"jsonata"}],checkall:true,outputs:1,wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(switchNode, flow, function() {
var n1 = helper.getNode("switchNode1");
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "switch";
});
var evt = logEvents[0][0];
evt.should.have.property('id', "switchNode1");
evt.should.have.property('type', "switch");
done();
}, 150);
n1.receive({payload:1});
});
});
}); });

View File

@ -15,6 +15,7 @@
**/ **/
var should = require("should"); var should = require("should");
var sinon = require("sinon");
var changeNode = require("../../../../nodes/core/logic/15-change.js"); var changeNode = require("../../../../nodes/core/logic/15-change.js");
var helper = require("node-red-node-test-helper"); var helper = require("node-red-node-test-helper");
@ -454,6 +455,28 @@ describe('change Node', function() {
}); });
}); });
it('reports invalid jsonata expression', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"set","p":"payload","to":"$invalid(payload)","tot":"jsonata"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
sinon.spy(changeNode1,"error");
helperNode1.on("input", function(msg) {
done("Invalid jsonata expression passed message through");
});
changeNode1.receive({payload:"Hello World!"});
setTimeout(function() {
try {
changeNode1.error.called.should.be.true();
done();
} catch(err) {
done(err);
}
},50);
});
});
}); });
describe('#change', function() { describe('#change', function() {
it('changes the value of the message property', function(done) { it('changes the value of the message property', function(done) {

View File

@ -247,4 +247,85 @@ describe('JSON node', function() {
}); });
}); });
}); });
it('should pass an object if provided a valid JSON string and schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload.number, 3);
should.equal(msg.payload.string, "allo");
done();
});
var jsonString = '{"number": 3, "string": "allo"}';
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:jsonString, schema:schema});
});
});
it('should pass a string if provided a valid object and schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, '{"number":3,"string":"allo"}');
done();
});
var obj = {"number": 3, "string": "allo"};
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:obj, schema:schema});
});
});
it('should log an error if passed an invalid object and valid schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.equal("json.errors.schema-error: data.number should be number, data.string should be string");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) {
done(err);
}
});
});
it('should log an error if passed a valid object and invalid schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var schema = "garbage";
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.equal("json.errors.schema-error-compile");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) {
done(err);
}
});
});
}); });

View File

@ -307,6 +307,10 @@ describe("red/util", function() {
},{}); },{});
result.should.eql("123"); result.should.eql("123");
}); });
it('returns null', function() {
var result = util.evaluateNodeProperty(null,'null');
(result === null).should.be.true();
})
describe('environment variable', function() { describe('environment variable', function() {
before(function() { before(function() {
process.env.NR_TEST_A = "foo"; process.env.NR_TEST_A = "foo";
@ -454,6 +458,30 @@ describe("red/util", function() {
var result = util.evaluateJSONataExpression(expr,{payload:"hello"}); var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
should.not.exist(result); should.not.exist(result);
}); });
it('handles async flow context access', function(done) {
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});
util.evaluateJSONataExpression(expr,{payload:"hello"},function(err,value) {
try {
should.not.exist(err);
value.should.eql("bar");
done();
} catch(err2) {
done(err2);
}
});
})
it('handles async global context access', function(done) {
var expr = util.prepareJSONataExpression('$globalContext("foo")',{context:function() { return {global:{get: function(key,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});
util.evaluateJSONataExpression(expr,{payload:"hello"},function(err,value) {
try {
should.not.exist(err);
value.should.eql("bar");
done();
} catch(err2) {
done(err2);
}
});
})
}); });