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

Merge branch '0.18' into projects

This commit is contained in:
Nick O'Leary 2018-01-24 23:06:27 +00:00 committed by GitHub
commit e250a91f09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 647 additions and 888 deletions

View File

@ -272,10 +272,19 @@ RED.nodes = (function() {
if (updatedConfigNode) {
RED.workspaces.refresh();
}
try {
if (node._def.oneditdelete) {
node._def.oneditdelete.call(node);
}
} catch(err) {
console.log("oneditdelete",node.id,node.type,err.toString());
}
RED.events.emit('nodes:remove',node);
}
}
if (node && node._def.onremove) {
// Deprecated: never documented but used by some early nodes
console.log("Deprecated API warning: node type ",node.type," has an onremove function - should be oneditremove - please report");
node._def.onremove.call(n);
}
return {links:removedLinks,nodes:removedNodes};

View File

@ -779,6 +779,16 @@ RED.editor = (function() {
var fileName = selectIconFile.val();
iconFileHidden.val(fileName);
});
var clear = $('<button class="editor-button editor-button-small"><i class="fa fa-times"></i></button>').appendTo(iconForm);
clear.click(function(evt) {
evt.preventDefault();
var iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
selectIconModule.val(iconPath.module);
moduleChange(selectIconModule, selectIconFile, iconModuleHidden, iconFileHidden, iconSets, true);
selectIconFile.removeClass("input-error");
selectIconFile.val(iconPath.file);
iconFileHidden.val(iconPath.file);
});
moduleChange(selectIconModule, selectIconFile, iconModuleHidden, iconFileHidden, iconSets, false);
var iconFileList = iconSets[selectIconModule.val()];

View File

@ -412,16 +412,17 @@ RED.palette = (function() {
for (var j=0;j<nodeSet.types.length;j++) {
showNodeType(nodeSet.types[j]);
var def = RED.nodes.getType(nodeSet.types[j]);
if (def.onpaletteadd && typeof def.onpaletteadd === "function") {
if (def && def.onpaletteadd && typeof def.onpaletteadd === "function") {
def.onpaletteadd.call(def);
}
}
});
RED.events.on('registry:node-set-disabled', function(nodeSet) {
console.log(nodeSet);
for (var j=0;j<nodeSet.types.length;j++) {
hideNodeType(nodeSet.types[j]);
var def = RED.nodes.getType(nodeSet.types[j]);
if (def.onpaletteremove && typeof def.onpaletteremove === "function") {
if (def && def.onpaletteremove && typeof def.onpaletteremove === "function") {
def.onpaletteremove.call(def);
}
}
@ -431,7 +432,7 @@ RED.palette = (function() {
for (var j=0;j<nodeSet.types.length;j++) {
removeNodeType(nodeSet.types[j]);
var def = RED.nodes.getType(nodeSet.types[j]);
if (def.onpaletteremove && typeof def.onpaletteremove === "function") {
if (def && def.onpaletteremove && typeof def.onpaletteremove === "function") {
def.onpaletteremove.call(def);
}
}

View File

@ -19,6 +19,7 @@ RED.typeSearch = (function() {
function search(val) {
activeFilter = val.toLowerCase();
var visible = searchResults.editableList('filter');
searchResults.editableList('sort');
setTimeout(function() {
selected = 0;
searchResults.children().removeClass('selected');
@ -101,6 +102,23 @@ RED.typeSearch = (function() {
}
return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1);
},
sort: function(A,B) {
if (activeFilter === "") {
return A.i - B.i;
}
var Ai = A.index.indexOf(activeFilter);
var Bi = B.index.indexOf(activeFilter);
if (Ai === -1) {
return 1;
}
if (Bi === -1) {
return -1;
}
if (Ai === Bi) {
return sortTypeLabels(A,B);
}
return Ai-Bi;
},
addItem: function(container,i,object) {
var def = object.def;
object.index = object.type.toLowerCase();
@ -225,7 +243,17 @@ RED.typeSearch = (function() {
}
return label;
}
function sortTypeLabels(a,b) {
var al = a.label.toLowerCase();
var bl = b.label.toLowerCase();
if (al < bl) {
return -1;
} else if (al === bl) {
return 0;
} else {
return 1;
}
}
function refreshTypeList() {
var i;
searchResults.editableList('empty');
@ -250,27 +278,19 @@ RED.typeSearch = (function() {
items.push({type:t,def: def, label:getTypeLabel(t,def)});
}
});
items.sort(function(a,b) {
var al = a.label.toLowerCase();
var bl = b.label.toLowerCase();
if (al < bl) {
return -1;
} else if (al === bl) {
return 0;
} else {
return 1;
}
})
items.sort(sortTypeLabels);
var commonCount = 0;
var item;
var index = 0;
for(i=0;i<common.length;i++) {
var itemDef = RED.nodes.getType(common[i]);
if (itemDef) {
item = {
type: common[i],
common: true,
def: itemDef
def: itemDef,
i: index++
};
item.label = getTypeLabel(item.type,item.def);
if (i === common.length-1) {
@ -283,7 +303,8 @@ RED.typeSearch = (function() {
item = {
type:recentlyUsed[i],
def: RED.nodes.getType(recentlyUsed[i]),
recent: true
recent: true,
i: index++
};
item.label = getTypeLabel(item.type,item.def);
if (i === recentlyUsed.length-1) {
@ -292,6 +313,7 @@ RED.typeSearch = (function() {
searchResults.editableList('addItem', item);
}
for (i=0;i<items.length;i++) {
items[i].i = index++;
searchResults.editableList('addItem', items[i]);
}
setTimeout(function() {

View File

@ -1,20 +1,9 @@
<!--
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.
-->
<script type="text/x-red" data-template-name="sentiment">
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@ -22,7 +11,7 @@
</script>
<script type="text/x-red" data-help-name="sentiment">
<p>Analyses the <code>payload</code> and adds a <code>sentiment</code> object.</p>
<p>Analyses the chosen property, default <code>payload</code>, and adds a <code>sentiment</code> object.</p>
<h3>Outputs</h3>
<dl class="message-properties">
<dt>sentiment <span class="property-type">object</span></dt>
@ -47,6 +36,7 @@
color:"#E6E0F8",
defaults: {
name: {value:""},
property: {value:"payload",required:true}
},
inputs:1,
outputs:1,
@ -56,6 +46,12 @@
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@ -1,18 +1,3 @@
/**
* 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.
**/
module.exports = function(RED) {
"use strict";
@ -20,16 +5,18 @@ module.exports = function(RED) {
function SentimentNode(n) {
RED.nodes.createNode(this,n);
this.property = n.property||"payload";
var node = this;
this.on("input", function(msg) {
if (msg.hasOwnProperty("payload")) {
sentiment(msg.payload, msg.overrides || null, function (err, result) {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
sentiment(value, msg.overrides || null, function (err, result) {
msg.sentiment = result;
node.send(msg);
});
}
else { node.send(msg); } // If no payload - just pass it on.
else { node.send(msg); } // If no matching property - just pass it on.
});
}
RED.nodes.registerType("sentiment",SentimentNode);

View File

@ -5,7 +5,8 @@
"topic": "Topic",
"name": "Name",
"username": "Username",
"password": "Password"
"password": "Password",
"property": "Property"
},
"status": {
"connected": "connected",
@ -542,7 +543,7 @@
"label": {
"property": "Property",
"rule": "rule",
"repair" : "repair sequence (reconstruct parts property of outgoing messages)"
"repair" : "recreate message sequences"
},
"and": "and",
"checkall": "checking all rules",
@ -558,7 +559,7 @@
"nnull":"is not null",
"head":"head",
"tail":"tail",
"index":"is between",
"index":"index between",
"exp":"JSONata exp",
"else":"otherwise"
},
@ -608,7 +609,7 @@
"maxout": "e.g. 255"
},
"scale": {
"payload": "Scale msg.payload",
"payload": "Scale the message property",
"limit": "Scale and limit to the target range",
"wrap": "Scale and wrap within the target range"
},
@ -846,7 +847,7 @@
"mode":{
"mode":"Mode",
"auto":"automatic",
"merge":"merge sequence",
"merge":"merge sequences",
"reduce":"reduce sequence",
"custom":"manual"
},
@ -881,8 +882,8 @@
"exp": "Reduce exp",
"exp-value": "exp",
"init": "Initial value",
"right": "Evaluate in reverse order (right to left)",
"fixup": "Fixup exp"
"right": "Evaluate in reverse order (last to first)",
"fixup": "Fix-up exp"
},
"errors": {
"invalid-expr": "Invalid JSONata expression: __error__"
@ -904,20 +905,19 @@
"batch" : {
"mode": {
"label" : "Mode",
"num-msgs" : "number of messages",
"interval" : "interval in seconds",
"concat" : "concatenate sequences"
"num-msgs" : "Group by number of messages",
"interval" : "Group by time interval",
"concat" : "Concatenate sequences"
},
"count": {
"label" : "Number of msgs",
"overwrap" : "Overwrap",
"label" : "Number of messages",
"overlap" : "Overlap",
"count" : "count",
"invalid" : "Invalid count and overwrap"
"invalid" : "Invalid count and overlap"
},
"interval": {
"label" : "Interval (sec)",
"label" : "Interval",
"seconds" : "seconds",
"sec" : "sec",
"empty" : "send empty message when no message arrives"
},
"concat": {

View File

@ -40,7 +40,7 @@
</script>
<script type="text/x-red" data-help-name="switch">
<p>Route messages based on their property values.</p>
<p>Route messages based on their property values or sequence position.</p>
<h3>Details</h3>
<p>When a message arrives, the node will evaluate each of the defined rules
and forward the message to the corresponding outputs of any matching rules.</p>
@ -48,115 +48,25 @@
that matches.</p>
<p>The rules can be evaluated against an individual message property, a flow or global
context property or the result of a JSONata expression.</p>
<h3>Rules</h3>
<p>Routing rules are categorized into three:</p>
<dl>
<dt>value rules</dt>
<dd>
<table>
<tr>
<th>operator</th>
<th>description</th>
</tr>
<tr>
<td><b>==</b></td>
<td>property value is equals to specified value</td>
</tr>
<tr>
<td><b>!=</b></td>
<td>property value is not equals to specified value</td>
</tr>
<tr>
<td><b>&lt;</b></td>
<td>property value is less than specified value</td>
</tr>
<tr>
<td><b>&lt;=</b></td>
<td>property value is less than or equals to specified value</td>
</tr>
<tr>
<td><b>&gt;</b></td>
<td>property value is greater than specified value</td>
</tr>
<tr>
<td><b>&gt;=</b></td>
<td>property value is greater than or equals to specified value</td>
</tr>
<tr>
<td><b>is between</b></td>
<td>property value is between specified values</td>
</tr>
<tr>
<td><b>contains</b></td>
<td>property string value contains specified string value</td>
</tr>
<tr>
<td><b>matches regex</b></td>
<td>property string value matches specified regex</td>
</tr>
<tr>
<td><b>is true</b></td>
<td>property value is true</td>
</tr>
<tr>
<td><b>is false</b></td>
<td>property value is false</td>
</tr>
<tr>
<td><b>is null</b></td>
<td>property value is null</td>
</tr>
<tr>
<td><b>is not null</b></td>
<td>property value is not null</td>
</tr>
</table>
</dd>
<dt>sequence rules</dt>
<dd>
<table>
<tr>
<th>operator</th>
<th>description</th>
</tr>
<tr>
<td><b>head</b></td>
<td>message is included in the first specified number of messages in a sequence</td>
</tr>
<tr>
<td><b>tail</b></td>
<td>message is included in the last specified number of messages in a sequence</td>
</tr>
<tr>
<td><b>pos. between</b></td>
<td>message is included between specified positions in a sequence</td>
</tr>
</table>
</dd>
<dt>other rules</dt>
<dd>
<table>
<tr>
<th>operator</th>
<th>description</th>
</tr>
<tr>
<td><b>JSONata exp</b></td>
<td>specified JSONata expression evaluate to true. In JSONata expression, $I represents <code>msg.parts.index</code> and $N represents <code>msg.parts.count</code> respectively if exists.</td>
</tr>
<tr>
<td><b>otherwise</b></td>
<td>no matching rule found</td>
</tr>
</table>
</dd>
</dl>
<h3>Repair Sequence</h3>
<p>If <b>repair sequence (reconstruct parts property of outgoing messages)</b> checkbox is selected, <code>msg.parts</code> property are reconstructed for each port to make forks of valid sequences. Otherwise, <code>msg.parts</code> property of incoming messages are passed through.</p>
<h3>Note:</h3>
<p>This node internally keeps messages for its operation if <b>repair sequence</b> checkbox is ON. In order to prevent unexpected memory usage, maximum number of messages kept can be specified by <code>switchMaxKeptMsgsCount</code> property in <b>settings.js</b>.</p>
<h4>Rules</h4>
<p>There are four types of rule:</p>
<ol>
<li><b>Value</b> rules are evaluated against the configured property</li>
<li><b>Sequence</b> rules can be used on message sequences, such as those
generated by the Split node</li>
<li>A JSONata <b>Expression</b> can be provided that will be evaluated
against the whole message and will match if the expression returns
a true value.</li>
<li>An <b>Otherwise</b> rule can be used to match if none of the preceeding
rules have matched.</li>
</ol>
<h4>Handling message sequences</h4>
<p>By default, the node does not modify the <code>msg.parts</code> property of messages
that are part of a sequence.</p>
<p>The <b>recreate message sequences</b> option can be enabled to generate new message sequences
for each rule that matches. In this mode, the node will buffer the entire incoming
sequence before sending the new sequences on. The runtime setting `nodeMessageBufferMaxLength`
can be used to limit how many messages nodes will buffer.</p>
</script>
<script type="text/javascript">
@ -221,7 +131,6 @@
}
}
if ((rule.t === 'btwn') || (rule.t === 'index')) {
console.log(`; ${label}/${JSON.stringify(rule)}`);
label += " "+getValueLabel(rule.vt,rule.v)+" & "+getValueLabel(rule.v2t,rule.v2);
} else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'else' ) {
label += " "+getValueLabel(rule.vt,rule.v);

View File

@ -50,10 +50,10 @@ module.exports = function(RED) {
};
var _max_kept_msgs_count = undefined;
function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) {
var name = "switchMaxKeptMsgsCount";
var name = "nodeMessageBufferMaxLength";
if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name];
}
@ -164,7 +164,7 @@ module.exports = function(RED) {
pending_count -= group.msgs.length;
delete pending_in[id];
}
function add2pending_in(msg) {
var parts = msg.parts;
if (parts.hasOwnProperty("id") &&
@ -182,7 +182,7 @@ module.exports = function(RED) {
}
return true;
}
return false;
return false;
}
function send_group(onwards, port_count) {
@ -226,7 +226,7 @@ module.exports = function(RED) {
if (!(gid in pending_out)) {
pending_out[gid] = {
onwards: []
onwards: []
};
}
var group = pending_out[gid];

View File

@ -1,5 +1,9 @@
<script type="text/x-red" data-template-name="range">
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="common.label.property"></span></label>
<input type="text" id="node-input-property" style="width:calc(70% - 1px)"/>
</div>
<div class="form-row">
<label for="node-input-action"><i class="fa fa-dot-circle-o"></i> <span data-i18n="range.label.action"></span></label>
<select id="node-input-action" style="width:70%;">
@ -65,6 +69,7 @@
maxout: {value:"",required:true,validate:RED.validators.number()},
action: {value:"scale"},
round: {value:false},
property: {value:"payload",required:true},
name: {value:""}
},
inputs: 1,
@ -75,6 +80,12 @@
},
labelStyle: function() {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@ -24,11 +24,13 @@ module.exports = function(RED) {
this.maxin = Number(n.maxin);
this.minout = Number(n.minout);
this.maxout = Number(n.maxout);
this.property = n.property||"payload";
var node = this;
this.on('input', function (msg) {
if (msg.hasOwnProperty("payload")) {
var n = Number(msg.payload);
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
var n = Number(value);
if (!isNaN(n)) {
if (node.action == "clamp") {
if (n < node.minin) { n = node.minin; }
@ -38,11 +40,12 @@ module.exports = function(RED) {
var divisor = node.maxin - node.minin;
n = ((n - node.minin) % divisor + divisor) % divisor + node.minin;
}
msg.payload = ((n - node.minin) / (node.maxin - node.minin) * (node.maxout - node.minout)) + node.minout;
if (node.round) { msg.payload = Math.round(msg.payload); }
value = ((n - node.minin) / (node.maxin - node.minin) * (node.maxout - node.minout)) + node.minout;
if (node.round) { value = Math.round(value); }
RED.util.setMessageProperty(msg,node.property,value);
node.send(msg);
}
else { node.log(RED._("range.errors.notnumber")+": "+msg.payload); }
else { node.log(RED._("range.errors.notnumber")+": "+value); }
}
else { node.send(msg); } // If no payload - just pass it on.
});

View File

@ -90,11 +90,9 @@
a message and send each complete segment. If there is a partial segment at the end,
the node will hold on to it and prepend it to the next message that is received.
</p>
<p>When operating in this mode, the node will not set the `msg.parts.count`
<p>When operating in this mode, the node will not set the <code>msg.parts.count</code>
property as it does not know how many messages to expect in the stream. This
means it cannot be used with the <b>join</b> node in its automatic mode</p>
</script>
<script type="text/javascript">
@ -170,7 +168,6 @@
<select id="node-input-mode" style="width:200px;">
<option value="auto" data-i18n="join.mode.auto"></option>
<option value="custom" data-i18n="join.mode.custom"></option>
<option value="merge" data-i18n="join.mode.merge"></option>
<option value="reduce" data-i18n="join.mode.reduce"></option>
</select>
</div>
@ -219,18 +216,6 @@
</ul>
</div>
</div>
<div class="node-row-merge">
<div class="form-row">
<label data-i18n="join.merge.topics-label"></label>
<div class="form-row node-input-topics-container-row">
<ol id="node-input-topics-container"></ol>
</div>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-mergeOnChange" style="margin-left:10px; vertical-align:top; width:auto;">
<label for="node-input-mergeOnChange" style="width:auto;" data-i18n="join.merge.on-change"></label>
</div>
</div>
<div class="node-row-reduce">
<div class="form-row">
<label for="node-input-reduceExp" data-i18n="join.reduce.exp" style="margin-left:10px;"></label>
@ -259,16 +244,15 @@
</script>
<script type="text/x-red" data-help-name="join">
<p>Joins sequences of messages into a single message. This node provides four mode for message combination:</p>
<p>Joins sequences of messages into a single message.</p>
<p>There are three modes available:</p>
<dl>
<dt>automatic</dt>
<dd>When paired with the <b>split</b> node, it will automatically join the messages to reverse the split that was performed.</dd>
<dt>manual</dt>
<dd>It will join sequences of messages in a variety of ways.</dd>
<dt>merge sequence</dt>
<dd>It will merge incoming messages into single message using <code>topic</code> property.</dd>
<dd>Join sequences of messages in a variety of ways.</dd>
<dt>reduce sequence</dt>
<dd>When paired with the <b>split</b> node, it will reduce the message sequence into single message.</dd>
<dd>Apply an expression against all messages in a sequence to reduce it to a single message.</dd>
</dl>
<h3>Inputs</h3>
<dl class="message-properties">
@ -292,11 +276,13 @@
<h3>Details</h3>
<h4>Automatic mode</h4>
<p>When configured to join in manual mode, the node is able to join sequences of messages using <code>parts</code> property of incoming messages.</p>
<p>Automatic mode uses the <code>parts</code> property of incoming messages to
determine how the sequence should be joined. This allows it to automatically
reverse the action of a <b>split</b> node.
<h4>Manual mode</h4>
<p>When configured to join in manual mode, the node is able to join sequences
of messages in a variety of ways.</p>
of messages into a number of different results:</p>
<ul>
<li>a <b>string</b> or <b>buffer</b> - created by joining the selected property of each message with the specified join characters or buffer.</li>
<li>an <b>array</b> - created by adding each selected property, or entire message, to the output array.</li>
@ -311,49 +297,48 @@
<p>A <i>timeout</i> can be set to trigger sending the new message using whatever has been received so far.</p>
<p>If a message is received with the <b>msg.complete</b> property set, the output message is sent.</p>
<h4>Merge Sequence mode</h4>
<p>When configured to join in merge mode, the join node can create a message based on <code>topic</code> value.</p>
<p>Input messages are merged in order specified by <b>Topics</b> value.
<p>For example, if value of <b>Topics</b> is <b>x,x,y</b>, two input messages with topic <b>x</b> and one input message with topic <b>y</b> are merged into a new message in order of arrival.</p>
<p>If "Send merged message on arrival of a new topic" check box is selected, the last messages with each topic is kept internally and output message is sent when a message with new topics arrives.</p>
<p>The merged message contains <code>payload</code> property and properties for each topic. The <code>payload</code> property represents ordered array of payload value of input messages for each topic. The property for each topic represents a payload value for single occurrence of topic or array of payload values for multiple occurrences of the topic.</p>
<h4>Reduce Sequence mode</h4>
<p>When configured to join in reduce sequence mode, following values can be specified:</p>
<p>When configured to join in reduce mode, an expression is applied to each
message in a sequence and the result accumulated to produce a single message.</p>
<dl class="message-properties">
<dt>Reduce exp</dt>
<dd>JSONata expression for reducing message group. This expression represents the accumulated result. In the expression, following special variables can be used:
<ul>
<li><code>$A</code> accumulated value, </li>
<li><code>$I</code> index of the message in a group, </li>
<li><code>$N</code> number of messages of a group.</li>
</ul>
</dd>
<dt>Initial value</dt>
<dd>
initial value of reduction.
</dd>
<dt>Fixup exp</dt>
<dd>
JSONata expression applied after reduction of a message group completed. In the expression, following special variables can be used:
<dd>The initial value of the accumulated value (<code>$A</code>).</dd>
<dt>Reduce expression</dt>
<dd>A JSONata expression that is called for each message in the sequence.
The result is passed to the next call of the expression as the accumulated value.
In the expression, the following special variables can be used:
<ul>
<li><code>$A</code> accumulated value, </li>
<li><code>$N</code> number of messages of a group.</li>
<li><code>$A</code>: the accumulated value, </li>
<li><code>$I</code>: index of the message in the sequence, </li>
<li><code>$N</code>: number of messages in the sequence.</li>
</ul>
</dd>
<p>Order of reduction on a message group can be specified by checkbox (<b>Evaluate in reverse order (right to left)</b>).</p>
<dt>Fix-up expression</dt>
<dd>An optional JSONata expression that is applied after the reduce expression
has been applied to all messages in the sequence.
In the expression, following special variables can be used:
<ul>
<li><code>$A</code>: the accumulated value, </li>
<li><code>$N</code>: number of messages in the sequence.</li>
</ul>
</dd>
<p>By default, the reduce expression is applied in order, from the first
to the last message of the sequence. It can optionally be applied in
reverse order.</p>
</dl>
<p><b>Example:</b> Join node outputs an average for each input message group with the following setting:
<p><b>Example:</b> the following settings, given a sequence of numeric values,
calculates the average value:
<ul>
<li><b>Reduce exp</b>: <code>$A+payload</code></li>
<li><b>Reduce expression</b>: <code>$A+payload</code></li>
<li><b>Initial value</b>: <code>0</code></li>
<li><b>Fixup exp</b>: <code>$A/$N</code></li>
<li><b>Fix-up expression</b>: <code>$A/$N</code></li>
</ul>
</p>
<h4>Note:</h4>
<p>This node internally keeps messages for its operation. In order to prevent unexpected memory usage, maximum number of messages kept can be specified by <code>joinMaxKeptMsgsCount</code> property in <b>settings.js</b>.</p>
<h4>Storing messages</h4>
<p>This node will buffer messages internally in order to work across sequences. The
runtime setting `nodeMessageBufferMaxLength` can be used to limit how many messages nodes
will buffer.</p>
</script>
<script type="text/javascript">
@ -372,8 +357,6 @@
accumulate: { value:"false" },
timeout: {value:""},
count: {value:""},
topics: {value:[{topic:""}]},
mergeOnChange: {value:false},
reduceRight: {value:false},
reduceExp: {value:undefined},
reduceInit: {value:undefined},
@ -391,41 +374,10 @@
},
oneditprepare: function() {
var node = this;
var topic_str = node._("join.merge.topic");
function resizeTopics(topic) {
var newWidth = topic.width();
topic.find('.red-ui-typedInput')
.typedInput("width",newWidth-15);
}
$("#node-input-topics-container")
.css('min-height','250px').css('min-width','420px')
.editableList({
addItem: function(container,i,opt) {
if (!opt.hasOwnProperty('topic')) {
opt.topic = "";
}
var row = $('<div/>').appendTo(container);
var valueField = $('<input/>',{
class:"node-input-topic-value",
type:"text",
style:"margin-left: 5px;"
}).appendTo(row)
.typedInput({default:'str', types:['str']});
valueField.typedInput('value', opt.topic);
valueField.typedInput('type', 'str');
valueField.attr('placeholder', topic_str);
resizeTopics(container);
},
resizeItem: resizeTopics,
sortable: true,
removable: true
});
$("#node-input-mode").change(function(e) {
var val = $(this).val();
$(".node-row-custom").toggle(val==='custom');
$(".node-row-merge").toggle(val==='merge');
$(".node-row-reduce").toggle(val==='reduce');
$(".form-tips-auto").toggle((val==='auto') || (val==='reduce'));
if (val === "auto") {
@ -434,15 +386,6 @@
else if (val === "custom") {
$("#node-input-build").change();
}
else if (val === "merge") {
var topics = node.topics;
var container = $("#node-input-topics-container");
container.editableList('empty');
for (var i=0;i<topics.length;i++) {
var topic = topics[i];
container.editableList('addItem', topic);
}
}
else if (val === "reduce") {
var jsonata_or_empty = {
value: "jsonata",
@ -519,27 +462,6 @@
if (build !== 'object' && build !== 'merged') {
$("#node-input-accumulate").prop("checked",false);
}
var topics = $("#node-input-topics-container").editableList('items');
var node = this;
node.topics = [];
topics.each(function(i) {
var topicData = $(this).data('data');
var topic = $(this);
var vf = topic.find(".node-input-topic-value");
var value = vf.typedInput('value');
var r = {topic:value};
node.topics.push(r);
});
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-topics-container-row)");
var height = size.height;
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-topics-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-topics-container").editableList('height',height);
}
});
</script>

View File

@ -233,10 +233,10 @@ module.exports = function(RED) {
var _max_kept_msgs_count = undefined;
function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) {
var name = "joinMaxKeptMsgsCount";
var name = "nodeMessageBufferMaxLength";
if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name];
}
@ -247,89 +247,6 @@ module.exports = function(RED) {
return _max_kept_msgs_count;
}
function add_to_topic(node, pending, topic, msg) {
var merge_on_change = node.merge_on_change;
if (!pending.hasOwnProperty(topic)) {
pending[topic] = [];
}
var topics = pending[topic];
topics.push(msg);
if (merge_on_change) {
var counts = node.topic_counts;
if (topics.length > counts[topic]) {
topics.shift();
node.pending_count--;
}
}
}
function compute_topic_counts(topics) {
var counts = {};
for (var topic of topics) {
counts[topic] = (counts.hasOwnProperty(topic) ? counts[topic] : 0) +1;
}
return counts;
}
function try_merge(node, pending, merge_on_change) {
var topics = node.topics;
var counts = node.topic_counts;
for(var topic of topics) {
if(!pending.hasOwnProperty(topic) ||
(pending[topic].length < counts[topic])) {
return;
}
}
var merge_on_change = node.merge_on_change;
var msgs = [];
var vals = [];
var new_msg = {payload: vals};
for (var topic of topics) {
var pmsgs = pending[topic];
var msg = pmsgs.shift();
if (merge_on_change) {
pmsgs.push(msg);
}
var pval = msg.payload;
var val = new_msg[topic];
msgs.push(msg);
vals.push(pval);
if (val instanceof Array) {
new_msg[topic].push(pval);
}
else if (val === undefined) {
new_msg[topic] = pval;
}
else {
new_msg[topic] = [val, pval]
}
}
node.send(new_msg);
if (!merge_on_change) {
node.pending_count -= topics.length;
}
}
function merge_msg(node, msg) {
var topics = node.topics;
var topic = msg.topic;
if(node.topics.indexOf(topic) >= 0) {
var pending = node.pending;
if(node.topic_counts == undefined) {
node.topic_counts = compute_topic_counts(topics)
}
add_to_topic(node, pending, topic, msg);
node.pending_count++;
var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (node.pending_count > max_msgs)) {
node.pending = {};
node.pending_count = 0;
node.error(RED._("join.too-many"), msg);
}
try_merge(node, pending);
}
}
function apply_r(exp, accum, msg, index, count) {
exp.assign("I", index);
exp.assign("N", count);
@ -350,7 +267,7 @@ module.exports = function(RED) {
}
return exp
}
function reduce_and_send_group(node, group) {
var is_right = node.reduce_right;
var flag = is_right ? -1 : 1;
@ -374,15 +291,15 @@ module.exports = function(RED) {
}
node.send({payload: accum});
}
function reduce_msg(node, msg) {
if(msg.hasOwnProperty('parts')) {
var parts = msg.parts;
var pending = node.pending;
var pending_count = node.pending_count;
var gid = msg.parts.id;
var count;
if(!pending.hasOwnProperty(gid)) {
var count = undefined;
if(parts.hasOwnProperty('count')) {
count = msg.parts.count;
}
@ -568,10 +485,6 @@ module.exports = function(RED) {
node.warn("Message missing msg.parts property - cannot join in 'auto' mode")
return;
}
if (node.mode === 'merge' && !msg.hasOwnProperty("topic")) {
node.warn("Message missing msg.topic property - cannot join in 'merge' mode");
return;
}
if (node.propertyType == "full") {
property = msg;
@ -602,10 +515,6 @@ module.exports = function(RED) {
arrayLen = msg.parts.len;
propertyIndex = msg.parts.index;
}
else if (node.mode === 'merge') {
merge_msg(node, msg);
return;
}
else if (node.mode === 'reduce') {
reduce_msg(node, msg);
return;

View File

@ -81,7 +81,7 @@
</p>
<p><b>Note:</b> This node internally keeps messages for its operation. In order to prevent unexpected memory usage, maximum number of messages kept can be specified. Default is no limit on number of messages.
<ul>
<li><code>maxKeptMsgsCount</code> property set in <b>settings.js</b>.</li>
<li><code>nodeMessageBufferMaxLength</code> property set in <b>settings.js</b>.</li>
</ul>
</p>
</script>

View File

@ -21,7 +21,7 @@ module.exports = function(RED) {
function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) {
var name = "maxKeptMsgsCount";
var name = "nodeMessageBufferMaxLength";
if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name];
}

View File

@ -18,7 +18,7 @@
<script type="text/x-red" data-template-name="batch">
<div class="form-row">
<label for="node-input-mode"><i class="fa fa-dot-circle-o"></i> <span data-i18n="batch.mode.label"></span></label>
<label for="node-input-mode"><span data-i18n="batch.mode.label"></span></label>
<select type="text" id="node-input-mode" style="width: 300px;">
<option value="count" data-i18n="batch.mode.num-msgs"></option>
<option value="interval" data-i18n="batch.mode.interval"></option>
@ -28,26 +28,26 @@
<div class="node-row-msg-count">
<div class="form-row node-row-count">
<label for="node-input-count" data-i18n="batch.count.label"></label>
<input type="text" id="node-input-count" data-i18n="[placeholder]batch.count.count" style="width: 50px;">
<label style="margin-left: 10px; width: 175px;" for="node-input-count" data-i18n="batch.count.label"></label>
<input type="text" id="node-input-count" style="width: 50px;">
</div>
</div>
<div class="node-row-msg-overwrap">
<div class="form-row node-row-overwrap">
<label for="node-input-count" data-i18n="batch.count.overwrap"></label>
<input type="text" id="node-input-overwrap" data-i18n="[placeholder]batch.count.count" style="width: 50px;">
<div class="node-row-msg-overlap">
<div class="form-row node-row-overlap">
<label style="margin-left: 10px; width: 175px;" for="node-input-overlap" data-i18n="batch.count.overlap"></label>
<input type="text" id="node-input-overlap" style="width: 50px;">
</div>
</div>
<div class="node-row-msg-interval">
<div class="form-row node-row-interval">
<label for="node-input-interval"> <span data-i18n="batch.interval.label"></span></label>
<input type="text" id="node-input-interval" data-i18n="[placeholder]batch.interval.seconds" style="width: 50px;">
<span data-i18n="batch.interval.sec"></span>
<label style="margin-left: 10px; width: 175px;" for="node-input-interval"> <span data-i18n="batch.interval.label"></span></label>
<input type="text" id="node-input-interval" style="width: 50px;">
<span data-i18n="batch.interval.seconds"></span>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-allowEmptySequence" style="margin-left:10px; vertical-align:top; width:auto;">
<input type="checkbox" id="node-input-allowEmptySequence" style="margin-left:20px; margin-right: 10px; vertical-align:top; width:auto;">
<label for="node-input-allowEmptySequence" style="width:auto;" data-i18n="batch.interval.empty"></label>
</div>
</div>
@ -60,29 +60,40 @@
</div>
</div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</script>
<script type="text/x-red" data-help-name="batch">
<p>A function that divides input messages into multiple sequences of messages or concatenates multiple sequences of messages into a single messages sequence.</p>
<p>Creates sequences of messages based on various rules.</p>
<h3>Details</h3>
<h4>group by number of messages</h4>
<p>groups incoming messages into sequences of messages with specified counts. The output message group can be overwrapped.</p>
<h4>group by interval in seconds</h4>
<p>groups incoming messages received withn specified interval into sequences of messages. </p>
<h4>concatenate message groups</h4>
<p>creates a message sequence based on <code>topic</code> value of incoming message sequences.</p>
<p>Target and order of concatenated sequences are specified by <code>Topics</code> value. Selection of concatenated message groups is based on arrival of first message of the group.</p>
<p><b>Note:</b> This node internally keeps messages for its operation. In order to prevent unexpected memory usage, maximum number of messages kept can be specified. Default is no limit on number of messages.
<ul>
<li><code>batchMaxKeptMsgsCount</code> property set in <b>settings.js</b>.</li>
</ul>
</p>
<p>There are three modes for creating message sequences:</p>
<dl>
<dt>Number of messages</dt>
<dd>groups messages into sequences of a given length. The <b>overlap</b>
option specifies how many messages and the end of one sequence should be
repeated at the start of the next sequence.</dd>
<dt>Time interval</dt>
<dd>groups messages that arrive within the specified interval. If no messages
arrive within the interval, the node can optionally send on an empty message.</dd>
<dt>Concatenate Sequences</dt>
<dd>creates a message sequence by concatenating incoming sequences. Each sequence
must have a <code>msg.topic</code> property to identify it. The node is
configured with a list of <code>topic</code> values to identify the order
sequences are concatenated.
</dd>
</dd>
</dl>
<h4>Storing messages</h4>
<p>This node will buffer messages internally in order to work across sequences. The
runtime setting `nodeMessageBufferMaxLength` can be used to limit how many messages nodes
will buffer.</p>
</script>
<script type="text/javascript">
@ -93,9 +104,9 @@
name: {value:""},
mode: {value:"count"},
count: {value:10},
overwrap: {value:0},
overlap: {value:0},
interval: {value:10},
allowEmptySequence: {value:false},
allowEmptySequence: {value:false},
topics: {value:[{topic:""}]}
},
inputs:1,
@ -110,7 +121,7 @@
oneditprepare: function() {
var node = this;
var topic_str = node._("batch.concat.topic");
function resizeTopics(topic) {
var newWidth = topic.width();
topic.find('.red-ui-typedInput')
@ -130,27 +141,27 @@
type:"text",
style:"margin-left: 5px;"
}).appendTo(row)
.typedInput({default:'str', types:['str']});
valueField.typedInput('value', opt.topic);
valueField.typedInput('type', 'str');
.typedInput({default:'str', types:['str']});
valueField.typedInput('value', opt.topic);
valueField.typedInput('type', 'str');
valueField.attr('placeholder', topic_str);
resizeTopics(container);
resizeTopics(container);
},
resizeItem: resizeTopics,
resizeItem: resizeTopics,
sortable: true,
removable: true
});
$("#node-input-count").spinner({
});
$("#node-input-overwrap").spinner({
$("#node-input-overlap").spinner({
});
$("#node-input-interval").spinner({
});
$("#node-input-mode").change(function(e) {
var val = $(this).val();
$(".node-row-msg-count").toggle(val==="count");
$(".node-row-msg-overwrap").toggle(val==="count");
$(".node-row-msg-overlap").toggle(val==="count");
$(".node-row-msg-interval").toggle(val==="interval");
$(".node-row-msg-concat").toggle(val==="concat");
if (val==="concat") {
@ -171,9 +182,9 @@
topics.each(function(i) {
var topicData = $(this).data('data');
var topic = $(this);
var vf = topic.find(".node-input-topic-value");
var value = vf.typedInput('value');
var type = vf.typedInput('type');
var vf = topic.find(".node-input-topic-value");
var value = vf.typedInput('value');
var type = vf.typedInput('type');
var r = {topic:value};
node.topics.push(r);
});

View File

@ -21,7 +21,7 @@ module.exports = function(RED) {
function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) {
var name = "batchMaxKeptMsgsCount";
var name = "nodeMessageBufferMaxLength";
if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name];
}
@ -47,7 +47,7 @@ module.exports = function(RED) {
node.send(msg);
}
}
function send_interval(node, allow_empty_seq) {
let msgs = node.pending;
if (msgs.length > 0) {
@ -119,7 +119,7 @@ module.exports = function(RED) {
send_msgs(node, msgs, false);
node.pending_count -= msgs.length;
}
function add_to_topic_group(pending, topic, gid, msg) {
if (!pending.hasOwnProperty(topic)) {
pending[topic] = { groups: {}, gids: [] };
@ -171,9 +171,9 @@ module.exports = function(RED) {
node.pending_count = 0;
if (mode === "count") {
var count = Number(n.count || 1);
var overwrap = Number(n.overwrap || 0);
var is_overwrap = (overwrap > 0);
if (count <= overwrap) {
var overlap = Number(n.overlap || 0);
var is_overlap = (overlap > 0);
if (count <= overlap) {
node.error(RED._("batch.count.invalid"));
return;
}
@ -183,9 +183,9 @@ module.exports = function(RED) {
queue.push(msg);
node.pending_count++;
if (queue.length === count) {
send_msgs(node, queue, is_overwrap);
send_msgs(node, queue, is_overlap);
node.pending =
(overwrap === 0) ? [] : queue.slice(-overwrap);
(overlap === 0) ? [] : queue.slice(-overlap);
node.pending_count = 0;
}
var max_msgs = max_kept_msgs_count(node);

View File

@ -1,9 +1,5 @@
<script type="text/x-red" data-template-name="json">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-action"><span data-i18n="json.label.action"></span></label>
<select style="width:70%" id="node-input-action">
@ -14,11 +10,13 @@
</div>
<div class="form-row">
<label data-i18n="json.label.property"></label>
<input type="text" id="node-input-property" style="width: 70%"/>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<hr align="middle"/>
<div class="form-row node-json-to-json-options">
<label style="width:100%; border-bottom: 1px solid #eee;"><span data-i18n="json.label.o2j"></span></label>
</div>
@ -55,7 +53,6 @@
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
is selected.</p>
</script>
<script type="text/javascript">
@ -64,8 +61,8 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
property: { value:"payload" },
action: { value:"" },
property: {value:"payload",required:true},
action: {value:""},
pretty: {value:false}
},
inputs:1,

View File

@ -1,5 +1,9 @@
<script type="text/x-red" data-template-name="xml">
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@ -58,6 +62,7 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
property: {value:"payload",required:true},
attr: {value:""},
chr: {value:""}
},
@ -69,6 +74,12 @@
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@ -1,18 +1,3 @@
/**
* 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.
**/
module.exports = function(RED) {
"use strict";
@ -23,35 +8,39 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
this.attrkey = n.attr;
this.charkey = n.chr;
this.property = n.property||"payload";
var node = this;
this.on("input", function(msg) {
if (msg.hasOwnProperty("payload")) {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
var options;
if (typeof msg.payload === "object") {
if (typeof value === "object") {
options = {renderOpts:{pretty:false}};
if (msg.hasOwnProperty("options") && typeof msg.options === "object") { options = msg.options; }
options.async = false;
var builder = new xml2js.Builder(options);
msg.payload = builder.buildObject(msg.payload, options);
value = builder.buildObject(value, options);
RED.util.setMessageProperty(msg,node.property,value);
node.send(msg);
}
else if (typeof msg.payload == "string") {
else if (typeof value == "string") {
options = {};
if (msg.hasOwnProperty("options") && typeof msg.options === "object") { options = msg.options; }
options.async = true;
options.attrkey = node.attrkey || options.attrkey || '$';
options.charkey = node.charkey || options.charkey || '_';
parseString(msg.payload, options, function (err, result) {
parseString(value, options, function (err, result) {
if (err) { node.error(err, msg); }
else {
msg.payload = result;
value = result;
RED.util.setMessageProperty(msg,node.property,value);
node.send(msg);
}
});
}
else { node.warn(RED._("xml.errors.xml_js")); }
}
else { node.send(msg); } // If no payload - just pass it on.
else { node.send(msg); } // If no property - just pass it on.
});
}
RED.nodes.registerType("xml",XMLNode);

View File

@ -1,5 +1,9 @@
<script type="text/x-red" data-template-name="yaml">
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="common.label.property"></span></label>
<input type="text" id="node-input-property" style="width:calc(70% - 1px);"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@ -30,6 +34,7 @@
category: 'function',
color:"#DEBD5C",
defaults: {
property: {value:"payload",required:true},
name: {value:""}
},
inputs:1,
@ -40,6 +45,12 @@
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@ -4,20 +4,24 @@ module.exports = function(RED) {
var yaml = require('js-yaml');
function YAMLNode(n) {
RED.nodes.createNode(this,n);
this.property = n.property||"payload";
var node = this;
this.on("input", function(msg) {
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload === "string") {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (typeof value === "string") {
try {
msg.payload = yaml.load(msg.payload);
value = yaml.load(value);
RED.util.setMessageProperty(msg,node.property,value);
node.send(msg);
}
catch(e) { node.error(e.message,msg); }
}
else if (typeof msg.payload === "object") {
if (!Buffer.isBuffer(msg.payload)) {
else if (typeof value === "object") {
if (!Buffer.isBuffer(value)) {
try {
msg.payload = yaml.dump(msg.payload);
value = yaml.dump(value);
RED.util.setMessageProperty(msg,node.property,value);
node.send(msg);
}
catch(e) {

View File

@ -13,8 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var when = require("when");
var clients = [
{id:"node-red-editor",secret:"not_available"},
@ -25,9 +23,9 @@ module.exports = {
get: function(id) {
for (var i=0;i<clients.length;i++) {
if (clients[i].id == id) {
return when.resolve(clients[i]);
return Promise.resolve(clients[i]);
}
}
return when.resolve(null);
return Promise.resolve(null);
}
}

View File

@ -135,6 +135,66 @@ function completeVerify(profile,done) {
});
}
function genericStrategy(adminApp,strategy) {
var crypto = require("crypto")
var session = require('express-session')
var MemoryStore = require('memorystore')(session)
adminApp.use(session({
// As the session is only used across the life-span of an auth
// hand-shake, we can use a instance specific random string
secret: crypto.randomBytes(20).toString('hex'),
resave: false,
saveUninitialized: false,
store: new MemoryStore({
checkPeriod: 86400000 // prune expired entries every 24h
})
}));
//TODO: all passport references ought to be in ./auth
adminApp.use(passport.initialize());
adminApp.use(passport.session());
var options = strategy.options;
passport.use(new strategy.strategy(options,
function() {
var originalDone = arguments[arguments.length-1];
if (options.verify) {
var args = Array.from(arguments);
args[args.length-1] = function(err,profile) {
if (err) {
return originalDone(err);
} else {
return completeVerify(profile,originalDone);
}
};
options.verify.apply(null,args);
} else {
var profile = arguments[arguments.length - 2];
return completeVerify(profile,originalDone);
}
}
));
adminApp.get('/auth/strategy',
passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }),
completeGenerateStrategyAuth
);
adminApp.get('/auth/strategy/callback',
passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }),
completeGenerateStrategyAuth
);
}
function completeGenerateStrategyAuth(req,res) {
var tokens = req.user.tokens;
delete req.user.tokens;
// Successful authentication, redirect home.
res.redirect(settings.httpAdminRoot + '?access_token='+tokens.accessToken);
}
module.exports = {
init: init,
needsPermission: needsPermission,
@ -149,58 +209,5 @@ module.exports = {
},
login: login,
revoke: revoke,
genericStrategy: function(adminApp,strategy) {
var crypto = require("crypto")
var session = require('express-session')
var MemoryStore = require('memorystore')(session)
adminApp.use(session({
// As the session is only used across the life-span of an auth
// hand-shake, we can use a instance specific random string
secret: crypto.randomBytes(20).toString('hex'),
resave: false,
saveUninitialized: false,
store: new MemoryStore({
checkPeriod: 86400000 // prune expired entries every 24h
})
}));
//TODO: all passport references ought to be in ./auth
adminApp.use(passport.initialize());
adminApp.use(passport.session());
var options = strategy.options;
passport.use(new strategy.strategy(options,
function() {
var originalDone = arguments[arguments.length-1];
if (options.verify) {
var args = Array.prototype.slice.call(arguments);
args[args.length-1] = function(err,profile) {
if (err) {
return originalDone(err);
} else {
return completeVerify(profile,originalDone);
}
};
options.verify.apply(null,args);
} else {
var profile = arguments[arguments.length - 2];
return completeVerify(profile,originalDone);
}
}
));
adminApp.get('/auth/strategy', passport.authenticate(strategy.name));
adminApp.get('/auth/strategy/callback',
passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }),
function(req, res) {
var tokens = req.user.tokens;
delete req.user.tokens;
// Successful authentication, redirect home.
res.redirect(settings.httpAdminRoot + '?access_token='+tokens.accessToken);
}
);
}
genericStrategy: genericStrategy
}

View File

@ -14,8 +14,6 @@
* limitations under the License.
**/
var when = require("when");
function generateToken(length) {
var c = "ABCDEFGHIJKLMNOPQRSTUZWXYZabcdefghijklmnopqrstuvwxyz1234567890";
var token = [];
@ -49,7 +47,7 @@ function expireSessions() {
if (modified) {
return storage.saveSessions(sessions);
} else {
return when.resolve();
return Promise.resolve();
}
}
function loadSessions() {
@ -69,7 +67,7 @@ module.exports = {
// At this point, storage will not have been initialised, so defer loading
// the sessions until there's a request for them.
loadedSessions = null;
return when.resolve();
return Promise.resolve();
},
get: function(token) {
return loadSessions().then(function() {
@ -78,7 +76,7 @@ module.exports = {
return expireSessions().then(function() { return null });
}
}
return when.resolve(sessions[token]);
return Promise.resolve(sessions[token]);
});
},
create: function(user,client,scope) {

View File

@ -14,13 +14,12 @@
* limitations under the License.
**/
var when = require("when");
var util = require("util");
var clone = require("clone");
var bcrypt;
try { bcrypt = require('bcrypt'); }
catch(e) { bcrypt = require('bcryptjs'); }
var users = {};
var passwords = {};
var defaultUser = null;
function authenticate() {
@ -28,31 +27,33 @@ function authenticate() {
if (typeof username !== 'string') {
username = username.username;
}
var user = users[username];
if (user) {
if (arguments.length === 2) {
// Username/password authentication
var password = arguments[1];
return when.promise(function(resolve,reject) {
bcrypt.compare(password, passwords[username], function(err, res) {
resolve(res?user:null);
const args = Array.from(arguments);
return api.get(username).then(function(user) {
if (user) {
if (args.length === 2) {
// Username/password authentication
var password = args[1];
return new Promise(function(resolve,reject) {
bcrypt.compare(password, user.password, function(err, res) {
resolve(res?cleanUser(user):null);
});
});
});
} else {
// Try to extract common profile information
if (arguments[0].hasOwnProperty('photos') && arguments[0].photos.length > 0) {
user.image = arguments[0].photos[0].value;
} else {
// Try to extract common profile information
if (args[0].hasOwnProperty('photos') && args[0].photos.length > 0) {
user.image = args[0].photos[0].value;
}
return cleanUser(user);
}
return when.resolve(user);
}
}
return when.resolve(null);
return null;
});
}
function get(username) {
return when.resolve(users[username]);
return Promise.resolve(users[username]);
}
function getDefaultUser() {
return when.resolve(null);
return Promise.resolve(null);
}
var api = {
@ -63,7 +64,6 @@ var api = {
function init(config) {
users = {};
passwords = {};
defaultUser = null;
if (config.type == "credentials" || config.type == "strategy") {
if (config.users) {
@ -77,11 +77,7 @@ function init(config) {
}
for (var i=0;i<us.length;i++) {
var u = us[i];
users[u.username] = {
"username":u.username,
"permissions":u.permissions
};
passwords[u.username] = u.password;
users[u.username] = clone(u);
}
}
}
@ -96,7 +92,7 @@ function init(config) {
api.default = config.default;
} else {
api.default = function() {
return when.resolve({
return Promise.resolve({
"anonymous": true,
"permissions":config.default.permissions
});
@ -106,10 +102,17 @@ function init(config) {
api.default = getDefaultUser;
}
}
function cleanUser(user) {
if (user && user.hasOwnProperty('password')) {
user = clone(user);
delete user.password;
}
return user;
}
module.exports = {
init: init,
get: function(username) { return api.get(username) },
get: function(username) { return api.get(username).then(cleanUser)},
authenticate: function() { return api.authenticate.apply(null, arguments) },
default: function() { return api.default(); }
};

View File

@ -29,8 +29,7 @@ var npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
var paletteEditorEnabled = false;
var settings;
var moduleRe = /^[^/]+$/;
var moduleRe = /^(@[^/]+?[/])?[^/]+?$/;
var slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/;
function init(_settings) {
@ -71,7 +70,6 @@ function checkExistingModule(module,version) {
}
return false;
}
function installModule(module,version) {
//TODO: ensure module is 'safe'
return when.promise(function(resolve,reject) {
@ -99,47 +97,52 @@ function installModule(module,version) {
}
var installDir = settings.userDir || process.env.NODE_RED_HOME || ".";
var args = ['install','--save','--save-prefix="~"','--production',installName];
log.trace(npmCommand + JSON.stringify(args));
var child = child_process.execFile(npmCommand,args,
{
cwd: installDir
},
function(err, stdin, stdout) {
if (err) {
var e;
var lookFor404 = new RegExp(" 404 .*"+module+"$","m");
var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m");
if (lookFor404.test(stdout)) {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
reject(e);
} else if (isUpgrade && lookForVersionNotFound.test(stdout)) {
log.warn(log._("server.install.upgrade-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
reject(e);
} else {
log.warn(log._("server.install.install-failed-long",{name:module}));
log.warn("------------------------------------------");
log.warn(err.toString());
log.warn("------------------------------------------");
reject(new Error(log._("server.install.install-failed")));
}
var child = child_process.spawn(npmCommand,args,{
cwd: installDir,
shell: true
});
var output = "";
child.stdout.on('data', (data) => {
output += data;
});
child.stderr.on('data', (data) => {
output += data;
});
child.on('close', (code) => {
if (code !== 0) {
var e;
var lookFor404 = new RegExp(" 404 .*"+module,"m");
var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m");
if (lookFor404.test(output)) {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
reject(e);
} else if (isUpgrade && lookForVersionNotFound.test(output)) {
log.warn(log._("server.install.upgrade-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
reject(e);
} else {
if (!isUpgrade) {
log.info(log._("server.install.installed",{name:module}));
resolve(require("./index").addModule(module).then(reportAddedModules));
} else {
log.info(log._("server.install.upgraded",{name:module, version:version}));
events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true});
resolve(require("./registry").setModulePendingUpdated(module,version));
}
log.warn(log._("server.install.install-failed-long",{name:module}));
log.warn("------------------------------------------");
log.warn(output);
log.warn("------------------------------------------");
reject(new Error(log._("server.install.install-failed")));
}
} else {
if (!isUpgrade) {
log.info(log._("server.install.installed",{name:module}));
resolve(require("./index").addModule(module).then(reportAddedModules));
} else {
log.info(log._("server.install.upgraded",{name:module, version:version}));
events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true});
resolve(require("./registry").setModulePendingUpdated(module,version));
}
}
);
});
});
}

View File

@ -115,12 +115,26 @@ function getLocalNodeFiles(dir) {
function scanDirForNodesModules(dir,moduleName) {
var results = [];
var scopeName;
try {
var files = fs.readdirSync(dir);
if (moduleName) {
var m = /^(?:(@[^/]+)[/])?([^@/]+)/.exec(moduleName);
if (m) {
scopeName = m[1];
moduleName = m[2];
}
}
for (var i=0;i<files.length;i++) {
var fn = files[i];
if (/^@/.test(fn)) {
results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName));
if (scopeName && scopeName === fn) {
// Looking for a specific scope/module
results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName));
break;
} else {
results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName));
}
} else {
if (isIncluded(fn) && !isExcluded(fn) && (!moduleName || fn == moduleName)) {
var pkgfn = path.join(dir,fn,"package.json");

View File

@ -47,10 +47,11 @@ module.exports = {
// The maximum length, in characters, of any message sent to the debug sidebar tab
debugMaxLength: 1000,
// The maximum number of messages kept internally in nodes.
// Zero or undefined value means not restricting number of messages.
//maxKeptMsgsCount: 0,
// The maximum number of messages nodes will buffer internally as part of their
// operation. This applies across a range of nodes that operate on message sequences.
// defaults to no limit. A value of 0 also means no limit is applied.
//nodeMaxMessageBufferLength: 0,
// To disable the option for using local files for storing keys and certificates in the TLS configuration
// node, set this to true
//tlsConfigDisableLocalFiles: true,

View File

@ -75,6 +75,28 @@ describe('sentiment Node', function() {
});
});
it('should add a positive score for good words - alternative property', function(done) {
var flow = [{id:"jn1",type:"sentiment",property:"foo",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
try {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.be.above(10);
done();
} catch(err) {
done(err);
}
});
var testString = 'good, great, best, brilliant';
jn1.receive({foo:testString});
});
});
it('should add a negative score for bad words', function(done) {
var flow = [{id:"jn1",type:"sentiment",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
@ -93,6 +115,24 @@ describe('sentiment Node', function() {
});
});
it('should add a negative score for bad words - alternative property', function(done) {
var flow = [{id:"jn1",type:"sentiment",property:"foo",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.be.below(-10);
done();
});
var testString = 'bad, horrible, negative, awful';
jn1.receive({foo:testString});
});
});
it('should allow you to override word scoring', function(done) {
var flow = [{id:"jn1",type:"sentiment",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
@ -112,4 +152,23 @@ describe('sentiment Node', function() {
});
});
it('should allow you to override word scoring - alternative property', function(done) {
var flow = [{id:"jn1",type:"sentiment",property:"foo",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.equal(20);
done();
});
var testString = 'sick, wicked';
var overrides = {'sick': 10, 'wicked': 10 };
jn1.receive({foo:testString,overrides:overrides});
});
});
});

View File

@ -29,7 +29,7 @@ describe('switch Node', function() {
afterEach(function(done) {
helper.unload();
helper.stopServer(done);
RED.settings.switchMaxKeptMsgsCount = 0;
RED.settings.nodeMessageBufferMaxLength = 0;
});
it('should be loaded with some defaults', function(done) {
@ -692,7 +692,7 @@ describe('switch Node', function() {
];
helper.load(switchNode, flow, function() {
var n1 = helper.getNode("n1");
RED.settings.switchMaxKeptMsgsCount = 2;
RED.settings.nodeMessageBufferMaxLength = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "switch";

View File

@ -270,7 +270,7 @@ describe('JOIN node', function() {
afterEach(function() {
helper.unload();
RED.settings.joinMaxKeptMsgsCount = 0;
RED.settings.nodeMessageBufferMaxLength = 0;
});
it('should be loaded', function(done) {
@ -731,197 +731,6 @@ describe('JOIN node', function() {
});
});
it('should merge messages with topics (single)', function(done) {
var flow = [{id:"n1", type:"join", mode:"merge",
topics:[{topic:"TA"}, {topic:"TB"}],
wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var count = 0;
n2.on("input", function(msg) {
try {
msg.should.have.property("TA");
msg.should.have.property("TB");
msg.should.have.property("payload");
msg.payload.should.be.an.Array();
msg.payload.length.should.equal(2);
count++;
if (count === 1) {
msg.TA.should.equal("a");
msg.TB.should.equal("b");
msg.payload[0].should.equal("a");
msg.payload[1].should.equal("b");
}
if (count === 2) {
msg.TA.should.equal("d");
msg.TB.should.equal("c");
msg.payload[0].should.equal("d");
msg.payload[1].should.equal("c");
done();
}
}
catch(e) { done(e); }
});
n1.receive({payload:"a", topic:"TA"});
n1.receive({payload:"b", topic:"TB"});
n1.receive({payload:"c", topic:"TB"});
n1.receive({payload:"d", topic:"TA"});
});
});
it('should merge messages with topics (multiple)', function(done) {
var flow = [{id:"n1", type:"join", mode:"merge",
topics:[{topic:"TA"}, {topic:"TB"}, {topic:"TA"}],
wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var count = 0;
n2.on("input", function(msg) {
try {
msg.should.have.property("TA");
msg.TA.should.be.an.Array();
msg.TA.length.should.equal(2);
msg.should.have.property("TB");
msg.should.have.property("payload");
msg.payload.should.be.an.Array();
msg.payload.length.should.equal(3);
count++;
if (count === 1) {
msg.TA[0].should.equal("a");
msg.TA[1].should.equal("d");
msg.TB.should.equal("b");
msg.payload[0].should.equal("a");
msg.payload[1].should.equal("b");
msg.payload[2].should.equal("d");
}
if (count === 2) {
msg.TA[0].should.equal("e");
msg.TA[1].should.equal("f");
msg.TB.should.equal("c");
msg.payload[0].should.equal("e");
msg.payload[1].should.equal("c");
msg.payload[2].should.equal("f");
done();
}
}
catch(e) { done(e); }
});
n1.receive({payload:"a", topic:"TA"});
n1.receive({payload:"b", topic:"TB"});
n1.receive({payload:"c", topic:"TB"});
n1.receive({payload:"d", topic:"TA"});
n1.receive({payload:"e", topic:"TA"});
n1.receive({payload:"f", topic:"TA"});
});
});
it('should merge messages with topics (single, send on new topic)', function(done) {
var flow = [{id:"n1", type:"join", mode:"merge",
topics:[{topic:"TA"}, {topic:"TB"}],
mergeOnChange:true,
wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var count = 0;
n2.on("input", function(msg) {
try {
msg.should.have.property("TA");
msg.should.have.property("TB");
msg.should.have.property("payload");
msg.payload.should.be.an.Array();
msg.payload.length.should.equal(2);
count++;
if (count === 1) {
msg.TA.should.equal("a");
msg.TB.should.equal("b");
msg.payload[0].should.equal("a");
msg.payload[1].should.equal("b");
}
if (count === 2) {
msg.TA.should.equal("a");
msg.TB.should.equal("c");
msg.payload[0].should.equal("a");
msg.payload[1].should.equal("c");
}
if (count === 3) {
msg.TA.should.equal("d");
msg.TB.should.equal("c");
msg.payload[0].should.equal("d");
msg.payload[1].should.equal("c");
done();
}
}
catch(e) { done(e); }
});
n1.receive({payload:"a", topic:"TA"});
n1.receive({payload:"b", topic:"TB"});
n1.receive({payload:"c", topic:"TB"});
n1.receive({payload:"d", topic:"TA"});
});
});
it('should merge messages with topics (multiple, send on new topic)', function(done) {
var flow = [{id:"n1", type:"join", mode:"merge",
topics:[{topic:"TA"}, {topic:"TB"}, {topic:"TA"}],
mergeOnChange:true,
wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var count = 0;
n2.on("input", function(msg) {
try {
msg.should.have.property("TA");
msg.TA.should.be.an.Array();
msg.TA.length.should.equal(2);
msg.should.have.property("TB");
msg.should.have.property("payload");
msg.payload.should.be.an.Array();
msg.payload.length.should.equal(3);
count++;
if (count === 1) {
msg.TA[0].should.equal("a");
msg.TA[1].should.equal("c");
msg.TB.should.equal("b");
msg.payload[0].should.equal("a");
msg.payload[1].should.equal("b");
msg.payload[2].should.equal("c");
}
if (count === 2) {
msg.TA[0].should.equal("c");
msg.TA[1].should.equal("d");
msg.TB.should.equal("b");
msg.payload[0].should.equal("c");
msg.payload[1].should.equal("b");
msg.payload[2].should.equal("d");
}
if (count === 3) {
msg.TA[0].should.equal("c");
msg.TA[1].should.equal("d");
msg.TB.should.equal("e");
msg.payload[0].should.equal("c");
msg.payload[1].should.equal("e");
msg.payload[2].should.equal("d");
done();
}
}
catch(e) { done(e); }
});
n1.receive({payload:"a", topic:"TA"});
n1.receive({payload:"b", topic:"TB"});
n1.receive({payload:"c", topic:"TA"});
n1.receive({payload:"d", topic:"TA"});
n1.receive({payload:"e", topic:"TB"});
});
});
it('should redece messages', function(done) {
var flow = [{id:"n1", type:"join", mode:"reduce",
reduceRight:false,
@ -1065,31 +874,6 @@ describe('JOIN node', function() {
});
});
it('should handle too many pending messages for merge mode', function(done) {
var flow = [{id:"n1", type:"join", mode:"merge",
topics:[{topic:"TA"}, {topic:"TA"}, {topic:"TB"}],
wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
RED.settings.joinMaxKeptMsgsCount = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "join";
});
var evt = logEvents[0][0];
evt.should.have.property('id', "n1");
evt.should.have.property('type', "join");
evt.should.have.property('msg', "join.too-many");
done();
}, 150);
n1.receive({payload:"a", topic:"TA"});
n1.receive({payload:"b", topic:"TB"});
n1.receive({payload:"c", topic:"TB"});
n1.receive({payload:"d", topic:"TA"});
});
});
it('should handle too many pending messages for reduce mode', function(done) {
var flow = [{id:"n1", type:"join", mode:"reduce",
reduceRight:false,
@ -1101,7 +885,7 @@ describe('JOIN node', function() {
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
RED.settings.joinMaxKeptMsgsCount = 2;
RED.settings.nodeMessageBufferMaxLength = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "join";

View File

@ -27,7 +27,7 @@ describe('SORT node', function() {
afterEach(function() {
helper.unload();
RED.settings.maxKeptMsgsCount = 0;
RED.settings.nodeMessageBufferMaxLength = 0;
});
it('should be loaded', function(done) {
@ -254,7 +254,7 @@ describe('SORT node', function() {
{id:"n2", type:"helper"}];
helper.load(sortNode, flow, function() {
var n1 = helper.getNode("n1");
RED.settings.maxKeptMsgsCount = 2;
RED.settings.nodeMessageBufferMaxLength = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "sort";

View File

@ -28,7 +28,7 @@ describe('BATCH node', function() {
afterEach(function() {
helper.unload();
RED.settings.batchMaxKeptMsgsCount = 0;
RED.settings.nodeMessageBufferMaxLength = 0;
});
it('should be loaded with defaults', function(done) {
@ -81,7 +81,7 @@ describe('BATCH node', function() {
}
});
}
function check_count(flow, results, done) {
try {
helper.load(batchNode, flow, function() {
@ -106,13 +106,13 @@ describe('BATCH node', function() {
}, delay);
}
}
function check_interval(flow, results, delay, done) {
helper.load(batchNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
check_data(n1, n2, results, done);
delayed_send(n1, 0, 4, delay);
delayed_send(n1, 0, 4, delay);
});
}
@ -144,7 +144,7 @@ describe('BATCH node', function() {
describe('mode: count', function() {
it('should create seq. with count', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 2, overwrap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 2, overlap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[0, 1],
@ -153,9 +153,9 @@ describe('BATCH node', function() {
];
check_count(flow, results, done);
});
it('should create seq. with count and overwrap', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 3, overwrap: 2, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
it('should create seq. with count and overlap', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 3, overlap: 2, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[0, 1, 2],
@ -165,14 +165,14 @@ describe('BATCH node', function() {
];
check_count(flow, results, done);
});
it('should handle too many pending messages', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 5, overwrap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 5, overlap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(batchNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
RED.settings.batchMaxKeptMsgsCount = 2;
RED.settings.nodeMessageBufferMaxLength = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "batch";
@ -190,11 +190,11 @@ describe('BATCH node', function() {
});
});
describe('mode: interval', function() {
it('should create seq. with interval', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[0, 1],
@ -204,7 +204,7 @@ describe('BATCH node', function() {
});
it('should create seq. with interval (in float)', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 0.5, allowEmptySequence: false, topics: [], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 0.5, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[0, 1],
@ -214,32 +214,32 @@ describe('BATCH node', function() {
});
it('should create seq. with interval & not send empty seq', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
// 1300, 2600, 3900, 5200,
// 1300, 2600, 3900, 5200,
[0], [1], [2], [3]
];
check_interval(flow, results, 1300, done);
});
it('should create seq. with interval & send empty seq', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: true, topics: [], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: true, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
// 1300, 2600, 3900, 5200,
// 1300, 2600, 3900, 5200,
[null], [0], [1], [2], [null], [3]
];
check_interval(flow, results, 1300, done);
});
it('should handle too many pending messages', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(batchNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
RED.settings.batchMaxKeptMsgsCount = 2;
RED.settings.nodeMessageBufferMaxLength = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "batch";
@ -257,11 +257,11 @@ describe('BATCH node', function() {
});
});
describe('mode: concat', function() {
it('should concat two seq. (series)', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[2, 3, 0, 1]
@ -276,7 +276,7 @@ describe('BATCH node', function() {
});
it('should concat two seq. (mixed)', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[2, 3, 0, 1]
@ -291,7 +291,7 @@ describe('BATCH node', function() {
});
it('should concat three seq.', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}, {topic: "TC"}], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}, {topic: "TC"}], wires:[["n2"]]},
{id:"n2", type:"helper"}];
var results = [
[2, 3, 0, 1, 4]
@ -307,12 +307,12 @@ describe('BATCH node', function() {
});
it('should handle too many pending messages', function(done) {
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overlap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(batchNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
RED.settings.batchMaxKeptMsgsCount = 2;
RED.settings.nodeMessageBufferMaxLength = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "batch";

View File

@ -57,6 +57,26 @@ describe('XML node', function() {
});
});
it('should convert a valid xml string to a javascript object - alternative property', function(done) {
var flow = [{id:"n1",type:"xml",property:"foo",wires:[["n2"]],func:"return msg;"},
{id:"n2", type:"helper"}];
helper.load(xmlNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
msg.foo.should.have.property('employees');
msg.foo.employees.should.have.property('firstName');
should.equal(msg.foo.employees.firstName[0], 'John');
msg.foo.employees.should.have.property('lastName');
should.equal(msg.foo.employees.lastName[0], 'Smith');
done();
});
var string = '<employees><firstName>John</firstName><lastName>Smith</lastName></employees>';
n1.receive({foo:string,topic: "bar"});
});
});
it('should convert a valid xml string to a javascript object with options', function(done) {
var flow = [{id:"n1",type:"xml",wires:[["n2"]],func:"return msg;"},
{id:"n2", type:"helper"}];
@ -94,20 +114,20 @@ describe('XML node', function() {
});
});
it('should convert a javascript object to an xml string with options', function(done) {
var flow = [{id:"n1",type:"xml",wires:[["n2"]],func:"return msg;"},
it('should convert a javascript object to an xml string with options - alternative property', function(done) {
var flow = [{id:"n1",type:"xml",property:"foo",wires:[["n2"]],func:"return msg;"},
{id:"n2", type:"helper"}];
helper.load(xmlNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
var index = msg.payload.indexOf('<employees>\n <firstName>John</firstName>\n <lastName>Smith</lastName>\n</employees>');
var index = msg.foo.indexOf('<employees>\n <firstName>John</firstName>\n <lastName>Smith</lastName>\n</employees>');
index.should.be.above(-1);
done();
});
var obj = {"employees":{"firstName":["John"],"lastName":["Smith"] }};
n1.receive({payload:obj, topic:"bar", options:{headless:true}});
n1.receive({foo:obj, topic:"bar", options:{headless:true}});
});
});
@ -133,7 +153,7 @@ describe('XML node', function() {
},200);
});
});
it('should log an error if asked to parse something thats not xml or js', function(done) {
var flow = [{id:"n1",type:"xml",wires:[["n2"]],func:"return msg;"},
{id:"n2", type:"helper"}];

View File

@ -55,6 +55,24 @@ describe('YAML node', function() {
});
});
it('should convert a valid yaml string to a javascript object - using another property', function(done) {
var flow = [{id:"yn1",type:"yaml",property:"foo",wires:[["yn2"]],func:"return msg;"},
{id:"yn2", type:"helper"}];
helper.load(yamlNode, flow, function() {
var yn1 = helper.getNode("yn1");
var yn2 = helper.getNode("yn2");
yn2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
msg.foo.should.have.property('employees');
msg.foo.employees[0].should.have.property('firstName', 'John');
msg.foo.employees[0].should.have.property('lastName', 'Smith');
done();
});
var yamlString = "employees:\n - firstName: John\n lastName: Smith\n";
yn1.receive({foo:yamlString,topic: "bar"});
});
});
it('should convert a javascript object to a yaml string', function(done) {
var flow = [{id:"yn1",type:"yaml",wires:[["yn2"]],func:"return msg;"},
{id:"yn2", type:"helper"}];
@ -70,6 +88,21 @@ describe('YAML node', function() {
});
});
it('should convert a javascript object to a yaml string - using another property', function(done) {
var flow = [{id:"yn1",type:"yaml",property:"foo",wires:[["yn2"]],func:"return msg;"},
{id:"yn2", type:"helper"}];
helper.load(yamlNode, flow, function() {
var yn1 = helper.getNode("yn1");
var yn2 = helper.getNode("yn2");
yn2.on("input", function(msg) {
should.equal(msg.foo, "employees:\n - firstName: John\n lastName: Smith\n");
done();
});
var obj = {employees:[{firstName:"John", lastName:"Smith"}]};
yn1.receive({foo:obj});
});
});
it('should convert an array to a yaml string', function(done) {
var flow = [{id:"yn1",type:"yaml",wires:[["yn2"]],func:"return msg;"},
{id:"yn2", type:"helper"}];

View File

@ -19,6 +19,7 @@ var sinon = require("sinon");
var when = require("when");
var path = require("path");
var fs = require('fs');
var EventEmitter = require('events');
var child_process = require('child_process');
var installer = require("../../../../../red/runtime/nodes/registry/installer");
@ -31,6 +32,9 @@ describe('nodes/registry/installer', function() {
installer.init({});
});
afterEach(function() {
if (child_process.spawn.restore) {
child_process.spawn.restore();
}
if (child_process.execFile.restore) {
child_process.execFile.restore();
}
@ -58,8 +62,15 @@ describe('nodes/registry/installer', function() {
describe("installs module", function() {
it("rejects when npm returns a 404", function(done) {
sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) {
cb(new Error(),""," 404 this_wont_exist");
sinon.stub(child_process,"spawn",function(cmd,args,opt) {
var ee = new EventEmitter();
ee.stdout = new EventEmitter();
ee.stderr = new EventEmitter();
setTimeout(function() {
ee.stderr.emit('data'," 404 this_wont_exist");
ee.emit('close',1);
},10)
return ee;
});
installer.installModule("this_wont_exist").otherwise(function(err) {
@ -68,8 +79,15 @@ describe('nodes/registry/installer', function() {
});
});
it("rejects when npm does not find specified version", function(done) {
sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) {
cb(new Error(),""," version not found: this_wont_exist@0.1.2");
sinon.stub(child_process,"spawn",function(cmd,args,opt) {
var ee = new EventEmitter();
ee.stdout = new EventEmitter();
ee.stderr = new EventEmitter();
setTimeout(function() {
ee.stderr.emit('data'," version not found: this_wont_exist@0.1.2");
ee.emit('close',1);
},10)
return ee;
});
sinon.stub(typeRegistry,"getModuleInfo", function() {
return {
@ -94,8 +112,15 @@ describe('nodes/registry/installer', function() {
});
});
it("rejects with generic error", function(done) {
sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) {
cb(new Error("test_error"),"","");
sinon.stub(child_process,"spawn",function(cmd,args,opt,cb) {
var ee = new EventEmitter();
ee.stdout = new EventEmitter();
ee.stderr = new EventEmitter();
setTimeout(function() {
ee.stderr.emit('data'," kaboom!");
ee.emit('close',1);
},10)
return ee;
});
installer.installModule("this_wont_exist").then(function() {
@ -106,8 +131,14 @@ describe('nodes/registry/installer', function() {
});
it("succeeds when module is found", function(done) {
var nodeInfo = {nodes:{module:"foo",types:["a"]}};
sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) {
cb(null,"","");
sinon.stub(child_process,"spawn",function(cmd,args,opt) {
var ee = new EventEmitter();
ee.stdout = new EventEmitter();
ee.stderr = new EventEmitter();
setTimeout(function() {
ee.emit('close',0);
},10)
return ee;
});
var addModule = sinon.stub(registry,"addModule",function(md) {
return when.resolve(nodeInfo);
@ -146,8 +177,14 @@ describe('nodes/registry/installer', function() {
return when.resolve(nodeInfo);
});
var resourcesDir = path.resolve(path.join(__dirname,"..","resources","local","TestNodeModule","node_modules","TestNodeModule"));
sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) {
cb(null,"","");
sinon.stub(child_process,"spawn",function(cmd,args,opt) {
var ee = new EventEmitter();
ee.stdout = new EventEmitter();
ee.stderr = new EventEmitter();
setTimeout(function() {
ee.emit('close',0);
},10)
return ee;
});
installer.installModule(resourcesDir).then(function(info) {
info.should.eql(nodeInfo);