Compare commits

..

85 Commits

Author SHA1 Message Date
Nick O'Leary
50d7e16365 Bump for 0.19.6 2019-02-25 14:40:08 +00:00
Nick O'Leary
ef7bc931b7 Merge pull request #2051 from node-red-hitachi/fix-multi-byte-output
Fix output of multi-byte string with file node
2019-02-25 10:48:23 +00:00
Hiroyasu Nishiyama
a713c92530 convert to buffer before write 2019-02-07 22:46:21 +09:00
Hiroyasu Nishiyama
1db1ec7b5e fix encoding of file node from binary to utf8 2019-02-06 21:53:23 +09:00
Nick O'Leary
a6ef755139 Merge pull request #1993 from arunnattarayan/patch-1
Export to library produces empty folder when name has a trailing slash
2018-12-13 11:05:52 +00:00
Nick O'Leary
7b80ae42e1 removed regex in if statement
Co-Authored-By: arunnattarayan <arunkumarit02@gmail.com>
2018-12-06 19:20:43 +05:30
Arun Nattarayan
06a1f30350 Added validation while export into library 2018-11-30 20:04:12 +05:30
Nick O'Leary
2f93bb969b Merge pull request #1976 from MatthiasU/master
Add quotation marks for basic auth challenge
2018-11-13 23:16:41 +00:00
Matthias Uttendorfer
e094ea3d2a Add quotation marks for basic auth challenge
This is required by RFC 2617
2018-11-13 23:05:19 +01:00
Nick O'Leary
c685a31056 Backlevel chromedriver version due to bad semver
Chromedriver 2.41.1 cannot be installed on Node 4.
2018-10-30 23:27:44 +00:00
Nick O'Leary
c32ce3bb7b Bump for 0.19.5 2018-10-30 22:46:42 +00:00
Nephiel
4f87ebdf0a Recognize pip installs of RPi.GPIO (#1934)
Fixes "[warn] rpi-gpio : Cannot find Pi RPi.GPIO python library" when it is installed with pip using the default prefix (/usr/local).
2018-10-23 23:20:44 +01:00
Nick O'Leary
19b6cba398 Merge pull request #1941 from node-red-hitachi/master-batch
Prevent invalid value for batch node property
2018-10-23 10:47:48 +01:00
Nick O'Leary
5cf9c07b73 Merge pull request #1931 from node-red-hitachi/master-typedinput
Fix unnecessary TypedInput element
2018-10-19 10:57:13 +01:00
HirokiUchikawa
211eeea05d Set min value of properties and spinners for batch 2018-10-19 17:52:57 +09:00
HirokiUchikawa
51a3521834 Fix that unnecessary optionMenu remains 2018-10-16 19:48:50 +09:00
Nick O'Leary
6a9575e9f4 Merge pull request #1894 from node-red-hitachi/fix-overlapping-file-node-execution
fix multiple input message processing of file node
2018-10-15 03:15:05 -07:00
Nick O'Leary
62088259ae Merge pull request #1924 from imZack/patch-1
Add missing comma
2018-10-15 02:39:29 -07:00
YuLun Shih
925ebcc06e Add missing comma 2018-10-12 14:50:45 -07:00
Nick O'Leary
673a6bbe2c Do not disable context sidebar during node edit
Fixes #1921
2018-10-07 11:59:43 +01:00
Nick O'Leary
cf32a33984 Don't allow virtual links to be spliced
Fixes #1920
2018-10-07 11:55:43 +01:00
Nick O'Leary
98c1bc276d Merge project package changes to avoid overwritten changes 2018-10-07 11:37:50 +01:00
Nick O'Leary
629536b562 Handle manually added project deps that are unused
Fixes #1908
2018-10-07 11:11:30 +01:00
Hiroyasu Nishiyama
1441042458 update close & input handling of File node 2018-10-03 21:29:28 +09:00
Hiroyasu Nishiyama
58c8311d56 make close handler argument only one 2018-10-02 20:37:30 +09:00
Nick O'Leary
e03a0fffa9 Merge pull request #1907 from amilajack/patch-2
Change repo badge to point to master branch
2018-10-01 21:00:41 +01:00
Amila Welihinda
8e2c12f8d9 Change repo badge to point to master branch 2018-10-01 12:50:03 -07:00
Hiroyasu Nishiyama
7cec7ae608 invoke callbacks if async handler is specified 2018-09-30 22:30:19 +09:00
Nick O'Leary
c49f722e4f Merge pull request #1891 from camlow325/resolve-example-path-for-windows-support
Resolve path when sending example file for Windows support
2018-09-28 13:18:53 +01:00
Nick O'Leary
d4d95a43b6 Merge pull request #1900 from kazuhitoyokoi/master-addtestcases4settings.js
Add test cases for red/api/editor/settings.js
2018-09-28 13:17:32 +01:00
Hiroyasu Nishiyama
a345089c8b wait closing while penging messages exist 2018-09-26 12:39:12 +09:00
Kazuhito Yokoi
67c268e13d Add test cases for red/api/editor/settings.js 2018-09-26 10:22:01 +09:00
Nick O'Leary
ce85c8d986 Ensure all palette categories are opened properly
Closes #1893
2018-09-24 21:15:39 +01:00
Jeremy Barlow
cb35604ef5 Resolve path when sending example file for Windows support
Previously, when trying to import an example into the flow editor on
Windows, the load attempt would fail with an HTTP 404 error in the
browser client, with a `TypeError: path must be absolute or specify
root to res.sendFile` error being written to the Node-RED log. This was
due to the path being passed to the `res.sendFile` function not being
fully-qualified (for example, `\Users\myuser\...\example.json`).

With the changes in this commit, the path to the example file is
resolved to a fully-qualified path before being passed into the
`res.sendFile` call. For example, a path on Windows of
`\Users\myuser\...\example.json` would be transformed to
`C:\\Users\\myuser\\...\\example.json` before being passed along to the
`sendFile` function. This change allows the file to be loaded and sent
properly to the browser client and for the embedded flows in the example
to be loaded in the flow editor.
2018-09-21 08:15:00 -07:00
Hiroyasu Nishiyama
61681bb1d6 lift processQ function 2018-09-21 23:02:45 +09:00
Hiroyasu Nishiyama
1a226c4dc6 fix multiple input message processing of file node 2018-09-21 21:07:44 +09:00
Nick O'Leary
08fccc4e77 Update for 0.19.4 2018-09-17 11:26:06 +01:00
Nick O'Leary
c1d50e82e1 Fix race condition in non-cache lfs context
Fixes #1888
2018-09-17 10:31:00 +01:00
Nick O'Leary
9777af7cb5 LocalFileSystem Context: Remove extra flush code 2018-09-16 22:04:09 +01:00
Hiroki Uchikawa
fd86035865 Prevent race condition (#1889)
* Make pending Flag to be deleted after write process complete.

* Prevent executing write process until the previous process is completed

* Fix to prevent file write race condition when closing file context

* Make flushing rerun if pendingWrites was added
2018-09-16 21:15:23 +01:00
Nick O'Leary
a8ec032553 Allow context store name to be provided in the key
For nodes that get/set context, when multiple stores are configured
they will not know to parse the store name from the key. So they
will pass the store name in the key, such as #:(store)::key.

Currently that will cause that full string to be used as the key
and the default context store used - which is wrong.

The code now parses out the store name from the key if it is set -
athough if the call to get/set does include the store argument, it
will take precedence.

This only applies when the key is a string - it doesn't apply when
an array of keys is provided.
2018-09-14 23:21:05 +01:00
Nick O'Leary
66ee27c5fa Switch node: only use promises when absolutely necessary
Fixes a significant performance regression introduced when the
node was made async-only with the persistent context work.
2018-09-14 14:03:36 +01:00
Nick O'Leary
17a737ca88 Fix dbl-click handling on webkit-based browsers
d3.event.buttons is not as widely supported as I thought. Can
change this one instance as it is inside a click handler so
d3.event.button will be defined instead
2018-09-14 11:09:56 +01:00
Nick O'Leary
75e7c0e50d Ensure context.flow/global cannot be deleted or enumerated 2018-09-10 22:30:51 +01:00
Nick O'Leary
fc0cf1ff51 Handle context.get with multiple levels of unknown key
Fixes #1883
2018-09-09 23:47:31 +01:00
Nick O'Leary
0f4d46671f Fix global.get("foo.bar") for functionGlobalContext set values 2018-09-09 11:07:44 +01:00
Kazuhito Yokoi
048f9c0294 Fix node color bug (#1877)
* Fix node color bug

* Add color property into sample node

* Revert view.js

* Add color handling into getNodeColor()
2018-09-08 22:41:38 +01:00
Nick O'Leary
ca77842b5b Merge pull request #1857 from cclauss/patch-1
Define raw_input() in Python 3 & fix time.sleep()
2018-09-06 22:59:48 +01:00
Hiroyasu Nishiyama
6fa8b7f5f1 fix persistable context handling of sort node & existing error in testcases 2018-09-05 16:04:12 +01:00
Nick O'Leary
a2d03c14ae Update CHANGELOG for 0.19.3 2018-09-05 09:49:09 +01:00
Dave Conway-Jones
c667a0e74c debug node - show ring at start until first msg 2018-09-05 09:45:34 +01:00
Dave Conway-Jones
8123828113 improve split node accumulation test to include early complete 2018-09-05 08:36:56 +01:00
Dave Conway-Jones
72b8dbb45b Split node - fix complete to send msg for k/v object
and update info to try to clarify.
2018-09-04 22:54:28 +01:00
Dave Conway-Jones
7703875740 tidy split node merged object key typed input 2018-09-04 17:41:14 +01:00
Nick O'Leary
6442bb8a13 Set the JavaScript editor to full-screen 2018-09-04 13:30:06 +01:00
Nick O'Leary
f29d7c9252 Fixup localfilesystem registry test 2018-09-04 11:37:04 +01:00
Nick O'Leary
2f7f53ed96 Filter global modules installed locally
If a module is found both locally and globally installed, the local
copy will take precedence. This will allow a user to upgrade a
node module that they may not otherwise be able to touch
2018-09-04 11:26:05 +01:00
Nick O'Leary
94031a52a5 Add svg to permitted icon extension list 2018-08-31 21:02:09 +01:00
Dave Conway-Jones
d67f91e7ed debug node - indicate status all the time if selected to do so 2018-08-31 16:31:08 +01:00
Nick O'Leary
f37697c4fb Merge pull request #1870 from natcl/json-schema
JSON node: fix schema validation for obj -> obj or str -> str
2018-08-31 11:25:31 +01:00
Dave Conway-Jones
69448c7329 pi nodes - increase test coverage slightly 2018-08-30 20:54:03 +01:00
Dave Conway-Jones
8e9815fb91 TCP-request node - only write payload
to close #1869
2018-08-30 20:47:39 +01:00
Nathanaël Lécaudé
4cdd7978cf JSON schema: remove unused function 2018-08-29 13:40:37 -04:00
Nathanaël Lécaudé
40d81358f4 JSON schema: perform validation when obj -> obj or str -> str 2018-08-29 13:36:28 -04:00
Nathanaël Lécaudé
c7b62aed91 JSON schema: add draft-06 support (via $schema keyword) 2018-08-29 12:20:04 -04:00
Stefan Machmeier
c0e7d6d826 Mqtt proxy configuration for websocket connection, #1651. 2018-08-29 09:53:07 +01:00
Nick O'Leary
f809377de8 Merge pull request #1854 from kazuhitoyokoi/master-fixtypointestcase4functionnode
Fix typo in test case
2018-08-28 21:19:48 +01:00
Nick O'Leary
9767bd9697 Merge pull request #1860 from node-red-hitachi/uitest-refactoring
Refactored UI testing code following a design note
2018-08-28 21:06:36 +01:00
Nick O'Leary
3a55528552 Merge pull request #1861 from SPIRIT-21/master
Allows MQTT Shared Subscriptions for MQTT-In core node
2018-08-28 21:04:53 +01:00
Nick O'Leary
56197ffe3a Merge pull request #1851 from t4skforce/patch-1
fixed some linting issues
2018-08-28 20:58:51 +01:00
Nick O'Leary
0f0d0c046c Merge pull request #1853 from node-red-hitachi/fix-icon-spec-for-typedInput
Fix use of HTML tag or CSS class specification as icon of TypedInput
2018-08-28 20:44:06 +01:00
nakanishi
ecc4973645 Fixed the problems that were caused by timing issue 2018-08-27 17:34:04 +09:00
Nick O'Leary
3169f93cc2 Bump for 0.19.2 2018-08-24 13:25:02 +01:00
Nick O'Leary
c1a1a73599 Ensure node default color is used if palette.theme has no match 2018-08-24 13:08:49 +01:00
Christopher Hiller
db1b0ccb79 fix lost messages / properties in TCPRequest Node; closes #1863 (#1864)
- Added some more checks around this.
- We're choosing to only use the latest message when sending, which is
  effectively what was happening before the queue implementation.
2018-08-23 08:50:51 +01:00
Lars Oldiges
df161ce672 Allows MQTT Shared Subscriptions for MQTT-In core node 2018-08-22 13:20:49 +02:00
nakanishi
72fe30892e Refactor UI testing code following a design note 2018-08-22 14:36:30 +09:00
Nick O'Leary
d373105b32 Fix typo in template.html 2018-08-21 13:42:51 +01:00
Nick O'Leary
28b311b7ed Improve error reporting from context plugin loading 2018-08-16 14:36:11 +01:00
Nick O'Leary
dcda513901 Prevent no-op edit of node marking as changed due to icon 2018-08-16 10:54:27 +01:00
Nick O'Leary
72c400794c Change node must handle empty rule set 2018-08-16 09:41:43 +01:00
cclauss
4374506981 Define raw_input() in Python 3 & fix time.sleep()
* __raw_input()__ was removed in Python 3 in favor of __input()__
* Fix __sleep()__ to match the import on line 22

[flake8](http://flake8.pycqa.org) testing of https://github.com/node-red/node-red on Python 3.7.0

$ __flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics__
```
./nodes/core/hardware/nrgpio.py:45:24: F821 undefined name 'raw_input'
                data = raw_input()
                       ^
./nodes/core/hardware/nrgpio.py:63:24: F821 undefined name 'raw_input'
                data = raw_input()
                       ^
./nodes/core/hardware/nrgpio.py:85:24: F821 undefined name 'raw_input'
                data = raw_input()
                       ^
./nodes/core/hardware/nrgpio.py:120:24: F821 undefined name 'raw_input'
                data = raw_input()
                       ^
./nodes/core/hardware/nrgpio.py:134:24: F821 undefined name 'raw_input'
                data = raw_input()
                       ^
./nodes/core/hardware/nrgpio.py:164:24: F821 undefined name 'raw_input'
                data = raw_input()
                       ^
./nodes/core/hardware/nrgpio.py:201:17: F821 undefined name 'time'
                time.sleep(10)
                ^
7     F821 undefined name 'raw_input'
7
```

@dceejay
2018-08-15 16:25:58 +02:00
Kazuhito Yokoi
695873d35a Fix typo in test case for function node 2018-08-06 21:14:53 +09:00
Hiroyasu Nishiyama
15da19dcea fix use of HTML tag or CSS class specification as icon of typedInput 2018-08-06 10:28:02 +09:00
t4skforce
d5bdc1600b fixed some linting issues
* added some semicolons
* removed double parsing of ```err.stack``` into ```var stack```
2018-07-31 22:54:15 +02:00
72 changed files with 1719 additions and 681 deletions

View File

@@ -1,3 +1,74 @@
#### 0.19.6: Maintenance Release
- Fix encoding of file node from binary to utf8 - #2051
#### 0.19.5: Maintenance Release
- Recognize pip installs of RPi.GPIO (#1934)
- Merge pull request #1941 from node-red-hitachi/master-batch
- Merge pull request #1931 from node-red-hitachi/master-typedinput
- Set min value of properties and spinners for batch
- Fix that unnecessary optionMenu remains
- Merge pull request #1894 from node-red-hitachi/fix-overlapping-file-node-execution
- Merge pull request #1924 from imZack/patch-1
- Add missing comma
- Do not disable context sidebar during node edit Fixes #1921
- Don't allow virtual links to be spliced Fixes #1920
- Merge project package changes to avoid overwritten changes
- Handle manually added project deps that are unused Fixes #1908
- update close & input handling of File node
- make close handler argument only one
- Merge pull request #1907 from amilajack/patch-2
- Change repo badge to point to master branch
- invoke callbacks if async handler is specified
- Merge pull request #1891 from camlow325/resolve-example-path-for-windows-support
- Merge pull request #1900 from kazuhitoyokoi/master-addtestcases4settings.js
- wait closing while pending messages exist
- Add test cases for red/api/editor/settings.js
- Ensure all palette categories are opened properly Closes #1893
- Resolve path when sending example file for Windows support
- fix multiple input message processing of file node
#### 0.19.4: Maintenance Release
- Fix race condition in non-cache lfs context Fixes #1888
- LocalFileSystem Context: Remove extra flush code
- Prevent race condition in caching mode of lfs context (#1889)
- Allow context store name to be provided in the key
- Switch node: only use promises when absolutely necessary
- Fix dbl-click handling on webkit-based browsers
- Ensure context.flow/global cannot be deleted or enumerated
- Handle context.get with multiple levels of unknown key Fixes #1883
- Fix global.get("foo.bar") for functionGlobalContext set values
- Fix node color bug (#1877)
- Merge pull request #1857 from cclauss/patch-1
- Define raw_input() in Python 3 & fix time.sleep()
#### 0.19.3: Maintenance Release
- Split node - fix complete to send msg for k/v object
- Remove unused Join node merged object key typed input
- Set the JavaScript editor to full-screen
- Filter global modules installed locally
- Add svg to permitted icon extension list
- Debug node - indicate status all the time if selected to do so
- pi nodes - increase test coverage slightly
- TCP-request node - only write payload
- JSON schema: perform validation when obj -> obj or str -> str
- JSON schema: add draft-06 support (via $schema keyword)
- Mqtt proxy configuration for websocket connection, #1651.
- Allows MQTT Shared Subscriptions for MQTT-In core node
- Fix use of HTML tag or CSS class specification as icon of typedInput
#### 0.19.2: Maintenance Release
- Ensure node default colour is used if palette.theme has no match
- fix lost messages / properties in TCPRequest Node; closes #1863 (#1864)
- Fix typo in template.html
- Improve error reporting from context plugin loading
- Prevent no-op edit of node marking as changed due to icon
- Change node must handle empty rule set
#### 0.19.1: Maintenance Release
- Pull in latest twitter node

View File

@@ -2,7 +2,7 @@
http://nodered.org
[![Build Status](https://travis-ci.org/node-red/node-red.svg)](https://travis-ci.org/node-red/node-red)
[![Build Status](https://travis-ci.org/node-red/node-red.svg?branch=master)](https://travis-ci.org/node-red/node-red)
[![Coverage Status](https://coveralls.io/repos/node-red/node-red/badge.svg?branch=master)](https://coveralls.io/r/node-red/node-red?branch=master)
A visual tool for wiring the Internet of Things.

View File

@@ -522,13 +522,25 @@
this.selectLabel.empty();
var image;
if (opt.icon) {
image = new Image();
image.name = opt.icon;
image.src = opt.icon;
$('<img>',{src:opt.icon,style:"margin-right: 4px;height: 18px;"}).prependTo(this.selectLabel);
if (opt.icon.indexOf("<") === 0) {
$(opt.icon).prependTo(this.selectLabel);
}
else if (opt.icon.indexOf("/") !== -1) {
image = new Image();
image.name = opt.icon;
image.src = opt.icon;
$('<img>',{src:opt.icon,style:"margin-right: 4px;height: 18px;"}).prependTo(this.selectLabel);
}
else {
$('<i>',{class:"red-ui-typedInput-icon "+opt.icon}).prependTo(this.selectLabel);
}
} else {
this.selectLabel.text(opt.label);
}
if (this.optionMenu) {
this.optionMenu.remove();
this.optionMenu = null;
}
if (opt.options) {
if (this.optionExpandButton) {
this.optionExpandButton.hide();
@@ -619,10 +631,6 @@
}
}
} else {
if (this.optionMenu) {
this.optionMenu.remove();
this.optionMenu = null;
}
if (this.optionSelectTrigger) {
this.optionSelectTrigger.hide();
}

View File

@@ -881,7 +881,6 @@ RED.diff = (function() {
}
}
}
var properties = Object.keys(node).filter(function(p) { return p!='inputLabels'&&p!='outputLabels'&&p!='z'&&p!='wires'&&p!=='x'&&p!=='y'&&p!=='id'&&p!=='type'&&(!def.defaults||!def.defaults.hasOwnProperty(p))});
if (def.defaults) {
properties = properties.concat(Object.keys(def.defaults));
@@ -889,6 +888,13 @@ RED.diff = (function() {
if (node.type !== 'tab') {
properties = properties.concat(['inputLabels','outputLabels']);
}
if ( ((localNode && localNode.hasOwnProperty('icon')) || (remoteNode && remoteNode.hasOwnProperty('icon'))) &&
properties.indexOf('icon') === -1
) {
properties.unshift('icon');
}
properties.forEach(function(d) {
localChanged = false;
remoteChanged = false;

View File

@@ -1110,7 +1110,7 @@ RED.editor = (function() {
changed = true;
}
} else {
if (icon !== defaultIcon) {
if (icon !== "" && icon !== defaultIcon) {
changes.icon = editing_node.icon;
editing_node.icon = icon;
changed = true;

View File

@@ -32,7 +32,7 @@ RED.editor.types._js = (function() {
var trayOptions = {
title: options.title,
width: "inherit",
width: options.width||"inherit",
buttons: [
{
id: "node-dialog-cancel",

View File

@@ -447,7 +447,10 @@ RED.library = (function() {
click: function() {
//TODO: move this to RED.library
var flowName = $("#node-input-library-filename").val();
if (!/^\s*$/.test(flowName)) {
flowName = flowName.trim();
if(flowName === "" || flowName.endsWith("/")) {
RED.notify(RED._("library.invalidFilename"),"warning");
} else {
$.ajax({
url:'library/flows/'+flowName,
type: "POST",

View File

@@ -275,7 +275,8 @@ RED.palette = (function() {
}
for (var i=0;i<nodes.length;i++) {
if (d3.select(nodes[i]).classed('link_background')) {
var node = d3.select(nodes[i]);
if (node.classed('link_background') && !node.classed('link_link')) {
var length = nodes[i].getTotalLength();
for (var j=0;j<length;j+=10) {
var p = nodes[i].getPointAtLength(j);
@@ -320,9 +321,9 @@ RED.palette = (function() {
}
setLabel(nt,$(d),label,nodeInfo);
var categoryNode = $("#palette-container-"+category);
var categoryNode = $("#palette-container-"+rootCategory);
if (categoryNode.find(".palette_node").length === 1) {
categoryContainers[category].open();
categoryContainers[rootCategory].open();
}
}
@@ -459,7 +460,6 @@ RED.palette = (function() {
}
});
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]);

View File

@@ -461,7 +461,11 @@ RED.projects.settings = (function() {
setTimeout(function() {
depsList.editableList('removeItem',entry);
refreshModuleInUseCounts();
entry.count = modulesInUse[entry.id].count;
if (modulesInUse.hasOwnProperty(entry.id)) {
entry.count = modulesInUse[entry.id].count;
} else {
entry.count = 0;
}
depsList.editableList('addItem',entry);
},500);
}

View File

@@ -132,7 +132,7 @@ RED.sidebar.context = (function() {
content: content,
toolbar: footerToolbar,
// pinned: true,
enableOnEdit: false
enableOnEdit: true
});
// var toggleLiveButton = $("#sidebar-context-toggle-live");

View File

@@ -800,8 +800,9 @@ RED.utils = (function() {
var paletteTheme = RED.settings.theme('palette.theme') || [];
if (paletteTheme.length > 0) {
if (!nodeColorCache.hasOwnProperty(type)) {
nodeColorCache[type] = def.color;
var l = paletteTheme.length;
for (var i=0;i<l;i++ ){
for (var i = 0; i < l; i++ ){
var themeRule = paletteTheme[i];
if (themeRule.hasOwnProperty('category')) {
if (!themeRule.hasOwnProperty('_category')) {
@@ -825,7 +826,11 @@ RED.utils = (function() {
}
result = nodeColorCache[type];
}
return result;
if (result) {
return result;
} else {
return "#ddd";
}
}
function addSpinnerOverlay(container,contain) {

View File

@@ -1766,7 +1766,7 @@ RED.view = (function() {
clickTime = now;
dblClickPrimed = (lastClickNode == mousedown_node &&
d3.event.buttons === 1 &&
d3.event.button === 0 &&
!d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey);
lastClickNode = mousedown_node;
@@ -2415,6 +2415,7 @@ RED.view = (function() {
var l = d3.select(this);
d.added = true;
l.append("svg:path").attr("class","link_background link_path")
.classed("link_link", function(d) { return d.link })
.on("mousedown",function(d) {
mousedown_link = d;
clearSelection();

View File

@@ -67,6 +67,7 @@
},
inputs:1, // set the number of inputs - only 0 or 1
outputs:1, // set the number of outputs - 0 to n
color: "#ddd", // set icon color
// set the icon (held in icons dir below where you save the node)
icon: "myicon.png", // saved in icons/myicon.png
label: function() { // sets the default label contents

View File

@@ -19,7 +19,11 @@ module.exports = function(RED) {
if (this.tosidebar === undefined) { this.tosidebar = true; }
this.severity = n.severity || 40;
this.active = (n.active === null || typeof n.active === "undefined") || n.active;
this.status({});
if (this.tostatus) {
this.oldStatus = {fill:"grey", shape:"ring"};
this.status(this.oldStatus);
}
else { this.status({}); }
var node = this;
var levels = {
@@ -122,12 +126,12 @@ module.exports = function(RED) {
if (state === "enable") {
node.active = true;
res.sendStatus(200);
if (node.tostatus) { node.status({}); }
if (node.tostatus) { node.status({fill:"grey", shape:"dot"}); }
} else if (state === "disable") {
node.active = false;
res.sendStatus(201);
if (node.tostatus && node.hasOwnProperty("oldStatus")) {
node.oldStatus.shape = "ring";
node.oldStatus.shape = "dot";
node.status(node.oldStatus);
}
} else {

View File

@@ -126,6 +126,7 @@
var value = that.editor.getValue();
RED.editor.editJavaScript({
value: value,
width: "Infinity",
cursor: that.editor.getCursorPosition(),
complete: function(v,cursor) {
that.editor.setValue(v, -1);

View File

@@ -42,7 +42,7 @@ module.exports = function(RED) {
if (type === 'object') {
type = Buffer.isBuffer(msg)?'Buffer':(util.isArray(msg)?'Array':'Date');
}
node.error(RED._("function.error.non-message-returned",{ type: type }))
node.error(RED._("function.error.non-message-returned",{ type: type }));
}
}
}
@@ -203,9 +203,9 @@ module.exports = function(RED) {
if (util.hasOwnProperty('promisify')) {
sandbox.setTimeout[util.promisify.custom] = function(after, value) {
return new Promise(function(resolve, reject) {
sandbox.setTimeout(function(){ resolve(value) }, after);
sandbox.setTimeout(function(){ resolve(value); }, after);
});
}
};
}
var context = vm.createContext(sandbox);
try {
@@ -241,7 +241,6 @@ module.exports = function(RED) {
var line = 0;
var errorMessage;
var stack = err.stack.split(/\r?\n/);
if (stack.length > 0) {
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
line++;
@@ -265,13 +264,13 @@ module.exports = function(RED) {
});
this.on("close", function() {
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop())
clearTimeout(node.outstandingTimers.pop());
}
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop())
clearInterval(node.outstandingIntervals.pop());
}
this.status({});
})
});
} catch(err) {
// eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this
@@ -280,4 +279,4 @@ module.exports = function(RED) {
}
RED.nodes.registerType("function",FunctionNode);
RED.library.register("functions");
}
};

View File

@@ -77,7 +77,9 @@
}</pre>
<p>The resulting property will be:
<pre>Hello Fred. Today is Monday</pre>
<p>It is possible to use a property from the flow context or global context. Just use <code>{{flow.name}}</code> or <code>{{global.name}}</code>, or for persistable store <code>store</code> use <code>{{flow[store].name}}</code> or <code>{{flobal[store].name}}</code>.
<p>It is possible to use a property from the flow context or global context. Just use <code>{{flow.name}}</code> or
<code>{{global.name}}</code>, or for persistable store <code>store</code> use <code>{{flow[store].name}}</code> or
<code>{{global[store].name}}</code>.
<p><b>Note: </b>By default, <i>mustache</i> will escape any HTML entities in the values it substitutes.
To prevent this, use <code>{{{triple}}}</code> braces.
</script>

View File

@@ -24,8 +24,12 @@ module.exports = function(RED) {
try {
fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot
} catch(err) {
RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.libnotfound"));
allOK = false;
try {
fs.statSync("/usr/local/lib/python2.7/dist-packages/RPi/GPIO"); // installed with pip
} catch(err) {
RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.libnotfound"));
allOK = false;
}
}
}
}

View File

@@ -21,7 +21,12 @@ import os
import subprocess
from time import sleep
bounce = 25;
try:
raw_input # Python 2
except NameError:
raw_input = input # Python 3
bounce = 25
if len(sys.argv) > 2:
cmd = sys.argv[1].lower()
@@ -198,7 +203,7 @@ if len(sys.argv) > 2:
elif cmd == "kbd": # catch keyboard button events
try:
while not os.path.isdir("/dev/input/by-path"):
time.sleep(10)
sleep(10)
infile = subprocess.check_output("ls /dev/input/by-path/ | grep -m 1 'kbd'", shell=True).strip()
infile_path = "/dev/input/by-path/" + infile
EVENT_SIZE = struct.calcsize('llHHI')

View File

@@ -19,11 +19,26 @@ module.exports = function(RED) {
var mqtt = require("mqtt");
var util = require("util");
var isUtf8 = require('is-utf8');
var HttpsProxyAgent = require('https-proxy-agent');
var url = require('url');
function matchTopic(ts,t) {
if (ts == "#") {
return true;
}
/* The following allows shared subscriptions (as in MQTT v5)
http://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html#_Toc514345522
4.8.2 describes shares like:
$share/{ShareName}/{filter}
$share is a literal string that marks the Topic Filter as being a Shared Subscription Topic Filter.
{ShareName} is a character string that does not include "/", "+" or "#"
{filter} The remainder of the string has the same syntax and semantics as a Topic Filter in a non-shared subscription. Refer to section 4.7.
*/
else if(ts.startsWith("$share")){
ts = ts.replace(/^\$share\/[^#+/]+\/(.*)/g,"$1");
}
var re = new RegExp("^"+ts.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$");
return re.test(t);
}
@@ -96,12 +111,29 @@ module.exports = function(RED) {
if (typeof this.cleansession === 'undefined') {
this.cleansession = true;
}
var prox;
if (process.env.http_proxy != null) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY != null) { prox = process.env.HTTP_PROXY; }
// Create the URL to pass in to the MQTT.js library
if (this.brokerurl === "") {
// if the broker may be ws:// or wss:// or even tcp://
if (this.broker.indexOf("://") > -1) {
this.brokerurl = this.broker;
// Only for ws or wss, check if proxy env var for additional configuration
if (this.brokerurl.indexOf("wss://") > -1 || this.brokerurl.indexOf("ws://") > -1 )
// check if proxy is set in env
if (prox) {
var parsedUrl = url.parse(this.brokerurl);
var proxyOpts = url.parse(prox);
// true for wss
proxyOpts.secureEndpoint = parsedUrl.protocol ? parsedUrl.protocol === 'wss:' : true;
// Set Agent for wsOption in MQTT
var agent = new HttpsProxyAgent(proxyOpts);
this.options.wsOptions = {
agent: agent
}
}
} else {
// construct the std mqtt:// url
if (this.usetls) {
@@ -435,4 +467,4 @@ module.exports = function(RED) {
}
}
RED.nodes.registerType("mqtt out",MQTTOutNode);
};
};

View File

@@ -33,9 +33,7 @@ module.exports = function(RED) {
*/
const enqueue = (queue, item) => {
// drop msgs from front of queue if size is going to be exceeded
if (queue.size() === msgQueueSize) {
queue.shift();
}
if (queue.size() === msgQueueSize) { queue.shift(); }
queue.push(item);
return queue;
};
@@ -467,6 +465,7 @@ module.exports = function(RED) {
connecting: false
};
enqueue(clients[connection_id].msgQueue, msg);
clients[connection_id].lastMsg = msg;
if (!clients[connection_id].connecting && !clients[connection_id].connected) {
var buf;
@@ -507,8 +506,7 @@ module.exports = function(RED) {
clients[connection_id].client.on('data', function(data) {
if (node.out === "sit") { // if we are staying connected just send the buffer
if (clients[connection_id]) {
let msg = dequeue(clients[connection_id].msgQueue) || {};
clients[connection_id].msgQueue.unshift(msg);
const msg = clients[connection_id].lastMsg || {};
msg.payload = data;
node.send(RED.util.cloneMessage(msg));
}
@@ -530,8 +528,7 @@ module.exports = function(RED) {
clients[connection_id].timeout = setTimeout(function () {
if (clients[connection_id]) {
clients[connection_id].timeout = null;
let msg = dequeue(clients[connection_id].msgQueue) || {};
clients[connection_id].msgQueue.unshift(msg);
const msg = clients[connection_id].lastMsg || {};
msg.payload = Buffer.alloc(i+1);
buf.copy(msg.payload,0,0,i+1);
node.send(msg);
@@ -553,8 +550,7 @@ module.exports = function(RED) {
i += 1;
if ( i >= node.splitc) {
if (clients[connection_id]) {
let msg = dequeue(clients[connection_id].msgQueue) || {};
clients[connection_id].msgQueue.unshift(msg);
const msg = clients[connection_id].lastMsg || {};
msg.payload = Buffer.alloc(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
@@ -573,8 +569,7 @@ module.exports = function(RED) {
i += 1;
if (data[j] == node.splitc) {
if (clients[connection_id]) {
let msg = dequeue(clients[connection_id].msgQueue) || {};
clients[connection_id].msgQueue.unshift(msg);
const msg = clients[connection_id].lastMsg || {};
msg.payload = Buffer.alloc(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
@@ -649,7 +644,7 @@ module.exports = function(RED) {
}
else if (!clients[connection_id].connecting && clients[connection_id].connected) {
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].client.write(dequeue(clients[connection_id].msgQueue));
clients[connection_id].client.write(dequeue(clients[connection_id].msgQueue).payload);
}
}
});

View File

@@ -92,31 +92,80 @@ module.exports = function(RED) {
}
function getProperty(node,msg) {
return new Promise((resolve,reject) => {
if (node.useAsyncRules) {
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);
}
});
}
});
} else {
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);
}
});
try {
return RED.util.evaluateJSONataExpression(node.property,msg);
} catch(err) {
throw new Error(RED._("switch.errors.invalid-expr",{error:err.message}))
}
} else {
RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
try {
return RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg);
} catch(err) {
return undefined;
}
}
});
}
}
function getV1(node,msg,rule,hasParts) {
return new Promise( (resolve,reject) => {
if (node.useAsyncRules) {
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"); // TODO: ?! invalid case
} 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);
}
});
}
});
} else {
if (rule.vt === 'prev') {
resolve(node.previousValue);
return node.previousValue;
} else if (rule.vt === 'jsonata') {
var exp = rule.v;
if (rule.t === 'jsonata_exp') {
@@ -125,83 +174,120 @@ module.exports = function(RED) {
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);
}
});
try {
return RED.util.evaluateJSONataExpression(exp,msg);
} catch(err) {
throw new Error(RED._("switch.errors.invalid-expr",{error:err.message}))
}
} else if (rule.vt === 'json') {
resolve("json");
return "json"; // TODO: ?! invalid case
} else if (rule.vt === 'null') {
resolve("null");
return "null";
} else {
RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
try {
return RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg);
} catch(err) {
return undefined;
}
}
});
}
}
function getV2(node,msg,rule) {
return new Promise((resolve,reject) => {
if (node.useAsyncRules) {
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);
}
})
} else {
var v2 = rule.v2;
if (rule.v2t === 'prev') {
resolve(node.previousValue);
return 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);
}
});
try {
return RED.util.evaluateJSONataExpression(rule.v2,msg);
} catch(err) {
throw new Error(RED._("switch.errors.invalid-expr",{error:err.message}))
}
} else if (typeof v2 !== 'undefined') {
RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
try {
return RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg);
} catch(err) {
return undefined;
}
} else {
resolve(v2);
return v2;
}
})
}
}
function applyRule(node, msg, property, state) {
return new Promise((resolve,reject) => {
if (node.useAsyncRules) {
return new Promise((resolve,reject) => {
var rule = node.rules[state.currentRule];
var v1,v2;
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);
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;
}
} else {
state.onward.push(null);
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);
});
})
} else {
var rule = node.rules[state.currentRule];
var v1 = getV1(node,msg,rule,state.hasParts);
var v2 = getV2(node,msg,rule);
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 false;
}
resolve(state.currentRule < node.rules.length - 1);
});
})
} else {
state.onward.push(null);
}
return state.currentRule < node.rules.length - 1
}
}
function applyRules(node, msg, property,state) {
@@ -215,7 +301,18 @@ module.exports = function(RED) {
msg.parts.hasOwnProperty("index")
}
}
return applyRule(node,msg,property,state).then(hasMore => {
if (node.useAsyncRules) {
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;
}
});
} else {
var hasMore = applyRule(node,msg,property,state);
if (hasMore) {
state.currentRule++;
return applyRules(node,msg,property,state);
@@ -223,7 +320,7 @@ module.exports = function(RED) {
node.previousValue = property;
return state.onward;
}
});
}
}
@@ -248,6 +345,14 @@ module.exports = function(RED) {
var valid = true;
var repair = n.repair;
var needsCount = repair;
this.useAsyncRules = (
this.propertyType === 'flow' ||
this.propertyType === 'global' || (
this.propertyType === 'jsonata' &&
/\$(flow|global)Context/.test(this.property)
)
);
for (var i=0; i<this.rules.length; i+=1) {
var rule = this.rules[i];
needsCount = needsCount || ((rule.t === "tail") || (rule.t === "jsonata_exp"));
@@ -258,6 +363,13 @@ module.exports = function(RED) {
rule.vt = 'str';
}
}
this.useAsyncRules = this.useAsyncRules || (
rule.vt === 'flow' ||
rule.vt === 'global' || (
rule.vt === 'jsonata' &&
/\$(flow|global)Context/.test(rule.v)
)
);
if (rule.vt === 'num') {
if (!isNaN(Number(rule.v))) {
rule.v = Number(rule.v);
@@ -270,6 +382,9 @@ module.exports = function(RED) {
valid = false;
}
}
if (rule.vt === 'flow' || rule.vt === 'global' || rule.vt === 'jsonata') {
this.useAsyncRules = true;
}
if (typeof rule.v2 !== 'undefined') {
if (!rule.v2t) {
if (!isNaN(Number(rule.v2))) {
@@ -278,6 +393,13 @@ module.exports = function(RED) {
rule.v2t = 'str';
}
}
this.useAsyncRules = this.useAsyncRules || (
rule.v2t === 'flow' ||
rule.v2t === 'global' || (
rule.v2t === 'jsonata' &&
/\$(flow|global)Context/.test(rule.v2)
)
);
if (rule.v2t === 'num') {
rule.v2 = Number(rule.v2);
} else if (rule.v2t === 'jsonata') {
@@ -290,7 +412,6 @@ module.exports = function(RED) {
}
}
}
if (!valid) {
return;
}
@@ -420,18 +541,32 @@ module.exports = function(RED) {
if (needsCount && checkParts && hasParts) {
return addMessageToPending(msg);
}
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);
});
if (node.useAsyncRules) {
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);
});
} else {
try {
var property = getProperty(node,msg);
var onward = applyRules(node,msg,property);
if (!repair || !hasParts) {
node.send(onward);
} else {
sendGroupMessages(onward, msg);
}
} catch(err) {
node.warn(err);
}
}
}
function clearPending() {
@@ -473,7 +608,11 @@ module.exports = function(RED) {
}
this.on('input', function(msg) {
processMessageQueue(msg);
if (node.useAsyncRules) {
processMessageQueue(msg);
} else {
processMessage(msg,true);
}
});
this.on('close', function() {

View File

@@ -283,6 +283,9 @@ module.exports = function(RED) {
}
}
function applyRules(msg, currentRule) {
if (currentRule >= node.rules.length) {
return Promise.resolve(msg);
}
var r = node.rules[currentRule];
var rulePromise;
if (r.t === "move") {

View File

@@ -295,7 +295,8 @@
For object outputs, once this count has been reached, the node can be configured to send a message for each subsequent message
received.</p>
<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>
<p>If a message is received with the <b>msg.complete</b> property set, the output message is finalised and sent.
This resets any part counts.</p>
<h4>Reduce Sequence mode</h4>
<p>When configured to join in reduce mode, an expression is applied to each
@@ -439,10 +440,7 @@
$("#node-input-joiner").typedInput({
default: 'str',
typeField: $("#node-input-joinerType"),
types:[
'str',
'bin'
]
types:['str', 'bin']
});
$("#node-input-property").typedInput({
@@ -451,7 +449,7 @@
});
$("#node-input-key").typedInput({
types:['msg', {value:"merge", label:"", hasValue:false}]
types:['msg']
});
$("#node-input-build").change();

View File

@@ -586,7 +586,10 @@ module.exports = function(RED) {
}
else {
if (msg.hasOwnProperty('complete')) {
completeSend(partId);
if (inflight[partId]) {
inflight[partId].msg.complete = msg.complete;
completeSend(partId);
}
}
else {
node.warn("Message missing key property 'msg."+node.key+"' - cannot add to object")
@@ -594,6 +597,7 @@ module.exports = function(RED) {
}
return;
}
if (!inflight.hasOwnProperty(partId)) {
if (payloadType === 'object' || payloadType === 'merged') {
inflight[partId] = {
@@ -604,19 +608,6 @@ module.exports = function(RED) {
msg:RED.util.cloneMessage(msg)
};
}
else if (node.accumulate === true) {
if (msg.hasOwnProperty("reset")) { delete inflight[partId]; }
inflight[partId] = inflight[partId] || {
currentCount:0,
payload:{},
targetCount:targetCount,
type:payloadType,
msg:RED.util.cloneMessage(msg)
}
if (payloadType === 'string' || payloadType === 'array' || payloadType === 'buffer') {
inflight[partId].payload = [];
}
}
else {
inflight[partId] = {
currentCount:0,

View File

@@ -196,9 +196,10 @@ module.exports = function(RED) {
}).catch(err => {
node.error(err,msg);
});
return;
}
var parts = msg.parts;
if (!parts.hasOwnProperty("id") || !parts.hasOwnProperty("index")) {
if (!parts || !parts.hasOwnProperty("id") || !parts.hasOwnProperty("index")) {
return;
}
var gid = parts.id;
@@ -242,7 +243,8 @@ module.exports = function(RED) {
delete pending[key];
}
}
pending_count = 0; })
pending_count = 0;
});
}
RED.nodes.registerType("sort", SortNode);

View File

@@ -100,9 +100,9 @@
defaults: {
name: {value:""},
mode: {value:"count"},
count: {value:10},
overlap: {value:0},
interval: {value:10},
count: {value:10,validate:function(v) { return RED.validators.number(v) && (v >= 1); }},
overlap: {value:0,validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
interval: {value:10,validate:function(v) { return RED.validators.number(v) && (v >= 1); }},
allowEmptySequence: {value:false},
topics: {value:[{topic:""}]}
},
@@ -149,12 +149,9 @@
removable: true
});
$("#node-input-count").spinner({
});
$("#node-input-overlap").spinner({
});
$("#node-input-interval").spinner({
});
$("#node-input-count").spinner({min:1});
$("#node-input-overlap").spinner({min:0});
$("#node-input-interval").spinner({min:1});
$("#node-input-mode").change(function(e) {
var val = $(this).val();
$(".node-row-msg-count").toggle(val==="count");

View File

@@ -19,6 +19,7 @@ module.exports = function(RED) {
const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true, schemaId: 'auto'});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
function JSONNode(n) {
RED.nodes.createNode(this,n);
@@ -29,6 +30,7 @@ module.exports = function(RED) {
this.compiledSchema = null;
var node = this;
this.on("input", function(msg) {
var validate = false;
if (msg.schema) {
@@ -65,7 +67,17 @@ module.exports = function(RED) {
}
catch(e) { node.error(e.message,msg); }
} else {
node.send(msg);
// If node.action is str and value is str
if (validate) {
if (this.compiledSchema(JSON.parse(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);
}
}
}
else if (typeof value === "object") {
@@ -84,13 +96,22 @@ module.exports = function(RED) {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
node.send(msg);
}
}
catch(e) { node.error(RED._("json.errors.dropped-error")); }
}
else { node.warn(RED._("json.errors.dropped-object")); }
} else {
node.send(msg);
// If node.action is obj and value is object
if (validate) {
if (this.compiledSchema(value)) {
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);
}
}
}
else { node.warn(RED._("json.errors.dropped")); }

View File

@@ -28,9 +28,11 @@ module.exports = function(RED) {
this.createDir = n.createDir || false;
var node = this;
node.wstream = null;
node.data = [];
node.msgQueue = [];
node.closing = false;
node.closeCallback = null;
this.on("input",function(msg) {
function processMsg(msg, done) {
var filename = node.filename || msg.filename || "";
if ((!node.filename) && (!node.tout)) {
node.tout = setTimeout(function() {
@@ -41,6 +43,7 @@ module.exports = function(RED) {
}
if (filename === "") {
node.warn(RED._("file.errors.nofilename"));
done();
} else if (node.overwriteFile === "delete") {
fs.unlink(filename, function (err) {
if (err) {
@@ -51,6 +54,7 @@ module.exports = function(RED) {
}
node.send(msg);
}
done();
});
} else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) {
var dir = path.dirname(filename);
@@ -59,6 +63,7 @@ module.exports = function(RED) {
fs.ensureDirSync(dir);
} catch(err) {
node.error(RED._("file.errors.createfail",{error:err.toString()}),msg);
done();
return;
}
}
@@ -70,85 +75,143 @@ module.exports = function(RED) {
if (typeof data === "boolean") { data = data.toString(); }
if (typeof data === "number") { data = data.toString(); }
if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; }
node.data.push({msg:msg,data:Buffer.from(data)});
while (node.data.length > 0) {
if (node.overwriteFile === "true") {
(function(packet) {
node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true });
node.wstream.on("error", function(err) {
node.error(RED._("file.errors.writefail",{error:err.toString()}),msg);
});
node.wstream.on("open", function() {
node.wstream.end(packet.data, function() {
node.send(packet.msg);
});
})
})(node.data.shift());
}
else {
// Append mode
var recreateStream = !node.wstream || !node.filename;
if (node.wstream && node.wstreamIno) {
// There is already a stream open and we have the inode
// of the file. Check the file hasn't been deleted
// or deleted and recreated.
try {
var stat = fs.statSync(filename);
// File exists - check the inode matches
if (stat.ino !== node.wstreamIno) {
// The file has been recreated. Close the current
// stream and recreate it
recreateStream = true;
node.wstream.end();
delete node.wstream;
delete node.wstreamIno;
}
} catch(err) {
// File does not exist
var buf = Buffer.from(data);
if (node.overwriteFile === "true") {
var wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true });
node.wstream = wstream;
wstream.on("error", function(err) {
node.error(RED._("file.errors.writefail",{error:err.toString()}),msg);
done();
});
wstream.on("open", function() {
wstream.end(buf, function() {
node.send(msg);
done();
});
})
return;
}
else {
// Append mode
var recreateStream = !node.wstream || !node.filename;
if (node.wstream && node.wstreamIno) {
// There is already a stream open and we have the inode
// of the file. Check the file hasn't been deleted
// or deleted and recreated.
try {
var stat = fs.statSync(filename);
// File exists - check the inode matches
if (stat.ino !== node.wstreamIno) {
// The file has been recreated. Close the current
// stream and recreate it
recreateStream = true;
node.wstream.end();
delete node.wstream;
delete node.wstreamIno;
}
}
if (recreateStream) {
node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'a', autoClose:true });
node.wstream.on("open", function(fd) {
try {
var stat = fs.statSync(filename);
node.wstreamIno = stat.ino;
} catch(err) {
}
});
node.wstream.on("error", function(err) {
node.error(RED._("file.errors.appendfail",{error:err.toString()}),msg);
});
}
if (node.filename) {
// Static filename - write and reuse the stream next time
var packet = node.data.shift()
node.wstream.write(packet.data, function() {
node.send(packet.msg);
});
} else {
// Dynamic filename - write and close the stream
var packet = node.data.shift()
node.wstream.end(packet.data, function() {
node.send(packet.msg);
});
} catch(err) {
// File does not exist
recreateStream = true;
node.wstream.end();
delete node.wstream;
delete node.wstreamIno;
}
}
if (recreateStream) {
node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'a', autoClose:true });
node.wstream.on("open", function(fd) {
try {
var stat = fs.statSync(filename);
node.wstreamIno = stat.ino;
} catch(err) {
}
});
node.wstream.on("error", function(err) {
node.error(RED._("file.errors.appendfail",{error:err.toString()}),msg);
done();
});
}
if (node.filename) {
// Static filename - write and reuse the stream next time
node.wstream.write(buf, function() {
node.send(msg);
done();
});
} else {
// Dynamic filename - write and close the stream
node.wstream.end(buf, function() {
node.send(msg);
delete node.wstream;
delete node.wstreamIno;
done();
});
}
}
}
else {
done();
}
}
function processQ(queue) {
var msg = queue[0];
processMsg(msg, function() {
queue.shift();
if (queue.length > 0) {
processQ(queue);
}
else if (node.closing) {
closeNode();
}
});
}
this.on("input", function(msg) {
var msgQueue = node.msgQueue;
if (msgQueue.push(msg) > 1) {
// pending write exists
return;
}
try {
processQ(msgQueue);
}
catch (e) {
node.msgQueue = [];
if (node.closing) {
closeNode();
}
throw e;
}
});
this.on('close', function() {
function closeNode() {
if (node.wstream) { node.wstream.end(); }
if (node.tout) { clearTimeout(node.tout); }
node.status({});
var cb = node.closeCallback;
node.closeCallback = null;
node.closing = false;
if (cb) {
cb();
}
}
this.on('close', function(done) {
if (node.closing) {
// already closing
return;
}
node.closing = true;
if (done) {
node.closeCallback = done;
}
if (node.msgQueue.length > 0) {
// close after queue processed
return;
}
else {
closeNode();
}
});
}
RED.nodes.registerType("file",FileNode);

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "0.19.1",
"version": "0.19.6",
"description": "A visual tool for wiring the Internet of Things",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@@ -33,8 +33,8 @@
"flow"
],
"dependencies": {
"ajv": "6.5.2",
"basic-auth": "2.0.0",
"ajv": "6.5.4",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"body-parser": "1.18.3",
"cheerio": "0.22.0",
@@ -42,13 +42,14 @@
"cookie": "0.3.1",
"cookie-parser": "1.4.3",
"cors": "2.8.4",
"cron": "1.3.0",
"cron": "1.5.0",
"denque": "1.3.0",
"express": "4.16.3",
"express": "4.16.4",
"express-session": "1.15.6",
"fs-extra": "5.0.0",
"fs.notify": "0.0.4",
"hash-sum": "1.0.2",
"https-proxy-agent": "2.2.1",
"i18next": "11.6.0",
"is-utf8": "0.2.1",
"js-yaml": "3.12.0",
@@ -56,9 +57,9 @@
"jsonata": "1.5.4",
"media-typer": "0.3.0",
"memorystore": "1.6.0",
"mqtt": "2.18.3",
"multer": "1.3.1",
"mustache": "2.3.1",
"mqtt": "2.18.8",
"multer": "1.4.1",
"mustache": "2.3.2",
"node-red-node-email": "0.1.*",
"node-red-node-feedparser": "^0.1.12",
"node-red-node-rbe": "0.2.*",
@@ -71,9 +72,9 @@
"passport-oauth2-client-password": "0.1.2",
"raw-body": "2.3.3",
"request": "2.88.0",
"semver": "5.5.0",
"semver": "5.6.0",
"sentiment": "2.1.0",
"uglify-js": "3.4.7",
"uglify-js": "3.4.9",
"when": "3.7.8",
"ws": "1.1.5",
"xml2js": "0.4.19"
@@ -82,10 +83,10 @@
"bcrypt": "~2.0.0"
},
"devDependencies": {
"chromedriver": "^2.41.0",
"chromedriver": "2.41.0",
"grunt": "~1.0.3",
"grunt-chmod": "~1.1.1",
"grunt-cli": "~1.2.0",
"grunt-cli": "~1.3.1",
"grunt-concurrent": "~2.3.1",
"grunt-contrib-clean": "~1.1.0",
"grunt-contrib-compress": "~1.4.0",
@@ -103,15 +104,15 @@
"http-proxy": "^1.16.2",
"istanbul": "0.4.5",
"mocha": "^5.2.0",
"node-red-node-test-helper": "0.1.7",
"should": "^8.4.0",
"sinon": "1.17.7",
"stoppable": "^1.0.6",
"supertest": "3.1.0",
"stoppable": "^1.0.7",
"supertest": "3.3.0",
"wdio-chromedriver-service": "^0.1.3",
"wdio-mocha-framework": "^0.6.2",
"wdio-spec-reporter": "^0.1.5",
"webdriverio": "^4.13.1",
"node-red-node-test-helper": "^0.1.7"
"webdriverio": "^4.14.0"
},
"engines": {
"node": ">=4"

2
red.js
View File

@@ -240,7 +240,7 @@ function basicAuthMiddleware(user,pass) {
}
var requestUser = basicAuth(req);
if (!requestUser || requestUser.name !== user || !checkPasswordAndCache(requestUser.pass)) {
res.set('WWW-Authenticate', 'Basic realm=Authorization Required');
res.set('WWW-Authenticate', 'Basic realm="Authorization Required"');
return res.sendStatus(401);
}
next();

View File

@@ -102,9 +102,10 @@ module.exports = {
var fullPath = redNodes.getNodeExampleFlowPath(module,path);
if (fullPath) {
try {
fs.statSync(fullPath);
var resolvedPath = fspath.resolve(fullPath);
fs.statSync(resolvedPath);
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
return res.sendFile(fullPath,{
return res.sendFile(resolvedPath,{
headers:{
'Content-Type': 'application/json'
}

View File

@@ -162,12 +162,10 @@ function start() {
if (settings.httpStatic) {
log.info(log._("runtime.paths.httpStatic",{path:path.resolve(settings.httpStatic)}));
}
redNodes.loadContextsPlugin().then(function () {
return redNodes.loadContextsPlugin().then(function () {
redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {});
started = true;
});
}).catch(function(err) {
console.log(err);
});
});
}

View File

@@ -163,7 +163,12 @@
"error-module-not-defined": "Context store '__storage__' missing 'module' option",
"error-invalid-module-name": "Invalid context store name: '__name__'",
"error-invalid-default-module": "Default context store unknown: '__storage__'",
"unknown-store": "Unknown context store '__name__' specified. Using default store."
"unknown-store": "Unknown context store '__name__' specified. Using default store.",
"error-loading-module": "Error loading context store: __message__",
"localfilesystem": {
"error-circular": "Context __scope__ contains a circular reference that cannot be persisted",
"error-write": "Error writing context: __message__"
}
}
}

View File

@@ -17,6 +17,7 @@
var clone = require("clone");
var log = require("../../log");
var memory = require("./memory");
var util = require("../../util");
var settings;
@@ -170,6 +171,8 @@ function load() {
defaultStore = "memory";
}
return resolve(Promise.all(promises));
}).catch(function(err) {
throw new Error(log._("context.error-loading-module",{message:err.toString()}));
});
}
@@ -207,104 +210,131 @@ function createContext(id,seed) {
insertSeedValues = function(keys,values) {
if (!Array.isArray(keys)) {
if (values[0] === undefined) {
values[0] = seed[keys];
values[0] = util.getObjectProperty(seed,keys);
}
} else {
for (var i=0;i<keys.length;i++) {
if (values[i] === undefined) {
values[i] = seed[keys[i]];
values[i] = util.getObjectProperty(seed,keys[i]);
}
}
}
}
}
Object.defineProperties(obj, {
get: {
value: function(key, storage, callback) {
var context;
obj.get = function(key, storage, callback) {
var context;
if (!storage && !callback) {
context = stores["_"];
} else {
if (typeof storage === 'function') {
callback = storage;
storage = "_";
}
if (callback && typeof callback !== 'function'){
throw new Error("Callback must be a function");
}
context = getContextStorage(storage);
}
if (callback) {
if (!seed) {
context.get(scope,key,callback);
} else {
context.get(scope,key,function() {
if (arguments[0]) {
callback(arguments[0]);
return;
if (!callback && typeof storage === 'function') {
callback = storage;
storage = undefined;
}
if (callback && typeof callback !== 'function'){
throw new Error("Callback must be a function");
}
if (!Array.isArray(key)) {
var keyParts = util.parseContextStore(key);
key = keyParts.key;
if (!storage) {
storage = keyParts.store || "_";
}
var results = Array.prototype.slice.call(arguments,[1]);
insertSeedValues(key,results);
// Put the err arg back
results.unshift(undefined);
callback.apply(null,results);
});
}
} else {
// No callback, attempt to do this synchronously
var results = context.get(scope,key);
if (seed) {
if (Array.isArray(key)) {
insertSeedValues(key,results);
} else if (results === undefined){
results = seed[key];
} else {
if (!storage) {
storage = "_";
}
}
context = getContextStorage(storage);
if (callback) {
if (!seed) {
context.get(scope,key,callback);
} else {
context.get(scope,key,function() {
if (arguments[0]) {
callback(arguments[0]);
return;
}
var results = Array.prototype.slice.call(arguments,[1]);
insertSeedValues(key,results);
// Put the err arg back
results.unshift(undefined);
callback.apply(null,results);
});
}
} else {
// No callback, attempt to do this synchronously
var results = context.get(scope,key);
if (seed) {
if (Array.isArray(key)) {
insertSeedValues(key,results);
} else if (results === undefined){
results = util.getObjectProperty(seed,key);
}
}
return results;
}
}
},
set: {
value: function(key, value, storage, callback) {
var context;
if (!callback && typeof storage === 'function') {
callback = storage;
storage = undefined;
}
if (callback && typeof callback !== 'function'){
throw new Error("Callback must be a function");
}
if (!Array.isArray(key)) {
var keyParts = util.parseContextStore(key);
key = keyParts.key;
if (!storage) {
storage = keyParts.store || "_";
}
} else {
if (!storage) {
storage = "_";
}
}
context = getContextStorage(storage);
context.set(scope, key, value, callback);
}
},
keys: {
value: function(storage, callback) {
var context;
if (!storage && !callback) {
context = stores["_"];
} else {
if (typeof storage === 'function') {
callback = storage;
storage = "_";
}
if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
}
context = getContextStorage(storage);
}
if (seed) {
if (callback) {
context.keys(scope, function(err,keys) {
callback(err,Array.from(new Set(seedKeys.concat(keys)).keys()));
});
} else {
var keys = context.keys(scope);
return Array.from(new Set(seedKeys.concat(keys)).keys())
}
} else {
return context.keys(scope, callback);
}
}
return results;
}
};
obj.set = function(key, value, storage, callback) {
var context;
if (!storage && !callback) {
context = stores["_"];
} else {
if (typeof storage === 'function') {
callback = storage;
storage = "_";
}
if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
}
context = getContextStorage(storage);
}
context.set(scope, key, value, callback);
};
obj.keys = function(storage, callback) {
var context;
if (!storage && !callback) {
context = stores["_"];
} else {
if (typeof storage === 'function') {
callback = storage;
storage = "_";
}
if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
}
context = getContextStorage(storage);
}
if (seed) {
if (callback) {
context.keys(scope, function(err,keys) {
callback(err,Array.from(new Set(seedKeys.concat(keys)).keys()));
});
} else {
var keys = context.keys(scope);
return Array.from(new Set(seedKeys.concat(keys)).keys())
}
} else {
return context.keys(scope, callback);
}
};
});
return obj;
}
@@ -318,9 +348,13 @@ function getContext(localId,flowId) {
}
var newContext = createContext(contextId);
if (flowId) {
newContext.flow = getContext(flowId);
Object.defineProperty(newContext, 'flow', {
value: getContext(flowId)
});
}
newContext.global = contexts['global'];
Object.defineProperty(newContext, 'global', {
value: contexts['global']
})
contexts[contextId] = newContext;
return newContext;
}

View File

@@ -104,8 +104,6 @@ function loadFile(storagePath){
}else{
return Promise.resolve(undefined);
}
}).catch(function(err){
throw Promise.reject(err);
});
}
@@ -113,9 +111,21 @@ function listFiles(storagePath) {
var promises = [];
return fs.readdir(storagePath).then(function(files) {
files.forEach(function(file) {
promises.push(fs.readdir(path.join(storagePath,file)).then(function(subdirFiles) {
return subdirFiles.map(subfile => path.join(file,subfile));
}))
if (!/^\./.test(file)) {
var fullPath = path.join(storagePath,file);
var stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
promises.push(fs.readdir(fullPath).then(function(subdirFiles) {
var result = [];
subdirFiles.forEach(subfile => {
if (/\.json$/.test(subfile)) {
result.push(path.join(file,subfile))
}
});
return result;
}))
}
}
});
return Promise.all(promises);
}).then(dirs => dirs.reduce((acc, val) => acc.concat(val), []));
@@ -130,6 +140,7 @@ function stringify(value) {
function LocalFileSystem(config){
this.config = config;
this.storageBaseDir = getBasePath(this.config);
this.writePromise = Promise.resolve();
if (config.hasOwnProperty('cache')?config.cache:true) {
this.cache = MemoryStore({});
}
@@ -172,7 +183,7 @@ LocalFileSystem.prototype.open = function(){
if(err.code == 'ENOENT') {
return fs.ensureDir(self.storageBaseDir);
}else{
return Promise.reject(err);
throw err;
}
}).then(function() {
self._flushPendingWrites = function() {
@@ -185,7 +196,7 @@ LocalFileSystem.prototype.open = function(){
var context = newContext[scope];
var stringifiedContext = stringify(context);
if (stringifiedContext.circular && !self.knownCircularRefs[scope]) {
log.warn("Context "+scope+" contains a circular reference that cannot be persisted");
log.warn(log._("error-circular",{scope:scope}));
self.knownCircularRefs[scope] = true;
} else {
delete self.knownCircularRefs[scope];
@@ -203,12 +214,13 @@ LocalFileSystem.prototype.open = function(){
}
LocalFileSystem.prototype.close = function(){
if (this.cache && this._flushPendingWrites) {
var self = this;
if (this.cache && this._pendingWriteTimeout) {
clearTimeout(this._pendingWriteTimeout);
delete this._pendingWriteTimeout;
return this._flushPendingWrites();
this.flushInterval = 0;
}
return Promise.resolve();
return this.writePromise;
}
LocalFileSystem.prototype.get = function(scope, key, callback) {
@@ -220,14 +232,31 @@ LocalFileSystem.prototype.get = function(scope, key, callback) {
}
var storagePath = getStoragePath(this.storageBaseDir ,scope);
loadFile(storagePath + ".json").then(function(data){
var value;
if(data){
data = JSON.parse(data);
if (!Array.isArray(key)) {
callback(null, util.getObjectProperty(data,key));
try {
value = util.getObjectProperty(data,key);
} catch(err) {
if (err.code === "INVALID_EXPR") {
throw err;
}
value = undefined;
}
callback(null, value);
} else {
var results = [undefined];
for (var i=0;i<key.length;i++) {
results.push(util.getObjectProperty(data,key[i]))
try {
value = util.getObjectProperty(data,key[i]);
} catch(err) {
if (err.code === "INVALID_EXPR") {
throw err;
}
value = undefined;
}
results.push(value)
}
callback.apply(null,results);
}
@@ -249,12 +278,18 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) {
// there's a pending write which will handle this
return;
} else {
this._pendingWriteTimeout = setTimeout(function() { self._flushPendingWrites.call(self)}, this.flushInterval);
this._pendingWriteTimeout = setTimeout(function() {
self.writePromise = self.writePromise.then(function(){
return self._flushPendingWrites.call(self).catch(function(err) {
log.error(log._("context.localfilesystem.error-write",{message:err.toString()}));
});
});
}, this.flushInterval);
}
} else if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
} else {
loadFile(storagePath + ".json").then(function(data){
self.writePromise = self.writePromise.then(function() { return loadFile(storagePath + ".json") }).then(function(data){
var obj = data ? JSON.parse(data) : {}
if (!Array.isArray(key)) {
key = [key];
@@ -272,7 +307,7 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) {
}
var stringifiedContext = stringify(obj);
if (stringifiedContext.circular && !self.knownCircularRefs[scope]) {
log.warn("Context "+scope+" contains a circular reference that cannot be persisted");
log.warn(log._("error-circular",{scope:scope}));
self.knownCircularRefs[scope] = true;
} else {
delete self.knownCircularRefs[scope];

View File

@@ -32,7 +32,14 @@ Memory.prototype._getOne = function(scope, key) {
var value;
var error;
if(this.data[scope]){
value = util.getObjectProperty(this.data[scope], key);
try {
value = util.getObjectProperty(this.data[scope], key);
} catch(err) {
if (err.code === "INVALID_EXPR") {
throw err;
}
value = undefined;
}
}
return value;
}

View File

@@ -23,7 +23,7 @@ var i18n;
var settings;
var disableNodePathScan = false;
var iconFileExtensions = [".png", ".gif"];
var iconFileExtensions = [".png", ".gif", ".svg"];
function init(runtime) {
settings = runtime.settings;
@@ -290,6 +290,33 @@ function getNodeFiles(disableNodePathScan) {
if (!disableNodePathScan) {
var moduleFiles = scanTreeForNodesModules();
// Filter the module list to ignore global modules
// that have also been installed locally - allowing the user to
// update a module they may not otherwise be able to touch
moduleFiles.sort(function(A,B) {
if (A.local && !B.local) {
return -1
} else if (!A.local && B.local) {
return 1
}
return 0;
})
var knownModules = {};
moduleFiles = moduleFiles.filter(function(mod) {
var result;
if (!knownModules[mod.package.name]) {
knownModules[mod.package.name] = true;
result = true;
} else {
result = false;
}
log.debug("Module: "+mod.package.name+" "+mod.package.version+(result?"":" *ignored due to local copy*"));
log.debug(" "+mod.dir);
return result;
});
moduleFiles.forEach(function(moduleFile) {
var nodeModuleFiles = getModuleNodeFiles(moduleFile);
nodeList[moduleFile.package.name] = {

View File

@@ -358,7 +358,15 @@ Project.prototype.update = function (user, data) {
promises.push(util.writeFile(this.paths['README.md'], this.description));
}
if (savePackage) {
promises.push(util.writeFile(this.paths['package.json'], JSON.stringify(this.package,"",4)));
promises.push(fs.readFile(project.paths['package.json'],"utf8").then(content => {
var currentPackage = {};
try {
currentPackage = util.parseJSON(content);
} catch(err) {
}
this.package = Object.assign(currentPackage,this.package);
return util.writeFile(this.paths['package.json'], JSON.stringify(this.package,"",4));
}));
}
return when.settle(promises).then(function(res) {
return {

View File

@@ -130,13 +130,19 @@ function compareObjects(obj1,obj2) {
return true;
}
function createError(code, message) {
var e = new Error(message);
e.code = code;
return e;
}
function normalisePropertyExpression(str) {
// This must be kept in sync with validatePropertyExpression
// in editor/js/ui/utils.js
var length = str.length;
if (length === 0) {
throw new Error("Invalid property expression: zero-length");
throw createError("INVALID_EXPR","Invalid property expression: zero-length");
}
var parts = [];
var start = 0;
@@ -149,14 +155,14 @@ function normalisePropertyExpression(str) {
if (!inString) {
if (c === "'" || c === '"') {
if (i != start) {
throw new Error("Invalid property expression: unexpected "+c+" at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
}
inString = true;
quoteChar = c;
start = i+1;
} else if (c === '.') {
if (i===0) {
throw new Error("Invalid property expression: unexpected . at position 0");
throw createError("INVALID_EXPR","Invalid property expression: unexpected . at position 0");
}
if (start != i) {
v = str.substring(start,i);
@@ -167,57 +173,57 @@ function normalisePropertyExpression(str) {
}
}
if (i===length-1) {
throw new Error("Invalid property expression: unterminated expression");
throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
}
// Next char is first char of an identifier: a-z 0-9 $ _
if (!/[a-z0-9\$\_]/i.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
}
start = i+1;
} else if (c === '[') {
if (i === 0) {
throw new Error("Invalid property expression: unexpected "+c+" at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
}
if (start != i) {
parts.push(str.substring(start,i));
}
if (i===length-1) {
throw new Error("Invalid property expression: unterminated expression");
throw createError("INVALID_EXPR","Invalid property expression: unterminated expression");
}
// Next char is either a quote or a number
if (!/["'\d]/.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
}
start = i+1;
inBox = true;
} else if (c === ']') {
if (!inBox) {
throw new Error("Invalid property expression: unexpected "+c+" at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+c+" at position "+i);
}
if (start != i) {
v = str.substring(start,i);
if (/^\d+$/.test(v)) {
parts.push(parseInt(v));
} else {
throw new Error("Invalid property expression: unexpected array expression at position "+start);
throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
}
}
start = i+1;
inBox = false;
} else if (c === ' ') {
throw new Error("Invalid property expression: unexpected ' ' at position "+i);
throw createError("INVALID_EXPR","Invalid property expression: unexpected ' ' at position "+i);
}
} else {
if (c === quoteChar) {
if (i-start === 0) {
throw new Error("Invalid property expression: zero-length string at position "+start);
throw createError("INVALID_EXPR","Invalid property expression: zero-length string at position "+start);
}
parts.push(str.substring(start,i));
// If inBox, next char must be a ]. Otherwise it may be [ or .
if (inBox && !/\]/.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected array expression at position "+start);
throw createError("INVALID_EXPR","Invalid property expression: unexpected array expression at position "+start);
} else if (!inBox && i+1!==length && !/[\[\.]/.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1));
throw createError("INVALID_EXPR","Invalid property expression: unexpected "+str[i+1]+" expression at position "+(i+1));
}
start = i+1;
inString = false;
@@ -226,7 +232,7 @@ function normalisePropertyExpression(str) {
}
if (inBox || inString) {
throw new Error("Invalid property expression: unterminated expression");
throw new createError("INVALID_EXPR","Invalid property expression: unterminated expression");
}
if (start < length) {
parts.push(str.substring(start));

View File

@@ -148,7 +148,7 @@ module.exports = {
// The following property can be used to cause insecure HTTP connections to
// be redirected to HTTPS.
//requireHttps: true
//requireHttps: true,
// The following property can be used to disable the editor. The admin API
// is not affected by this option. To disable both the editor and the admin

View File

@@ -24,7 +24,7 @@ function injectNode(id) {
util.inherits(injectNode, nodePage);
var payloadType = {
var payloadTypeList = {
"flow": 1,
"global": 2,
"str": 3,
@@ -36,54 +36,43 @@ var payloadType = {
"env": 9,
};
var timeType = {
var repeatTypeList = {
"none": 1,
"interval": 2,
"intervalBetweenTimes": 3,
"atASpecificTime": 4,
};
var timeType = {
"none": 1,
"interval": 2,
"intervalBetweenTimes": 3,
"atASpecificTime": 4,
};
var timeType = {
"none": 1,
"interval": 2,
"intervalBetweenTimes": 3,
"atASpecificTime": 4,
};
injectNode.prototype.setPayload = function(type, value) {
injectNode.prototype.setPayload = function(payloadType, payload) {
// Open a payload type list.
browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-container")]');
// Select a payload type.
var payloadTypeXPath = '//*[@class="red-ui-typedInput-options"]/a[' + payloadType[type] + ']';
var payloadTypeXPath = '//*[@class="red-ui-typedInput-options"]/a[' + payloadTypeList[payloadType] + ']';
browser.clickWithWait(payloadTypeXPath);
if (value) {
if (payload) {
// Input a value.
browser.setValue('//*[@class="red-ui-typedInput-input"]/input', value);
browser.setValue('//*[@class="red-ui-typedInput-input"]/input', payload);
}
}
injectNode.prototype.setTopic = function(value) {
browser.setValue('#node-input-topic', value);
injectNode.prototype.setTopic = function(topic) {
browser.setValue('#node-input-topic', topic);
}
injectNode.prototype.setOnce = function(value) {
browser.clickWithWait('#node-input-once');
injectNode.prototype.setOnce = function(once) {
var isChecked = browser.isSelected('#node-input-once');
if (isChecked !== once) {
browser.clickWithWait('#node-input-once');
}
}
injectNode.prototype.setTimeType = function(type) {
var timeTypeXPath = '//*[@id="inject-time-type-select"]/option[' + timeType[type] + ']';
browser.clickWithWait(timeTypeXPath);
injectNode.prototype.setRepeat = function(repeatType) {
var repeatTypeXPath = '//*[@id="inject-time-type-select"]/option[' + repeatTypeList[repeatType] + ']';
browser.clickWithWait(repeatTypeXPath);
}
injectNode.prototype.setRepeat = function(sec) {
browser.setValue('#inject-time-interval-count', sec);
injectNode.prototype.setRepeatInterval = function(repeat) {
browser.setValue('#inject-time-interval-count', repeat);
}
module.exports = injectNode;

View File

@@ -24,22 +24,20 @@ function debugNode(id) {
util.inherits(debugNode, nodePage);
var target = {
"msg": 1,
"full": 2
};
debugNode.prototype.setTarget = function(type, value) {
debugNode.prototype.setOutput = function(complete) {
// Open a payload type list.
browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-container")]/button');
// Select a payload type.
var xPath = '/html/body/div[11]/a[' + target[type] + ']';
browser.clickWithWait(xPath);
if (value) {
if (complete !== 'true') {
// Select the "msg" type.
browser.clickWithWait('/html/body/div[11]/a[1]');
// Input the path in msg.
browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-input")]/input');
browser.keys(['Control', 'a', 'Control']);
browser.keys(['Delete']);
browser.setValue('//*[contains(@class, "red-ui-typedInput-input")]/input', value);
browser.setValue('//*[contains(@class, "red-ui-typedInput-input")]/input', complete);
} else {
// Select the "complete msg object" type.
browser.clickWithWait('/html/body/div[11]/a[2]');
}
}

View File

@@ -24,12 +24,17 @@ function functionNode(id) {
util.inherits(functionNode, nodePage);
functionNode.prototype.setCode = function(value) {
functionNode.prototype.setFunction = function(func) {
browser.click('#node-input-func-editor');
browser.keys(['Control', 'Home', 'Control']);
for (var i=0; i<value.length; i++) {
browser.keys([value.substr(i, 1)]);
for (var i = 0; i < func.length; i++) {
browser.keys([func.charAt(i)]);
}
// Delete the unnecessary code that ace editor does the autocompletion.
browser.keys(['Control', 'Shift', 'End', 'Shift', 'Control']);
browser.keys(['Delete']);
// Need to wait until ace editor correctly checks the syntax.
browser.pause(50);
}
module.exports = functionNode;

View File

@@ -24,29 +24,20 @@ function templateNode(id) {
util.inherits(templateNode, nodePage);
var syntaxType = {
"mustache": 1,
"plain": 2
};
templateNode.prototype.setSyntax = function(type) {
// Open a method type list.
browser.clickWithWait('#node-input-syntax');
// Select a method type.
var syntaxTypeXPath = '//*[@id="node-input-syntax"]/option[' + syntaxType[type] + ']';
browser.clickWithWait(syntaxTypeXPath);
templateNode.prototype.setSyntax = function(syntax) {
browser.selectWithWait('#node-input-syntax', syntax);
}
templateNode.prototype.setFormat = function(type) {
browser.selectByValue('#node-input-format', type);
templateNode.prototype.setFormat = function(format) {
browser.selectWithWait('#node-input-format', format);
}
templateNode.prototype.setTemplate = function(value) {
templateNode.prototype.setTemplate = function(template) {
browser.click('#node-input-template-editor');
browser.keys(['Control', 'a', 'Control']); // call twice to release the keys.
// Need to add a character one by one since some words such as 'Control' are treated as a special word.
for (var i=0; i<value.length; i++) {
browser.keys([value.charAt(i)]);
for (var i = 0; i < template.length; i++) {
browser.keys([template.charAt(i)]);
}
browser.keys(['Control', 'Shift', 'End', 'Shift', 'Control']);
browser.keys(['Delete']);

View File

@@ -22,30 +22,14 @@ function httpinNode(id) {
nodePage.call(this, id);
}
function setMethod(type) {
browser.selectByValue('#node-input-method', type);
}
util.inherits(httpinNode, nodePage);
var methodType = {
"get": 1,
"post": 2,
"put": 3,
"delete": 4,
"patch": 5,
};
httpinNode.prototype.setMethod = function(type) {
// Open a method type list.
browser.clickWithWait('#node-input-method');
// Select a method type.
var methodTypeXPath = '//*[@id="node-input-method"]/option[' + methodType[type] + ']';
browser.clickWithWait(methodTypeXPath);
httpinNode.prototype.setMethod = function(method) {
browser.selectWithWait('#node-input-method', method);
}
httpinNode.prototype.setUrl = function(value) {
browser.setValue('#node-input-url', value);
httpinNode.prototype.setUrl = function(url) {
browser.setValue('#node-input-url', url);
}
module.exports = httpinNode;

View File

@@ -24,36 +24,16 @@ function httpRequestNode(id) {
util.inherits(httpRequestNode, nodePage);
var methodType = {
"get": 1,
"post": 2,
"put": 3,
"delete": 4,
"setByMsgMethod": 5,
};
var retType = {
"txt": 1,
"bin": 2,
"obj": 3,
};
httpRequestNode.prototype.setUrl = function(value) {
browser.setValue('#node-input-url', value);
httpRequestNode.prototype.setUrl = function(url) {
browser.setValue('#node-input-url', url);
}
httpRequestNode.prototype.setMethod = function(type) {
// Open a method type list.
browser.clickWithWait('#node-input-method');
// Select a method type.
var methodTypeXPath = '//*[@id="node-input-method"]/option[' + methodType[type] + ']';
browser.clickWithWait(methodTypeXPath);
httpRequestNode.prototype.setMethod = function(method) {
browser.selectWithWait('#node-input-method', method);
}
httpRequestNode.prototype.setRet = function(type) {
browser.clickWithWait('#node-input-ret');
var retTypeXPath = '//*[@id="node-input-ret"]/option[' + retType[type] + ']';
browser.clickWithWait(retTypeXPath);
httpRequestNode.prototype.setReturn = function(ret) {
browser.selectWithWait('#node-input-ret', ret);
}
module.exports = httpRequestNode;

View File

@@ -24,13 +24,6 @@ function changeNode(id) {
util.inherits(changeNode, nodePage);
var tType = {
"set": 1,
"change": 2,
"delete": 3,
"move": 4,
};
var totType = {
"msg": 1,
"flow": 2,
@@ -42,6 +35,7 @@ var totType = {
"bin": 8,
"date": 9,
"jsonata": 10,
"env": 11,
};
var ptType = {
@@ -50,8 +44,8 @@ var ptType = {
"global": 3,
};
function setT(rule, index) {
browser.selectByValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/select', rule);
function setT(t, index) {
browser.selectWithWait('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/select', t);
}
// It is better to create a function whose input value is the object type in the future,

View File

@@ -24,8 +24,8 @@ function rangeNode(id) {
util.inherits(rangeNode, nodePage);
rangeNode.prototype.setAction = function(value) {
browser.selectByValue('#node-input-action', value);
rangeNode.prototype.setAction = function(action) {
browser.selectWithWait('#node-input-action', action);
}
rangeNode.prototype.setRange = function(minin, maxin, minout, maxout) {

View File

@@ -24,8 +24,8 @@ function htmlNode(id) {
util.inherits(htmlNode, nodePage);
htmlNode.prototype.setTag = function(value) {
browser.setValue('#node-input-tag', value);
htmlNode.prototype.setSelector = function(tag) {
browser.setValue('#node-input-tag', tag);
}
module.exports = htmlNode;

View File

@@ -31,13 +31,13 @@ var formatType = {
"stream": 4
};
fileinNode.prototype.setFilename = function(value) {
browser.setValue('#node-input-filename', value);
fileinNode.prototype.setFilename = function(filename) {
browser.setValue('#node-input-filename', filename);
}
fileinNode.prototype.setFormat = function(type) {
fileinNode.prototype.setOutput = function(format) {
browser.clickWithWait('#node-input-format');
var formatTypeXPath = '//*[@id="node-input-format"]/option[' + formatType[type] + ']';
var formatTypeXPath = '//*[@id="node-input-format"]/option[' + formatType[format] + ']';
browser.clickWithWait(formatTypeXPath);
}

View File

@@ -38,6 +38,12 @@ function init() {
var ret = browser.getText(selector);
return ret;
}, false);
browser.addCommand("selectWithWait", function(selector, value) {
browser.waitForVisible(selector, 5000);
var ret = browser.selectByValue(selector, value);
return ret;
}, false);
}
module.exports = {

View File

@@ -19,8 +19,8 @@ var should = require("should");
var fs = require('fs-extra');
var helper = require("../../editor_helper");
var debugTab = require('../../pageobjects/workspace/debugTab_page');
var workspace = require('../../pageobjects/workspace/workspace_page');
var debugTab = require('../../pageobjects/editor/debugTab_page');
var workspace = require('../../pageobjects/editor/workspace_page');
var nodeWidth = 200;

View File

@@ -17,8 +17,8 @@
var should = require("should");
var helper = require("../../editor_helper");
var debugTab = require('../../pageobjects/workspace/debugTab_page');
var workspace = require('../../pageobjects/workspace/workspace_page');
var debugTab = require('../../pageobjects/editor/debugTab_page');
var workspace = require('../../pageobjects/editor/workspace_page');
var nodeWidth = 200;
var nodeHeight = 100;
@@ -65,7 +65,7 @@ describe('cookbook', function() {
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello');
httpRequestNode.setMethod("get");
httpRequestNode.setMethod("GET");
httpRequestNode.clickOk();
injectNode.connect(httpRequestNode);
@@ -105,7 +105,7 @@ describe('cookbook', function() {
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-query?name=Nick');
httpRequestNode.setMethod("get");
httpRequestNode.setMethod("GET");
httpRequestNode.clickOk();
injectNode.connect(httpRequestNode);
@@ -145,7 +145,7 @@ describe('cookbook', function() {
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-param/Dave');
httpRequestNode.setMethod("get");
httpRequestNode.setMethod("GET");
httpRequestNode.clickOk();
injectNode.connect(httpRequestNode);
@@ -185,12 +185,12 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 3, nodeHeight);
changeNode.edit();
changeNode.ruleSet("headers", "msg", "{\"user-agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64)\"}", "json");
changeNode.ruleSet("headers", "msg", '{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}', "json");
changeNode.clickOk();
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-headers');
httpRequestNode.setMethod("get");
httpRequestNode.setMethod("GET");
httpRequestNode.clickOk();
injectNode.connect(changeNode);
@@ -249,7 +249,7 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 2, nodeHeight * 2);
httpRequestNode.edit();
httpRequestNode.setMethod("get");
httpRequestNode.setMethod("GET");
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-data');
httpRequestNode.clickOk();
@@ -280,7 +280,7 @@ describe('cookbook', function() {
templateNode.edit();
templateNode.setSyntax("mustache");
templateNode.setFormat("handlebars");
templateNode.setTemplate("{ \"Hello\": \"World\" }");
templateNode.setTemplate('{ "Hello": "World" }');
templateNode.clickOk();
changeNode.edit();
@@ -299,12 +299,12 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 2, nodeHeight);
httpRequestNode.edit();
httpRequestNode.setMethod("get");
httpRequestNode.setMethod("GET");
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-json');
httpRequestNode.clickOk();
debugNode.edit();
debugNode.setTarget("msg", "headers");
debugNode.setOutput("headers");
debugNode.clickOk();
injectNode.connect(httpRequestNode);
@@ -332,7 +332,7 @@ describe('cookbook', function() {
fileinNode.edit();
fileinNode.setFilename("test/resources/file-in-node/test.txt");
fileinNode.setFormat("");
fileinNode.setOutput("");
fileinNode.clickOk();
changeNode.edit();
@@ -352,7 +352,7 @@ describe('cookbook', function() {
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-file');
httpRequestNode.setMethod("get");
httpRequestNode.setMethod("GET");
httpRequestNode.clickOk();
injectNode.connect(httpRequestNode);
@@ -396,7 +396,7 @@ describe('cookbook', function() {
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-raw');
httpRequestNode.setMethod("post");
httpRequestNode.setMethod("POST");
httpRequestNode.clickOk();
injectNode.connect(httpRequestNode);
@@ -440,12 +440,12 @@ describe('cookbook', function() {
injectNode.clickOk();
changeNode.edit();
changeNode.ruleSet("headers", "msg", "{\"content-type\":\"application/x-www-form-urlencoded\"}", "json");
changeNode.ruleSet("headers", "msg", '{"content-type":"application/x-www-form-urlencoded"}', "json");
changeNode.clickOk();
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-form');
httpRequestNode.setMethod("post");
httpRequestNode.setMethod("POST");
httpRequestNode.clickOk();
injectNode.connect(changeNode);
@@ -486,16 +486,16 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 3, nodeHeight);
injectNode.edit()
injectNode.setPayload("json", "{\"name\":\"Nick\"}");
injectNode.setPayload("json", '{"name":"Nick"}');
injectNode.clickOk();
changeNode.edit();
changeNode.ruleSet("headers", "msg", "{\"content-type\":\"application/json\"}", "json");
changeNode.ruleSet("headers", "msg", '{"content-type":"application/json"}', "json");
changeNode.clickOk();
httpRequestNode.edit();
httpRequestNode.setUrl(helper.url() + httpNodeRoot + '/hello-json');
httpRequestNode.setMethod("post");
httpRequestNode.setMethod("POST");
httpRequestNode.clickOk();
injectNode.connect(changeNode);
@@ -531,13 +531,13 @@ describe('cookbook', function() {
httpinNodeFormat.clickOk();
functionNodeFormat.edit();
functionNodeFormat.setCode("msg.payload = JSON.stringify(msg.req.cookies,null,4);");
functionNodeFormat.setFunction("msg.payload = JSON.stringify(msg.req.cookies,null,4);\nreturn msg;");
functionNodeFormat.clickOk();
templateNode.edit();
templateNode.setSyntax("mustache");
templateNode.setFormat("handlebars");
templateNode.setTemplate("<html>\n<head></head>\n<body>\n<h1>Cookies</h1>\n<p></p><a href=\"hello-cookie/add\">Add a cookie</a> &bull; <a href=\"hello-cookie/clear\">Clear cookies</a></p>\n<pre>{{ payload }}</pre>\n</body>\n</html>");
templateNode.setTemplate('<html>\n<head></head>\n<body>\n<h1>Cookies</h1>\n<p></p><a href="hello-cookie/add">Add a cookie</a> &bull; <a href="hello-cookie/clear">Clear cookies</a></p>\n<pre>{{ payload }}</pre>\n</body>\n</html>');
templateNode.clickOk();
httpinNodeFormat.connect(functionNodeFormat);
@@ -550,7 +550,7 @@ describe('cookbook', function() {
httpinNodeAdd.clickOk();
functionNodeAdd.edit();
functionNodeAdd.setCode("msg.cookies = { };\n msg.cookies[\"demo-\"+(Math.floor(Math.random()*1000))] = Date.now();");
functionNodeAdd.setFunction('msg.cookies = { };\n msg.cookies["demo-"+(Math.floor(Math.random()*1000))] = Date.now();\nreturn msg;');
functionNodeAdd.clickOk();
changeNode.edit();
@@ -571,7 +571,7 @@ describe('cookbook', function() {
httpinNodeClear.clickOk();
functionNodeClear.edit();
functionNodeClear.setCode("var cookieNames = Object.keys(msg.req.cookies).filter(function(cookieName) { return /^demo-/.test(cookieName);});\nmsg.cookies = {};\n\ncookieNames.forEach(function(cookieName) {\n msg.cookies[cookieName] = null;\n});\n\n");
functionNodeClear.setFunction("var cookieNames = Object.keys(msg.req.cookies).filter(function(cookieName) { return /^demo-/.test(cookieName);});\nmsg.cookies = {};\n\ncookieNames.forEach(function(cookieName) {\n msg.cookies[cookieName] = null;\n});\nreturn msg;\n");
functionNodeClear.clickOk();
httpinNodeClear.connect(functionNodeClear);

View File

@@ -19,8 +19,8 @@ var should = require("should");
var fs = require('fs-extra');
var helper = require("../../editor_helper");
var debugTab = require('../../pageobjects/workspace/debugTab_page');
var workspace = require('../../pageobjects/workspace/workspace_page');
var debugTab = require('../../pageobjects/editor/debugTab_page');
var workspace = require('../../pageobjects/editor/workspace_page');
var specUtil = require('../../pageobjects/util/spec_util_page');
var nodeWidth = 200;
@@ -168,8 +168,8 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 2);
injectNode.edit();
injectNode.setTimeType("interval");
injectNode.setRepeat(1);
injectNode.setRepeat("interval");
injectNode.setRepeatInterval(1);
injectNode.clickOk();
injectNode.connect(debugNode);
@@ -196,12 +196,12 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 3);
httpRequetNode.edit();
httpRequetNode.setMethod("get");
httpRequetNode.setMethod("GET");
httpRequetNode.setUrl(helper.url());
httpRequetNode.clickOk();
htmlNode.edit();
htmlNode.setTag("title");
htmlNode.setSelector("title");
htmlNode.clickOk();
injectNode.connect(httpRequetNode);
@@ -336,14 +336,14 @@ describe('cookbook', function() {
changeNodeSetPost.clickOk();
httpRequetNode.edit();
httpRequetNode.setMethod("get");
httpRequetNode.setMethod("GET");
var url = helper.url() + httpNodeRoot + "/{{post}}";
httpRequetNode.setUrl(url);
httpRequetNode.setRet("obj");
httpRequetNode.setReturn("obj");
httpRequetNode.clickOk();
debugNode.edit();
debugNode.setTarget("msg", "payload.title");
debugNode.setOutput("payload.title");
debugNode.clickOk();
injectNode.connect(changeNodeSetPost);
@@ -364,11 +364,11 @@ describe('cookbook', function() {
templateNode.edit();
templateNode.setSyntax("mustache");
templateNode.setFormat("handlebars");
templateNode.setTemplate("{\"title\": \"Hello\"}");
templateNode.setTemplate('{"title": "Hello"}');
templateNode.clickOk();
changeNodeSetHeader.edit();
changeNodeSetHeader.ruleSet("headers", "msg", "{\"content-type\":\"application/json\"}", "json");
changeNodeSetHeader.ruleSet("headers", "msg", '{"content-type":"application/json"}', "json");
changeNodeSetHeader.clickOk();
httpinNode.connect(templateNode);
@@ -389,9 +389,9 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 2);
httpRequetNode.edit();
httpRequetNode.setMethod("get");
httpRequetNode.setMethod("GET");
httpRequetNode.setUrl(helper.url() + "/settings");
httpRequetNode.setRet("bin");
httpRequetNode.setReturn("bin");
httpRequetNode.clickOk();
injectNode.connect(httpRequetNode);
@@ -413,11 +413,11 @@ describe('cookbook', function() {
var debugNode = workspace.addNode("debug", nodeWidth * 3);
functionNode.edit();
functionNode.setCode("msg.payload = \"data to post\";");
functionNode.setFunction('msg.payload = "data to post";\nreturn msg;');
functionNode.clickOk();
httpRequetNode.edit();
httpRequetNode.setMethod("post");
httpRequetNode.setMethod("POST");
var url = helper.url() + httpNodeRoot + "/set-header";
httpRequetNode.setUrl(url);
httpRequetNode.clickOk();

View File

@@ -1216,7 +1216,7 @@ describe('function node', function() {
msg.should.have.property('payload', n1.id);
done();
});
n1.receive({payload:"foo",topicb: "bar"});
n1.receive({payload:"foo",topic: "bar"});
});
});
@@ -1230,7 +1230,7 @@ describe('function node', function() {
msg.should.have.property('payload', n1.name);
done();
});
n1.receive({payload:"foo",topicb: "bar"});
n1.receive({payload:"foo",topic: "bar"});
});
});

View File

@@ -15,7 +15,8 @@
**/
var should = require("should");
var rpi = require("../../../../nodes/core/hardware/36-rpi-gpio.js");
var rpiNode = require("../../../../nodes/core/hardware/36-rpi-gpio.js");
var statusNode = require("../../../../nodes/core/core/25-status.js");
var helper = require("node-red-node-test-helper");
var fs = require("fs");
@@ -50,7 +51,7 @@ describe('RPI GPIO Node', function() {
it('should load Input node', function(done) {
var flow = [{id:"n1", type:"rpi-gpio in", name:"rpi-gpio in" }];
helper.load(rpi, flow, function() {
helper.load(rpiNode, flow, function() {
var n1 = helper.getNode("n1");
n1.should.have.property('name', 'rpi-gpio in');
try {
@@ -69,7 +70,7 @@ describe('RPI GPIO Node', function() {
it('should load Output node', function(done) {
var flow = [{id:"n1", type:"rpi-gpio out", name:"rpi-gpio out" }];
helper.load(rpi, flow, function() {
helper.load(rpiNode, flow, function() {
var n1 = helper.getNode("n1");
n1.should.have.property('name', 'rpi-gpio out');
try {
@@ -86,4 +87,62 @@ describe('RPI GPIO Node', function() {
});
});
it('should read a dummy value high (not on Pi)', function(done) {
var flow = [{id:"n1", type:"rpi-gpio in", pin:"7", intype:"up", debounce:"25", read:true, wires:[["n2"]] },
{id:"n2", type:"helper"}];
helper.load(rpiNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('topic', 'pi/7');
msg.should.have.property('payload', 1);
done();
} catch(err) {
done(err);
}
});
});
});
it('should read a dummy value low (not on Pi)', function(done) {
var flow = [{id:"n1", type:"rpi-gpio in", pin:"11", intype:"down", debounce:"25", read:true, wires:[["n2"]] },
{id:"n2", type:"helper"}];
helper.load(rpiNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('topic', 'pi/11');
msg.should.have.property('payload', 0);
done();
} catch(err) {
done(err);
}
});
});
});
it('should be able preset out to a dummy value (not on Pi)', function(done) {
var flow = [{id:"n1", type:"rpi-gpio out", pin:"7", out:"out", level:"0", set:true, freq:"", wires:[], z:"1"},
{id:"n2", type:"status", scope:null, wires:[["n3"]], z:"1"},
{id:"n3", type:"helper", z:"1"}];
helper.load([rpiNode,statusNode], flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
n3.on("input", function(msg) {
try {
msg.should.have.property('status');
msg.status.should.have.property('text', "rpi-gpio.status.na");
done();
} catch(err) {
done(err);
}
});
n1.receive({payload:"1"});
});
});
});

View File

@@ -59,7 +59,11 @@ describe('TCP Request Node', function() {
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload', Buffer(val1));
if (typeof val1 === 'object') {
msg.should.have.properties(Object.assign({}, val1, {payload: Buffer(val1.payload)}));
} else {
msg.should.have.property('payload', Buffer(val1));
}
done();
} catch(err) {
done(err);
@@ -79,7 +83,11 @@ describe('TCP Request Node', function() {
const n2 = helper.getNode("n2");
n2.on("input", msg => {
try {
msg.should.have.property('payload', Buffer(result));
if (typeof result === 'object') {
msg.should.have.properties(Object.assign({}, result, {payload: Buffer(result.payload)}));
} else {
msg.should.have.property('payload', Buffer(result));
}
done();
} catch(err) {
done(err);
@@ -95,31 +103,75 @@ describe('TCP Request Node', function() {
it('should send & recv data', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, "foo", "ACK:foo", done)
testTCP(flow, {
payload: 'foo',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should retain complete message', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {
payload: 'foo',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & recv data when specified character received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, "foo0bar0", "ACK:foo0", done);
testTCP(flow, {
payload: 'foo0bar0',
topic: 'bar'
}, {
payload: 'ACK:foo0',
topic: 'bar'
}, done);
});
it('should send & recv data after fixed number of chars received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, "foo bar", "ACK:foo", done);
testTCP(flow, {
payload: 'foo bar',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & receive, then keep connection', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, "foo", "ACK:foo", done);
testTCP(flow, {
payload: 'foo',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & recv data to/from server:port from msg', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {payload:"foo", host:"localhost", port:port}, "ACK:foo", done)
testTCP(flow, {
payload: "foo",
host: "localhost",
port: port
}, {
payload: "ACK:foo",
host: 'localhost',
port: port
}, done);
});
});
@@ -127,36 +179,95 @@ describe('TCP Request Node', function() {
it('should send & recv data', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, ['f', 'o', 'o'], 'ACK:foo', done);
testTCPMany(flow, [{
payload: 'f',
topic: 'bar'
}, {
payload: 'o',
topic: 'bar'
}, {
payload: 'o',
topic: 'bar'
}], {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & recv data when specified character received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, ["foo0","bar0"], "ACK:foo0", done);
testTCPMany(flow, [{
payload: "foo0",
topic: 'bar'
}, {
payload: "bar0",
topic: 'bar'
}], {
payload: "ACK:foo0",
topic: 'bar'
}, done);
});
it('should send & recv data after fixed number of chars received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, ["fo", "ob", "ar"], "ACK:foo", done);
testTCPMany(flow, [{
payload: "fo",
topic: 'bar'
}, {
payload: "ob",
topic: 'bar'
}, {
payload: "ar",
topic: 'bar'
}], {
payload: "ACK:foo",
topic: 'bar'
}, done);
});
it('should send & receive, then keep connection', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, ["foo", "bar", "baz"], "ACK:foobarbaz", done);
testTCPMany(flow, [{
payload: "foo",
topic: 'bar'
}, {
payload: "bar",
topic: 'bar'
}, {
payload: "baz",
topic: 'bar'
}], {
payload: "ACK:foobarbaz",
topic: 'bar'
}, done);
});
it('should send & recv data to/from server:port from msg', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [
{payload:"f", host:"localhost", port:port},
{payload:"o", host:"localhost", port:port},
{payload:"o", host:"localhost", port:port}], "ACK:foo", done);
testTCPMany(flow, [{
payload: "f",
host: "localhost",
port: port
},
{
payload: "o",
host: "localhost",
port: port
},
{
payload: "o",
host: "localhost",
port: port
}
], {
payload: "ACK:foo",
host: 'localhost',
port: port
}, done);
});
it('should limit the queue size', function (done) {
@@ -168,5 +279,23 @@ describe('TCP Request Node', function() {
const expected = msgs.slice(0, -1);
testTCPMany(flow, msgs, "ACK:" + expected.join(''), done);
});
it('should only retain the latest message', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: 'f',
topic: 'bar'
}, {
payload: 'o',
topic: 'baz'
}, {
payload: 'o',
topic: 'quux'
}], {
payload: 'ACK:foo',
topic: 'quux'
}, done);
});
});
});

View File

@@ -78,6 +78,24 @@ describe('change Node', function() {
done();
});
});
it('should no-op if there are no rules', function(done) {
var flow = [{"id":"changeNode1","type":"change","rules":[],"action":"","property":"","from":"","to":"","reg":false,"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.should.eql(sentMsg);
done();
} catch(err) {
done(err);
}
});
var sentMsg = {payload:"leaveMeAlong"};
changeNode1.receive(sentMsg);
});
});
describe('#set' , function() {

View File

@@ -603,14 +603,14 @@ describe('JOIN node', function() {
});
it('should accumulate a merged object', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], build:"merged",mode:"custom",accumulate:true, count:1},
var flow = [{id:"n1", type:"join", wires:[["n2"]], build:"merged",mode:"custom",accumulate:true, count:3},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 5) {
if (c === 3) {
try {
msg.should.have.property("payload");
msg.payload.should.have.property("a",3);
@@ -632,14 +632,14 @@ describe('JOIN node', function() {
});
it('should be able to reset an accumulation', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], build:"merged",accumulate:true,mode:"custom", count:1},
var flow = [{id:"n1", type:"join", wires:[["n2"]], build:"merged",accumulate:true,mode:"custom", count:3},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 3) {
if (c === 1) {
try {
msg.should.have.property("payload");
msg.payload.should.have.property("a",1);
@@ -649,11 +649,20 @@ describe('JOIN node', function() {
}
catch(e) { done(e) }
}
if (c === 5) {
if (c === 2) {
try {
msg.should.have.property("payload");
msg.payload.should.have.property("b",2);
msg.payload.should.have.property("c",1);
msg.payload.should.have.property("e",2);
msg.payload.should.have.property("f",1);
}
catch(e) { done(e) }
}
if (c === 3) {
try {
msg.should.have.property("payload");
msg.payload.should.have.property("g",2);
msg.payload.should.have.property("h",1);
msg.payload.should.have.property("i",3);
done();
}
catch(e) { done(e) }
@@ -664,8 +673,11 @@ describe('JOIN node', function() {
n1.receive({payload:{b:2}, topic:"b"});
n1.receive({payload:{c:3}, topic:"c"});
n1.receive({payload:{d:4}, topic:"d", complete:true});
n1.receive({payload:{b:2}, topic:"e"});
n1.receive({payload:{c:1}, topic:"f"});
n1.receive({payload:{e:2}, topic:"e"});
n1.receive({payload:{f:1}, topic:"f", complete:true});
n1.receive({payload:{g:2}, topic:"g"});
n1.receive({payload:{h:1}, topic:"h"});
n1.receive({payload:{i:3}, topic:"i"});
});
});

View File

@@ -66,13 +66,25 @@ describe('SORT node', function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property(target);
var data = msg[target];
data.length.should.equal(data_out.length);
for(var i = 0; i < data_out.length; i++) {
data[i].should.equal(data_out[i]);
try {
msg.should.have.property(target);
var data = msg[target];
data.length.should.equal(data_out.length);
for(var i = 0; i < data_out.length; i++) {
var data0 = data[i];
var data1 = data_out[i];
if (typeof data0 === "object") {
data0.should.deepEqual(data1);
}
else {
data0.should.equal(data1);
}
}
done();
}
catch(e) {
console.log(e);
}
done();
});
var msg = {};
msg[target] = data_in;
@@ -93,6 +105,34 @@ describe('SORT node', function() {
}
function check_sort1(flow, key, key_type, data_in, data_out, done) {
function equals(v0, v1) {
var k0 = Object.keys(v0);
var k1 = Object.keys(v1);
if (k0.length === k1.length) {
for (var i = 0; i < k0.length; i++) {
var k = k0[i];
if (!v1.hasOwnProperty(k) ||
(v0[k] !== v1[k])) {
return false;
}
}
return true;
}
return false;
}
function indexOf(a, v) {
for(var i = 0; i < a.length; i++) {
var av = a[i];
if ((typeof v === 'object') && equals(v, av)) {
return i;
}
else if (v === av) {
return i;
}
}
return -1;
}
var sort = flow[0];
var prop = (key_type === "msg") ? key : "payload";
sort.targetType = "seq";
@@ -107,7 +147,7 @@ describe('SORT node', function() {
msg.should.have.property("parts");
msg.parts.should.have.property("count", data_out.length);
var data = msg[prop];
var index = data_out.indexOf(data);
var index = indexOf(data_out, data);
msg.parts.should.have.property("index", index);
count++;
if (count === data_out.length) {
@@ -136,7 +176,6 @@ describe('SORT node', function() {
check_sort1(flow, exp, "jsonata", data_in, data_out, done);
}
(function() {
var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, wires:[["n2"]]},
{id:"n2", type:"helper"}];
@@ -239,6 +278,19 @@ describe('SORT node', function() {
});
})();
(function() {
var flow = [{id:"n1", type:"sort", order:"ascending", as_num:true, wires:[["n2"]]},
{id:"n2", type:"helper"}];
var conv = function(x) {
return x.map(function(v) { return { val:v }; });
};
var data_in = conv([ "200", "4", "30", "1000" ]);
var data_out = conv([ "4", "30", "200", "1000" ]);
it('should sort payload of objects', function(done) {
check_sort0C(flow, "val", data_in, data_out, done);
});
})();
it('should sort payload by context (exp, not number, ascending)', function(done) {
var flow = [{id:"n1", type:"sort", target:"data", targetType:"msg", msgKey:"$flowContext($)", msgKeyType:"jsonata", order:"ascending", as_num:false, wires:[["n2"]],z:"flow"},
{id:"n2", type:"helper",z:"flow"},
@@ -268,7 +320,7 @@ describe('SORT node', function() {
});
it('should sort message group by context (exp, not number, ascending)', function(done) {
var flow = [{id:"n1", type:"sort", target:"data", targetType:"msg", msgKey:"$globalContext(payload)", msgKeyType:"jsonata", order:"ascending", as_num:false, wires:[["n2"]],z:"flow"},
var flow = [{id:"n1", type:"sort", target:"data", targetType:"seq", seqKey:"$globalContext(payload)", seqKeyType:"jsonata", order:"ascending", as_num:false, wires:[["n2"]],z:"flow"},
{id:"n2", type:"helper",z:"flow"},
{id:"flow", type:"tab"}];
var data_in = [ "first", "second", "third", "fourth" ];
@@ -282,15 +334,20 @@ describe('SORT node', function() {
n1.context()["global"].set("third","3");
n1.context()["global"].set("fourth","2");
n2.on("input", function(msg) {
msg.should.have.property("payload");
msg.should.have.property("parts");
msg.parts.should.have.property("count", data_out.length);
var data = msg["payload"];
var index = data_out.indexOf(data);
msg.parts.should.have.property("index", index);
count++;
if (count === data_out.length) {
done();
try {
msg.should.have.property("payload");
msg.should.have.property("parts");
msg.parts.should.have.property("count", data_out.length);
var data = msg["payload"];
var index = data_out.indexOf(data);
msg.parts.should.have.property("index", index);
count++;
if (count === data_out.length) {
done();
}
}
catch(e) {
done(e);
}
});
var len = data_in.length;
@@ -332,7 +389,7 @@ describe('SORT node', function() {
});
it('should sort message group by persistable context (exp, not number, descending)', function(done) {
var flow = [{id:"n1", type:"sort", target:"data", targetType:"msg", msgKey:"$flowContext(payload,\"memory\")", msgKeyType:"jsonata", order:"descending", as_num:false, wires:[["n2"]],z:"flow"},
var flow = [{id:"n1", type:"sort", target:"data", targetType:"seq", seqKey:"$flowContext(payload,\"memory\")", seqKeyType:"jsonata", order:"descending", as_num:false, wires:[["n2"]],z:"flow"},
{id:"n2", type:"helper",z:"flow"},
{id:"flow", type:"tab"}];
var data_in = [ "first", "second", "third", "fourth" ];

View File

@@ -265,6 +265,23 @@ describe('JSON node', function() {
});
});
it('should pass an object if provided a valid object and schema and action is object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",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 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 pass a string if provided a valid object and schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
@@ -281,6 +298,22 @@ describe('JSON node', function() {
});
});
it('should pass a string if provided a valid JSON string and schema and action is string', function(done) {
var flow = [{id:"jn1",type:"json",action:"str",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 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 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"}];
@@ -305,6 +338,78 @@ describe('JSON node', function() {
});
});
it('should log an error if passed an invalid object and valid schema and action is object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",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 an invalid JSON string 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 jsonString = '{"number":"Hello","string":3}';
jn1.receive({payload:jsonString, 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 an invalid JSON string and valid schema and action is string', function(done) {
var flow = [{id:"jn1",type:"json",action:"str",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 jsonString = '{"number":"Hello","string":3}';
jn1.receive({payload:jsonString, 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"}];

View File

@@ -46,14 +46,14 @@ describe('file Nodes', function() {
it('should be loaded', function(done) {
var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":true, "overwriteFile":true}];
helper.load(fileNode, flow, function() {
try {
try {
var fileNode1 = helper.getNode("fileNode1");
fileNode1.should.have.property('name', 'fileNode');
done();
}
catch (e) {
done(e);
}
}
catch (e) {
done(e);
}
});
});
@@ -64,21 +64,44 @@ describe('file Nodes', function() {
var n1 = helper.getNode("fileNode1");
var n2 = helper.getNode("helperNode1");
n2.on("input", function(msg) {
try {
var f = fs.readFileSync(fileToTest);
f.should.have.length(4);
fs.unlinkSync(fileToTest);
msg.should.have.property("payload", "test");
done();
}
catch (e) {
done(e);
}
try {
var f = fs.readFileSync(fileToTest);
f.should.have.length(4);
fs.unlinkSync(fileToTest);
msg.should.have.property("payload", "test");
done();
}
catch (e) {
done(e);
}
});
n1.receive({payload:"test"});
});
});
it('should write multi-byte string to a file', function(done) {
var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":true, wires: [["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileNode1");
var n2 = helper.getNode("helperNode1");
n2.on("input", function(msg) {
try {
var f = fs.readFileSync(fileToTest).toString();
f.should.have.length(2);
f.should.equal("試験");
fs.unlinkSync(fileToTest);
msg.should.have.property("payload", "試験");
done();
}
catch (e) {
done(e);
}
});
n1.receive({payload:"試験"});
});
});
it('should append to a file and add newline', function(done) {
var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":true, "overwriteFile":false, wires: [["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
@@ -93,26 +116,26 @@ describe('file Nodes', function() {
var data = ["test2", true, 999, [2]];
n2.on("input", function (msg) {
try {
msg.should.have.property("payload");
data.should.containDeep([msg.payload]);
if (count === 3) {
try {
msg.should.have.property("payload");
data.should.containDeep([msg.payload]);
if (count === 3) {
var f = fs.readFileSync(fileToTest).toString();
if (os.type() !== "Windows_NT") {
f.should.have.length(19);
f.should.equal("test2\ntrue\n999\n[2]\n");
f.should.have.length(19);
f.should.equal("test2\ntrue\n999\n[2]\n");
}
else {
f.should.have.length(23);
f.should.equal("test2\r\ntrue\r\n999\r\n[2]\r\n");
f.should.have.length(23);
f.should.equal("test2\r\ntrue\r\n999\r\n[2]\r\n");
}
done();
}
count++;
}
catch (e) {
done(e);
}
}
count++;
}
catch (e) {
done(e);
}
});
n1.receive({payload:"test2"}); // string
@@ -142,37 +165,37 @@ describe('file Nodes', function() {
var count = 0;
n2.on("input", function (msg) {
try {
msg.should.have.property("payload");
data.should.containDeep([msg.payload]);
try {
try {
msg.should.have.property("payload");
data.should.containDeep([msg.payload]);
try {
if (count === 1) {
// Check they got appended as expected
var f = fs.readFileSync(fileToTest).toString();
f.should.equal("onetwo");
// Check they got appended as expected
var f = fs.readFileSync(fileToTest).toString();
f.should.equal("onetwo");
// Delete the file
fs.unlinkSync(fileToTest);
setTimeout(function() {
// Delete the file
fs.unlinkSync(fileToTest);
setTimeout(function() {
// Send two more messages to the file
n1.receive({payload:"three"});
n1.receive({payload:"four"});
}, wait);
}, wait);
}
if (count === 3) {
var f = fs.readFileSync(fileToTest).toString();
f.should.equal("threefour");
fs.unlinkSync(fileToTest);
done();
var f = fs.readFileSync(fileToTest).toString();
f.should.equal("threefour");
fs.unlinkSync(fileToTest);
done();
}
} catch(err) {
} catch(err) {
done(err);
}
count++;
}
catch (e) {
done(e);
}
}
count++;
}
catch (e) {
done(e);
}
});
// Send two messages to the file
@@ -197,7 +220,7 @@ describe('file Nodes', function() {
n2.on("input", function (msg) {
try {
msg.should.have.property("payload");
data.should.containDeep([msg.payload]);
data.should.containDeep([msg.payload]);
if (count == 1) {
// Check they got appended as expected
var f = fs.readFileSync(fileToTest).toString();
@@ -256,25 +279,25 @@ describe('file Nodes', function() {
var n2 = helper.getNode("helperNode1");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "fine");
msg.should.have.property("filename", fileToTest);
try {
msg.should.have.property("payload", "fine");
msg.should.have.property("filename", fileToTest);
var f = fs.readFileSync(fileToTest).toString();
if (os.type() !== "Windows_NT") {
var f = fs.readFileSync(fileToTest).toString();
if (os.type() !== "Windows_NT") {
f.should.have.length(5);
f.should.equal("fine\n");
}
else {
}
else {
f.should.have.length(6);
f.should.equal("fine\r\n");
}
done();
}
catch (e) {
done(e);
}
});
}
done();
}
catch (e) {
done(e);
}
});
n1.receive({payload:"fine", filename:fileToTest});
});
@@ -540,6 +563,93 @@ describe('file Nodes', function() {
});
});
it('should write to multiple files', function(done) {
var flow = [{id:"fileNode1", type:"file", name: "fileNode", "appendNewline":true, "overwriteFile":true, "createDir":true, wires: [["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
var tmp_path = path.join(resourcesDir, "tmp");
var len = 1024*1024*10;
var file_count = 5;
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileNode1");
var n2 = helper.getNode("helperNode1");
var count = 0;
n2.on("input", function(msg) {
try {
count++;
if (count == file_count) {
for(var i = 0; i < file_count; i++) {
var name = path.join(tmp_path, String(i));
var f = fs.readFileSync(name);
f.should.have.length(len);
f[0].should.have.equal(i);
}
fs.removeSync(tmp_path);
done();
}
}
catch (e) {
try {
fs.removeSync(tmp_path);
}
catch (e1) {
}
done(e);
}
});
for(var i = 0; i < file_count; i++) {
var data = new Buffer(len);
data.fill(i);
var name = path.join(tmp_path, String(i));
var msg = {payload:data, filename:name};
n1.receive(msg);
}
});
});
it('should write to multiple files if node is closed', function(done) {
var flow = [{id:"fileNode1", type:"file", name: "fileNode", "appendNewline":true, "overwriteFile":true, "createDir":true, wires: [["helperNode1"]]},
{id:"helperNode1", type:"helper"}];
var tmp_path = path.join(resourcesDir, "tmp");
var len = 1024*1024*10;
var file_count = 5;
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileNode1");
var n2 = helper.getNode("helperNode1");
var count = 0;
n2.on("input", function(msg) {
try {
count++;
if (count == file_count) {
for(var i = 0; i < file_count; i++) {
var name = path.join(tmp_path, String(i));
var f = fs.readFileSync(name);
f.should.have.length(len);
f[0].should.have.equal(i);
}
fs.removeSync(tmp_path);
done();
}
}
catch (e) {
try {
fs.removeSync(tmp_path);
}
catch (e1) {
}
done(e);
}
});
for(var i = 0; i < file_count; i++) {
var data = new Buffer(len);
data.fill(i);
var name = path.join(tmp_path, String(i));
var msg = {payload:data, filename:name};
n1.receive(msg);
}
n1.close();
});
});
});
@@ -572,7 +682,7 @@ describe('file Nodes', function() {
it('should read in a file and output a buffer', function(done) {
var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", "filename":fileToTest, "format":"", wires:[["n2"]]},
{id:"n2", type:"helper"}];
{id:"n2", type:"helper"}];
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileInNode1");
var n2 = helper.getNode("n2");
@@ -589,7 +699,7 @@ describe('file Nodes', function() {
it('should read in a file and output a utf8 string', function(done) {
var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "filename":fileToTest, "format":"utf8", wires:[["n2"]]},
{id:"n2", type:"helper"}];
{id:"n2", type:"helper"}];
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileInNode1");
var n2 = helper.getNode("n2");
@@ -610,7 +720,7 @@ describe('file Nodes', function() {
it('should read in a file and output split lines with parts', function(done) {
var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", filename:fileToTest, format:"lines", wires:[["n2"]]},
{id:"n2", type:"helper"}];
{id:"n2", type:"helper"}];
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileInNode1");
var n2 = helper.getNode("n2");
@@ -648,7 +758,7 @@ describe('file Nodes', function() {
var line = data.join("\n");
fs.writeFileSync(fileToTest, line);
var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", filename:fileToTest, format:"lines", wires:[["n2"]]},
{id:"n2", type:"helper"}];
{id:"n2", type:"helper"}];
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileInNode1");
var n2 = helper.getNode("n2");
@@ -681,7 +791,7 @@ describe('file Nodes', function() {
it('should read in a file and output a buffer with parts', function(done) {
var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", filename:fileToTest, format:"stream", wires:[["n2"]]},
{id:"n2", type:"helper"}];
{id:"n2", type:"helper"}];
helper.load(fileNode, flow, function() {
var n1 = helper.getNode("fileInNode1");
var n2 = helper.getNode("n2");

View File

@@ -225,7 +225,7 @@ describe("api/editor/library", function() {
throw err;
}
res.body.should.have.property('sendFile',
'node-module:example-one');
fspath.resolve('node-module') + ':example-one');
done();
});
});
@@ -243,7 +243,8 @@ describe("api/editor/library", function() {
throw err;
}
res.body.should.have.property('sendFile',
'@org_scope/node_package:example-one');
fspath.resolve('@org_scope/node_package') +
':example-one');
done();
});
});

View File

@@ -19,6 +19,7 @@ var request = require('supertest');
var express = require('express');
var sinon = require('sinon');
var when = require('when');
var bodyParser = require('body-parser');
var app = express();
var info = require("../../../../red/api/editor/settings");
@@ -29,6 +30,7 @@ describe("api/editor/settings", function() {
before(function() {
sinon.stub(theme,"settings",function() { return { test: 456 };});
app = express();
app.use(bodyParser.json());
app.get("/settings",info.runtimeSettings);
app.get("/settingsWithUser",function(req,res,next) {
req.user = {
@@ -40,12 +42,13 @@ describe("api/editor/settings", function() {
}
next();
},info.runtimeSettings);
app.get("/settings/user", info.userSettings);
app.post("/settings/user", info.updateUserSettings);
});
after(function() {
theme.settings.restore();
});
it('returns the filtered settings', function(done) {
info.init({
settings: {
@@ -120,6 +123,83 @@ describe("api/editor/settings", function() {
done();
});
});
it('returns user settings', function (done) {
info.init({
settings: {
getUserSettings: function () {
return {
"editor": {
"view": {
"view-grid-size": "20",
"view-node-status": true,
"view-show-tips": true,
"view-snap-grid": true,
"view-show-grid": true
}
}
};
}
}
});
request(app)
.get("/settings/user")
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
res.body.should.have.property("editor");
res.body.editor.should.have.property("view");
res.body.editor.view.should.have.property("view-grid-size", "20");
res.body.editor.view.should.have.property("view-node-status", true);
res.body.editor.view.should.have.property("view-show-tips", true);
res.body.editor.view.should.have.property("view-snap-grid", true);
res.body.editor.view.should.have.property("view-show-grid", true);
done();
});
});
it('sets user settings', function (done) {
info.init({
settings: {
getUserSettings: function () {
return {};
},
setUserSettings: function (username, currentSettings) {
currentSettings.should.have.property("editor");
currentSettings.editor.should.have.property("view");
currentSettings.editor.view.should.have.property("view-grid-size", "21");
currentSettings.editor.view.should.have.property("view-node-status", false);
currentSettings.editor.view.should.have.property("view-show-tips", false);
currentSettings.editor.view.should.have.property("view-snap-grid", false);
currentSettings.editor.view.should.have.property("view-show-grid", false);
return when.resolve();
}
},
log: {
audit: function () {}
}
});
request(app)
.post("/settings/user")
.send({
"editor": {
"view": {
"view-grid-size": "21",
"view-node-status": false,
"view-show-tips": false,
"view-snap-grid": false,
"view-show-grid": false
}
}
})
.expect(204)
.end(function (err, res) {
res.should.have.property("status");
res.status.should.equal(204);
res.should.have.property("text", "");
done();
});
});
it('includes project settings if projects available', function(done) {
info.init({
settings: {
@@ -277,7 +357,7 @@ describe("api/editor/settings", function() {
res.body.editorTheme.should.have.property("palette",{editable:false});
done();
});
})
});
});
});

View File

@@ -113,6 +113,21 @@ describe('context', function() {
context2.global.get("foo").should.equal("test");
});
it('context.flow/global are not enumerable', function() {
var context1 = Context.get("1","flowA");
Object.keys(context1).length.should.equal(0);
Object.keys(context1.flow).length.should.equal(0);
Object.keys(context1.global).length.should.equal(0);
})
it('context.flow/global cannot be deleted', function() {
var context1 = Context.get("1","flowA");
delete context1.flow;
should.exist(context1.flow);
delete context1.global;
should.exist(context1.global);
})
it('deletes context',function() {
var context = Context.get("1","flowA");
should.not.exist(context.get("foo"));
@@ -191,15 +206,26 @@ describe('context', function() {
});
it('returns functionGlobalContext value if store value undefined', function() {
Context.init({functionGlobalContext: {foo:"bar"}});
Context.load().then(function(){
return Context.load().then(function(){
var context = Context.get("1","flowA");
var v = context.global.get('foo');
v.should.equal('bar');
});
})
it('returns functionGlobalContext sub-value if store value undefined', function() {
Context.init({functionGlobalContext: {foo:{bar:123}}});
return Context.load().then(function(){
var context = Context.get("1","flowA");
var v = context.global.get('foo.bar');
should.equal(v,123);
});
})
});
describe('external context storage',function() {
@@ -473,6 +499,23 @@ describe('context', function() {
done();
}).catch(done);
});
it('should allow the store name to be provide in the key', function(done) {
Context.init({contextStorage:contextDefaultStorage});
Context.load().then(function(){
var context = Context.get("1","flow");
var cb = function(){done("An error occurred")}
context.set("#:(test)::foo","bar");
context.get("#:(test)::foo");
stubGet2.called.should.be.false();
stubSet2.called.should.be.false();
stubSet.calledWithExactly("1:flow","foo","bar",undefined).should.be.true();
stubGet.calledWith("1:flow","foo").should.be.true();
done();
}).catch(done);
});
it('should use default as the alias of other context', function(done) {
Context.init({contextStorage:contextAlias});
Context.load().then(function(){
@@ -579,16 +622,16 @@ describe('context', function() {
});
it('should return multiple functionGlobalContext values if key is an array', function(done) {
var fGC = { "foo1": 456, "foo2": 789 };
var fGC = { "foo1": 456, "foo2": {"bar":789} };
Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC });
Context.load().then(function(){
var context = Context.get("1","flow");
context.global.get(["foo1","foo2","foo3"], "memory", function(err,foo1,foo2,foo3){
context.global.get(["foo1","foo2.bar","foo3"], "memory", function(err,foo1,foo2,foo3){
if (err) {
done(err);
} else {
foo1.should.be.equal(456);
foo2.should.be.equal(789);
should.equal(foo1, 456);
should.equal(foo2, 789);
should.not.exist(foo3);
done();
}

View File

@@ -672,7 +672,7 @@ describe('localfilesystem',function() {
it('should enumerate context keys in the cache',function() {
var globalData = {foo:"bar"};
fs.outputFile(path.join(resourcesDir,defaultContextBase,"global","global.json"), JSON.stringify(globalData,null,4), "utf8").then(function(){
return fs.outputFile(path.join(resourcesDir,defaultContextBase,"global","global.json"), JSON.stringify(globalData,null,4), "utf8").then(function(){
context = LocalFileSystem({dir: resourcesDir, cache: true, flushInterval: 2});
return context.open()
}).then(function(){

View File

@@ -96,7 +96,7 @@ describe('memory',function() {
context.set("nodeX","three","test3");
context.set("nodeX","four","test4");
var values = context.get("nodeX",["one","unknown"]);
var values = context.get("nodeX",["one","unknown.with.multiple.levels"]);
values.should.eql(["test1",undefined])
})
it('should throw error if bad key included in multiple keys', function() {

View File

@@ -128,7 +128,7 @@ describe("red/nodes/registry/localfilesystem",function() {
}
return _join.apply(null,arguments);
}));
localfilesystem.init({i18n:{registerMessageCatalog:function(){}},events:{emit:function(){}},settings:{coreNodesDir:moduleDir}});
localfilesystem.init({log:{debug:function(){}},i18n:{registerMessageCatalog:function(){}},events:{emit:function(){}},settings:{coreNodesDir:moduleDir}});
var nodeList = localfilesystem.getNodeFiles();
nodeList.should.have.a.property("node-red");
var nm = nodeList['node-red'];