Compare commits

..

55 Commits

Author SHA1 Message Date
Nick O'Leary
d5b424910f Merge branch 'dev' into Fix-join-to-ignore-parts-in-manual-mode 2024-03-07 16:05:41 +00:00
Nick O'Leary
d94d13737f Merge pull request #4572 from kevinGodell/dev
let settings.httpNodeAuth accept single middleware or array of middlewares
2024-03-07 15:48:12 +00:00
Nick O'Leary
b1fa4918e3 Merge pull request #4386 from node-red/make-split/join-more-flexible
let split node specify property to split on, and join auto join correctly
2024-03-07 15:47:08 +00:00
Nick O'Leary
742aa2fa0d Merge branch 'dev' into make-split/join-more-flexible 2024-03-07 15:40:56 +00:00
Nick O'Leary
ce133c1c04 Merge pull request #4540 from Steve-Mcl/3934-csv-rfc4180
Add RFC4180 compliant mode to CSV node
2024-03-07 15:39:25 +00:00
Nick O'Leary
e4dc1779c3 Merge pull request #4590 from node-red/jsonata-update
Upgrade to JSONata 2.x
2024-03-07 14:28:35 +00:00
Nick O'Leary
22b4ab6bb2 Merge pull request #4468 from node-red/timestamp-formatting
Timestamp formatting
2024-03-07 14:28:23 +00:00
Nick O'Leary
2dcff51125 Update timestamp formats 2024-03-04 16:51:06 +00:00
Nick O'Leary
b50e0533eb Add js date option to inject 2024-03-04 16:35:59 +00:00
Nick O'Leary
711545539f Allow typedInput timestamp to specify format 2024-03-04 16:35:54 +00:00
Nick O'Leary
a6cbceed28 Upgrade to JSONata 2.x 2024-03-04 16:32:31 +00:00
Nick O'Leary
6802539ccc Merge pull request #4571 from node-red/bump-node-version
Bump minimum version to node 18
2024-02-20 10:37:25 +00:00
Kevin Godell
74efaa3c2d let settings.httpNodeAuth accept single middleware or array of middlewares to replace built-in basic-auth middleware at top level of RED.httpNode 2024-02-19 16:42:14 -06:00
Nick O'Leary
a5223709ba Bump minimum version to node 18 2024-02-19 16:38:06 +00:00
Nick O'Leary
2291dc6132 Merge branch 'master' into dev 2024-02-19 16:14:58 +00:00
Steve-Mcl
b2548c158d fix layout and missing tip 2024-02-09 20:50:24 +00:00
Steve-Mcl
5a48d6d4cd Merge branch 'dev' into 3934-csv-rfc4180 2024-02-09 20:33:49 +00:00
Nick O'Leary
7ee2b93b10 Merge pull request #4553 from lgrkvst/dev
Allow RED.view.select to select links
2024-02-05 16:28:17 +00:00
Christian Lagerkvist
cc611a7a02 Fixes #4551 2024-01-30 10:37:52 +01:00
Steve-Mcl
1a9c34fe40 Merge branch 'dev' into 3934-csv-rfc4180 2024-01-23 10:59:05 +00:00
Steve-Mcl
ff8eb0ec2b Add RFC spec mode to CSV node
closes #3934
2024-01-23 10:35:08 +00:00
Nick O'Leary
f66b48e586 Merge pull request #4525 from node-red/fix-change-node-boolean-response
Fix change node to return boolean if asked
2024-01-16 11:58:30 +00:00
Nick O'Leary
931a2344b4 Merge pull request #4480 from node-red/context-auto-complete
Add auto-complete to flow/global typedInput types
2024-01-15 15:53:46 +00:00
Dave Conway-Jones
dd3c75d298 Fix change node to return boolean if asked
to fix #4372
2024-01-14 12:56:25 +00:00
Nick O'Leary
4a4a15de93 Fix context store handling in autocomplete 2024-01-09 01:05:09 +00:00
Nick O'Leary
a007ab7f2e Merge pull request #4492 from node-red/envvar-auto-complete
Add auto-complete for env vars
2024-01-08 23:41:12 +00:00
Nick O'Leary
54e6d60fe5 Add simple caching of env var lookup 2024-01-05 21:07:20 +00:00
Nick O'Leary
c2710f4f6f Add auto-complete for env vars 2023-12-20 17:52:52 +00:00
Nick O'Leary
20187b51b1 Fix up cache scope 2023-12-20 16:51:34 +00:00
Nick O'Leary
4be6d57d98 Apply suggestions from code review
Co-authored-by: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com>
2023-12-20 16:39:52 +00:00
Nick O'Leary
a77f8cc3e9 Clear context cache when closing edit dialog 2023-12-15 15:09:15 +00:00
Nick O'Leary
ea4c0cdbee Fix error when switching context types 2023-12-14 17:14:56 +00:00
Nick O'Leary
7197153fd5 Support bracket-notation in auto complete when needed 2023-12-11 21:18:44 +00:00
Nick O'Leary
b9c1dedab3 Add auto-complete to flow/global typedInput types 2023-12-11 17:55:02 +00:00
Nick O'Leary
918943816f Merge branch 'master' into dev 2023-12-08 10:27:04 +00:00
Nick O'Leary
33cf34f7c7 Merge branch 'master' into dev 2023-12-04 15:58:45 +00:00
Nick O'Leary
5b5b06cc06 Merge pull request #4406 from node-red/tcp-request-node-reset-when-in-stay-connected-mode
Let msg.reset  reset Tcp request node connection when in stay connected mode
2023-11-07 17:42:17 +00:00
Dave Conway-Jones
f49f692ffa Better fix for TCP node reset
now handles reply out node,
and can specify which connection to reset.
2023-11-03 11:57:16 +00:00
Dave Conway-Jones
10ce681d46 Fix join node to ignore parts in manual mode
to fix #1957
2023-11-01 13:59:09 +00:00
Nick O'Leary
08c6ea94cb Merge pull request #4347 from ZJvandeWeg/zj-remove-production-flag-npm
npm: Remove production flag on npm invocation
2023-11-01 14:18:14 +01:00
Nick O'Leary
fea1da5542 Merge pull request #4402 from node-red/Let-debug-status-length-be-settable
Let debug node status msg length be settable via settings
2023-11-01 14:13:38 +01:00
Dave Conway-Jones
32e8f4eac6 Add help info 2023-11-01 12:33:57 +00:00
Dave Conway-Jones
bfe5a8a986 Update 31-tcpin.js
don't send if payload not defined.
2023-11-01 12:27:11 +00:00
Dave Conway-Jones
f2cb5ea44e Allow msg.reset to reset connection when tcp request in stay connected mode 2023-11-01 12:07:50 +00:00
Dave Conway-Jones
c7335ed25b Let debug node status msg length be settable via settings 2023-10-31 09:11:17 +00:00
Dave Conway-Jones
5fda57c730 too enthusiastic clearing up property
reverted
2023-10-18 13:51:40 +01:00
Dave Conway-Jones
9fd929ac1e let split node specify property to split on
and let join auto join the correct property
or manually the specified one.
2023-10-17 21:12:13 +01:00
Nick O'Leary
eb940d6d57 Merge pull request #4367 from hlovdal/timer_testing_fix
Timer testing fix
2023-09-25 18:19:30 +01:00
Håkon Løvdal
9091935d77 Update variable names 2023-09-25 18:53:11 +02:00
Håkon Løvdal
34e8d2b051 Add workaround for timers triggering too early in test 2023-09-24 18:16:59 +02:00
Håkon Løvdal
0c2ab13c48 Print all delta values in case of error, not just the last value
Which might not even be the one triggering the error condition.
2023-09-24 18:16:59 +02:00
Håkon Løvdal
9489953a8f Introduce timeout constant 2023-09-24 18:16:59 +02:00
ZJ van de Weg
54d4079457 npm: Remove production flag on npm invocation
When installing packages the `--production` flag used to be added to the
arguments that `npm` received. As npm wants developers to use the
`--omit=dev` flag instead it warned users on STDERR. Standard error was
captured by Node-RED and output to the logs as being an error. This
caught users off-guard and they expected something to have gone
wrong.

With this change the `--omit=dev` is used instead, to remove the
warning.

This change works for NPM of version 8 and beyond[1], included in
Node.JS 16. This change will not work on NPM version 6[2] which is included
in Node.JS 14[3].

[1]: https://docs.npmjs.com/cli/v8/commands/npm-install#omit
[2]: https://docs.npmjs.com/cli/v6/commands/npm-install
[3]: https://nodejs.org/en/download/releases#looking-for-latest-release-of-a-version-branch
2023-09-17 08:43:11 +02:00
Nick O'Leary
cef3a01042 Merge pull request #4322 from node-red/prep4
Bump to 4.0.0-dev
2023-09-06 15:04:41 +01:00
Nick O'Leary
0c042abcab Bump to 4.0.0-dev 2023-09-06 14:45:45 +01:00
74 changed files with 3497 additions and 1067 deletions

View File

@@ -12,12 +12,11 @@ permissions:
jobs:
build:
permissions:
checks: write # for coverallsapp/github-action to create new checks
contents: read # for actions/checkout to fetch code
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
@@ -29,8 +28,3 @@ jobs:
- name: Run tests
run: |
npm run test
# - name: Publish to coveralls.io
# if: ${{ matrix.node-version == 16 }}
# uses: coverallsapp/github-action@v1.1.2
# with:
# github-token: ${{ github.token }}

View File

@@ -1,42 +1,3 @@
#### 3.1.9: Maintenance Release
- Prevent subflow being added to itself (#4654) @knolleary
- Fix use of spawn on windows with cmd files (#4652) @knolleary
- Guard refresh of unknown subflow (#4640) @knolleary
- Fix subflow module sending messages to debug sidebar (#4642) @knolleary
#### 3.1.8: Maintenance Release
- Add validation and error handling on subflow instance properties (#4632) @knolleary
- Hide import/export context menu if disabled in theme (#4633) @knolleary
- Show change indicator on subflow tabs (#4631) @knolleary
- Bump dependencies (#4630) @knolleary
- Reset workspace index when clearing nodes (#4619) @knolleary
- Remove typo in global config (#4613) @kazuhitoyokoi
#### 3.1.7: Maintenance Release
- Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi
- Update jsonata version (#4593) @hardillb
#### 3.1.6: Maintenance Release
Editor
- Do not flag env var in num typedInput as error (#4582) @knolleary
- Fix example flow name in import dialog (#4578) @kazuhitoyokoi
- Fix missing node icons in workspace (#4570) @knolleary
Runtime
- Handle undefined env vars (#4581) @knolleary
- fix: Removed offending MD5 crypto hash and replaced with SHA1 and SHA256 … (#4568) @JaysonHurst
- chore: remove never use import code (#4580) @giscafer
Nodes
- fix: template node zh-CN translation (#4575) @giscafer
#### 3.1.5: Maintenance Release
Runtime

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "3.1.9",
"version": "4.0.0-dev",
"description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org",
"license": "Apache-2.0",
@@ -41,7 +41,7 @@
"cors": "2.8.5",
"cronosjs": "1.7.1",
"denque": "2.1.0",
"express": "4.19.2",
"express": "4.18.2",
"express-session": "1.17.3",
"form-data": "4.0.0",
"fs-extra": "11.1.1",
@@ -54,7 +54,7 @@
"is-utf8": "0.2.1",
"js-yaml": "4.1.0",
"json-stringify-safe": "5.0.1",
"jsonata": "1.8.7",
"jsonata": "2.0.4",
"lodash.clonedeep": "^4.5.0",
"media-typer": "1.1.0",
"memorystore": "1.6.7",
@@ -64,7 +64,7 @@
"mqtt": "4.3.7",
"multer": "1.4.5-lts.1",
"mustache": "4.2.0",
"node-red-admin": "^3.1.3",
"node-red-admin": "^3.1.2",
"node-watch": "0.7.4",
"nopt": "5.0.0",
"oauth2orize": "1.11.1",
@@ -74,7 +74,7 @@
"passport-oauth2-client-password": "0.1.2",
"raw-body": "2.5.2",
"semver": "7.5.4",
"tar": "6.2.1",
"tar": "6.1.13",
"tough-cookie": "4.1.3",
"uglify-js": "3.17.4",
"uuid": "9.0.0",
@@ -112,7 +112,7 @@
"mermaid": "^10.4.0",
"minami": "1.2.3",
"mocha": "9.2.2",
"node-red-node-test-helper": "^0.3.3",
"node-red-node-test-helper": "^0.3.2",
"nodemon": "2.0.20",
"proxy": "^1.0.2",
"sass": "1.62.1",
@@ -122,6 +122,6 @@
"supertest": "6.3.3"
},
"engines": {
"node": ">=14"
"node": ">=18"
}
}

View File

@@ -33,6 +33,9 @@ module.exports = {
store: req.query['store'],
req: apiUtils.getRequestLogObject(req)
}
if (req.query['keysOnly'] !== undefined) {
opts.keysOnly = true
}
runtimeAPI.context.getValue(opts).then(function(result) {
res.json(result);
}).catch(function(err) {

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var apiUtils = require("../util");
var runtimeAPI;
var settings;
var theme = require("../editor/theme");

View File

@@ -205,10 +205,9 @@ function genericStrategy(adminApp,strategy) {
passport.use(new strategy.strategy(options, verify));
adminApp.get('/auth/strategy',
passport.authenticate(strategy.name, {
session:false,
passport.authenticate(strategy.name, {session:false,
failureMessage: true,
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed'
failureRedirect: settings.httpAdminRoot
}),
completeGenerateStrategyAuth,
handleStrategyError
@@ -222,7 +221,7 @@ function genericStrategy(adminApp,strategy) {
passport.authenticate(strategy.name, {
session:false,
failureMessage: true,
failureRedirect: settings.httpAdminRoot + '?session_message=Login Failed'
failureRedirect: settings.httpAdminRoot
}),
completeGenerateStrategyAuth,
handleStrategyError

View File

@@ -18,6 +18,7 @@ var BearerStrategy = require('passport-http-bearer').Strategy;
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy;
var passport = require("passport");
var crypto = require("crypto");
var util = require("util");
var Tokens = require("./tokens");

View File

@@ -14,9 +14,11 @@
* limitations under the License.
**/
var express = require("express");
var path = require('path');
var comms = require("./comms");
var library = require("./library");
var info = require("./settings");
var auth = require("../auth");

View File

@@ -15,6 +15,8 @@
**/
var apiUtils = require("../util");
var fs = require('fs');
var fspath = require('path');
var runtimeAPI;

View File

@@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var fs = require('fs');
var path = require('path');
// var apiUtil = require('../util');
var i18n = require("@node-red/util").i18n; // TODO: separate module

View File

@@ -15,6 +15,7 @@
**/
var apiUtils = require("../util");
var express = require("express");
var runtimeAPI;
var settings;

View File

@@ -14,6 +14,7 @@
* limitations under the License.
**/
var express = require("express");
var util = require("util");
var path = require("path");
var fs = require("fs");

View File

@@ -99,7 +99,7 @@ module.exports = {
// settings.instanceId is set asynchronously to the editor-api
// being initiaised. So we defer calculating the cacheBuster hash
// until the first load of the editor
cacheBuster = crypto.createHash('sha1').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12)
cacheBuster = crypto.createHash('md5').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12)
}
let sessionMessages;

View File

@@ -24,8 +24,11 @@
* @namespace @node-red/editor-api
*/
var express = require("express");
var bodyParser = require("body-parser");
var util = require('util');
var passport = require('passport');
var cors = require('cors');
var auth = require("./auth");
var apiUtil = require("./util");

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "3.1.9",
"version": "4.0.0-dev",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,14 +16,14 @@
}
],
"dependencies": {
"@node-red/util": "3.1.9",
"@node-red/editor-client": "3.1.9",
"@node-red/util": "4.0.0-dev",
"@node-red/editor-client": "4.0.0-dev",
"bcryptjs": "2.4.3",
"body-parser": "1.20.2",
"clone": "2.1.2",
"cors": "2.8.5",
"express-session": "1.17.3",
"express": "4.19.2",
"express": "4.18.2",
"memorystore": "1.6.7",
"mime": "3.0.0",
"multer": "1.4.5-lts.1",

View File

@@ -925,6 +925,12 @@
"jsonata": "expression",
"env": "env variable",
"cred": "credential"
},
"date": {
"format": {
"timestamp": "milliseconds since epoch",
"object": "JavaScript Date Object"
}
}
},
"editableList": {

View File

@@ -303,8 +303,7 @@
"missingType": "不正なフロー - __index__ 番目の要素に'type'プロパティがありません"
},
"conflictNotification1": "読み込もうとしているノードのいくつかは、既にワークスペース内に存在しています。",
"conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。",
"alreadyExists": "本ノードは既に存在"
"conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。"
},
"copyMessagePath": "パスをコピーしました",
"copyMessageValue": "値をコピーしました",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-client",
"version": "3.1.9",
"version": "4.0.0-dev",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -547,16 +547,12 @@ RED.nodes = (function() {
* @param {String} z tab id
*/
checkTabState: function (z) {
const ws = workspaces[z] || subflows[z]
const ws = workspaces[z]
if (ws) {
const contentsChanged = tabDirtyMap[z].size > 0 || tabDeletedNodesMap[z].size > 0
if (Boolean(ws.contentsChanged) !== contentsChanged) {
ws.contentsChanged = contentsChanged
if (ws.type === 'tab') {
RED.events.emit("flows:change", ws);
} else {
RED.events.emit("subflows:change", ws);
}
RED.events.emit("flows:change", ws);
}
}
}
@@ -1029,22 +1025,7 @@ RED.nodes = (function() {
RED.nodes.registerType("subflow:"+sf.id, {
defaults:{
name:{value:""},
env:{value:[], validate: function(value) {
const errors = []
if (value) {
value.forEach(env => {
const r = RED.utils.validateTypedProperty(env.value, env.type)
if (r !== true) {
errors.push(env.name+': '+r)
}
})
}
if (errors.length === 0) {
return true
} else {
return errors
}
}}
env:{value:[]}
},
icon: function() { return sf.icon||"subflow.svg" },
category: sf.category || "subflows",

View File

@@ -54,25 +54,26 @@
return icon;
}
var autoComplete = function(options) {
function getMatch(value, searchValue) {
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const len = idx > -1 ? searchValue.length : 0;
return {
index: idx,
found: idx > -1,
pre: value.substring(0,idx),
match: value.substring(idx,idx+len),
post: value.substring(idx+len),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
function getMatch(value, searchValue) {
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const len = idx > -1 ? searchValue.length : 0;
return {
index: idx,
found: idx > -1,
pre: value.substring(0,idx),
match: value.substring(idx,idx+len),
post: value.substring(idx+len),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
}
const msgAutoComplete = function(options) {
return function(val) {
var matches = [];
options.forEach(opt => {
@@ -102,6 +103,197 @@
}
}
function getEnvVars (obj, envVars = {}) {
contextKnownKeys.env = contextKnownKeys.env || {}
if (contextKnownKeys.env[obj.id]) {
return contextKnownKeys.env[obj.id]
}
let parent
if (obj.type === 'tab' || obj.type === 'subflow') {
RED.nodes.eachConfig(function (conf) {
if (conf.type === "global-config") {
parent = conf;
}
})
} else if (obj.g) {
parent = RED.nodes.group(obj.g)
} else if (obj.z) {
parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z)
}
if (parent) {
getEnvVars(parent, envVars)
}
if (obj.env) {
obj.env.forEach(env => {
envVars[env.name] = obj
})
}
contextKnownKeys.env[obj.id] = envVars
return envVars
}
const envAutoComplete = function (val) {
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done([])
return
}
const editingNode = editStack.pop()
if (!editingNode) {
return []
}
const envVarsMap = getEnvVars(editingNode)
const envVars = Object.keys(envVarsMap)
const matches = []
const i = val.lastIndexOf('${')
let searchKey = val
let isSubkey = false
if (i > -1) {
if (val.lastIndexOf('}') < i) {
searchKey = val.substring(i+2)
isSubkey = true
}
}
envVars.forEach(v => {
let valMatch = getMatch(v, searchKey);
if (valMatch.found) {
const optSrc = envVarsMap[v]
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
if (optSrc) {
const optEl = $('<div>').css({ "font-size": "0.8em" });
let label
if (optSrc.type === 'global-config') {
label = RED._('sidebar.context.global')
} else if (optSrc.type === 'group') {
label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id)
} else {
label = RED.utils.getNodeLabel(optSrc) || optSrc.id
}
optEl.append(generateSpans({ match: label }));
optEl.appendTo(element);
}
matches.push({
value: isSubkey ? val + v + '}' : v,
label: element,
i: valMatch.index
});
}
})
matches.sort(function(A,B){return A.i-B.i})
return matches
}
let contextKnownKeys = {}
let contextCache = {}
if (RED.events) {
RED.events.on("editor:close", function () {
contextCache = {}
contextKnownKeys = {}
});
}
const contextAutoComplete = function() {
const that = this
const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
contextKnownKeys[scope] = contextKnownKeys[scope] || {}
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
if (searchKey.length > 0) {
try {
RED.utils.normalisePropertyExpression(searchKey)
} catch (err) {
// Not a valid context key, so don't try looking up
done()
return
}
}
const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
if (contextCache[url]) {
// console.log('CACHED', url)
done()
} else {
// console.log('GET', url)
$.getJSON(url, function(data) {
// console.log(data)
contextCache[url] = true
const result = data[store] || {}
const keys = result.keys || []
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
keys.forEach(key => {
if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
contextKnownKeys[scope][store].add(keyPrefix + key)
} else {
contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
}
})
done()
})
}
}
const getContextKeys = function(key, done) {
const keyParts = key.split('.')
const partialKey = keyParts.pop()
let scope = that.propertyType
if (scope === 'flow') {
// Get the flow id of the node we're editing
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done([])
return
}
const editingNode = editStack.pop()
if (editingNode.z) {
scope = `${scope}/${editingNode.z}`
} else {
done([])
return
}
}
const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue
const searchKey = keyParts.join('.')
getContextKeysFromRuntime(scope, store, searchKey, function() {
if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) {
getContextKeysFromRuntime(scope, store, key, function() {
done(contextKnownKeys[scope][store])
})
}
done(contextKnownKeys[scope][store])
})
}
return function(val, done) {
getContextKeys(val, function (keys) {
const matches = []
keys.forEach(v => {
let optVal = v
let valMatch = getMatch(optVal, val);
if (!valMatch.found && val.length > 0 && val.endsWith('.')) {
// Search key ends in '.' - but doesn't match. Check again
// with [" at the end instead so we match bracket notation
valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
}
if (valMatch.found) {
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
matches.push({
value: optVal,
label: element,
});
}
})
matches.sort(function(a, b) { return a.value.localeCompare(b.value) });
done(matches);
})
}
}
// This is a hand-generated list of completions for the core nodes (based on the node help html).
var msgCompletions = [
{ value: "payload" },
@@ -166,20 +358,22 @@
{ value: "_session", source: ["websocket out","tcp out"] },
]
var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
flow: {value:"flow",label:"flow.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport,
valueLabel: contextLabel
valueLabel: contextLabel,
autoComplete: contextAutoComplete
},
global: {value:"global",label:"global.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport,
valueLabel: contextLabel
valueLabel: contextLabel,
autoComplete: contextAutoComplete
},
str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) {
@@ -214,7 +408,25 @@
}
},
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
date: {
value:"date",
label:"timestamp",
icon:"fa fa-clock-o",
options:[
{
label: 'milliseconds since epoch',
value: ''
},
{
label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
value: 'iso'
},
{
label: 'JavaScript Date Object',
value: 'object'
}
]
},
jsonata: {
value: "jsonata",
label: "expression",
@@ -251,7 +463,8 @@
env: {
value: "env",
label: "env variable",
icon: "red/images/typedInput/env.svg"
icon: "red/images/typedInput/env.svg",
autoComplete: envAutoComplete
},
node: {
value: "node",
@@ -427,6 +640,7 @@
}
var nlsd = false;
let contextStoreOptions;
$.widget( "nodered.typedInput", {
_create: function() {
@@ -438,7 +652,7 @@
}
}
var contextStores = RED.settings.context.stores;
var contextOptions = contextStores.map(function(store) {
contextStoreOptions = contextStores.map(function(store) {
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
}).sort(function(A,B) {
if (A.value === RED.settings.context.default) {
@@ -449,13 +663,17 @@
return A.value.localeCompare(B.value);
}
})
if (contextOptions.length < 2) {
if (contextStoreOptions.length < 2) {
allOptions.flow.options = [];
allOptions.global.options = [];
} else {
allOptions.flow.options = contextOptions;
allOptions.global.options = contextOptions;
allOptions.flow.options = contextStoreOptions;
allOptions.global.options = contextStoreOptions;
}
// Translate timestamp options
allOptions.date.options.forEach(opt => {
opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
})
}
nlsd = true;
var that = this;
@@ -544,7 +762,7 @@
that.element.trigger('paste',evt);
});
this.input.on('keydown', function(evt) {
if (that.typeMap[that.propertyType].autoComplete) {
if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
return
}
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@@ -967,6 +1185,9 @@
// If previousType is !null, then this is a change of the type, rather than the initialisation
var previousType = this.typeMap[this.propertyType];
previousValue = this.input.val();
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
if (previousType && this.typeChanged) {
if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
@@ -1013,7 +1234,9 @@
this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
}
if (previousType.autoComplete) {
this.input.autoComplete("destroy");
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
}
}
this.propertyType = type;
@@ -1141,6 +1364,16 @@
} else {
this.optionSelectTrigger.hide();
}
if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({
search: searchFunction,
minLength: 0
})
}
}
this.optionMenu = this._createMenu(opt.options,opt,function(v){
if (!opt.multiple) {
@@ -1183,8 +1416,12 @@
this.valueLabelContainer.hide();
this.elementDiv.show();
if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({
search: opt.autoComplete,
search: searchFunction,
minLength: 0
})
}

View File

@@ -118,16 +118,10 @@ RED.contextMenu = (function () {
onselect: 'core:split-wire-with-link-nodes',
disabled: !canEdit || !hasLinks
},
null
null,
{ onselect: 'core:show-import-dialog', label: RED._('common.label.import')},
{ onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') }
)
if (RED.settings.theme("menu.menu-item-import-library", true)) {
insertOptions.push(
{ onselect: 'core:show-import-dialog', label: RED._('common.label.import')},
{ onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') }
)
}
if (hasSelection && canEdit) {
const nodeOptions = []
if (!hasMultipleSelection && !isGroup) {
@@ -200,14 +194,8 @@ RED.contextMenu = (function () {
{ onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() },
{ onselect: 'core:delete-selection', label: RED._('keyboard.deleteSelected'), disabled: !canEdit || !canDelete },
{ onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete },
)
if (RED.settings.theme("menu.menu-item-export-library", true)) {
menuItems.push(
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }
)
}
menuItems.push(
{ onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") }
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") },
{ onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") },
)
}

View File

@@ -612,10 +612,7 @@ RED.deploy = (function() {
}
});
RED.nodes.eachSubflow(function (subflow) {
if (subflow.changed) {
subflow.changed = false;
RED.events.emit("subflows:change", subflow);
}
subflow.changed = false;
});
RED.nodes.eachWorkspace(function (ws) {
if (ws.changed || ws.added) {

View File

@@ -741,16 +741,9 @@ RED.editor = (function() {
}
try {
const rc = editing_node._def.oneditsave.call(editing_node);
var rc = editing_node._def.oneditsave.call(editing_node);
if (rc === true) {
editState.changed = true;
} else if (typeof rc === 'object' && rc !== null ) {
if (rc.changed === true) {
editState.changed = true
}
if (Array.isArray(rc.history) && rc.history.length > 0) {
editState.history = rc.history
}
}
} catch(err) {
console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
@@ -1022,7 +1015,7 @@ RED.editor = (function() {
}
});
}
let historyEvent = {
var historyEvent = {
t:'edit',
node:editing_node,
changes:editState.changes,
@@ -1038,15 +1031,6 @@ RED.editor = (function() {
instances:subflowInstances
}
}
if (editState.history) {
historyEvent = {
t: 'multi',
events: [ historyEvent, ...editState.history ],
dirty: wasDirty
}
}
RED.history.push(historyEvent);
}
editing_node.dirty = true;
@@ -1639,8 +1623,8 @@ RED.editor = (function() {
}
if (!isSameObj(old_env, new_env)) {
editState.changes.env = editing_node.env;
editing_node.env = new_env;
editState.changes.env = editing_node.env;
editState.changed = true;
}
@@ -2103,6 +2087,7 @@ RED.editor = (function() {
}
},
editBuffer: function(options) { showTypeEditor("_buffer", options) },
getEditStack: function () { return [...editStack] },
buildEditForm: buildEditForm,
validateNode: validateNode,
updateNodeProperties: updateNodeProperties,

View File

@@ -514,7 +514,7 @@ RED.editor.codeEditor.monaco = (function() {
_monaco.languages.json.jsonDefaults.setDiagnosticsOptions(diagnosticOptions);
if(modeConfiguration) { _monaco.languages.json.jsonDefaults.setModeConfiguration(modeConfiguration); }
} catch (error) {
console.warn("monaco - Error setting up json options", error)
console.warn("monaco - Error setting up json options", err)
}
}
@@ -526,7 +526,7 @@ RED.editor.codeEditor.monaco = (function() {
if(htmlDefaults) { _monaco.languages.html.htmlDefaults.setOptions(htmlDefaults); }
if(handlebarDefaults) { _monaco.languages.html.handlebarDefaults.setOptions(handlebarDefaults); }
} catch (error) {
console.warn("monaco - Error setting up html options", error)
console.warn("monaco - Error setting up html options", err)
}
}
@@ -546,7 +546,7 @@ RED.editor.codeEditor.monaco = (function() {
if(lessDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(lessDefaults_modeConfiguration); }
if(scssDefaults_modeConfiguration) { _monaco.languages.css.cssDefaults.setDiagnosticsOptions(scssDefaults_modeConfiguration); }
} catch (error) {
console.warn("monaco - Error setting up CSS/SCSS/LESS options", error)
console.warn("monaco - Error setting up CSS/SCSS/LESS options", err)
}
}

View File

@@ -158,10 +158,8 @@ RED.sidebar.help = (function() {
function refreshSubflow(sf) {
var item = treeList.treeList('get',"node-type:subflow:"+sf.id);
if (item) {
item.subflowLabel = sf._def.label().toLowerCase();
item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()}));
}
item.subflowLabel = sf._def.label().toLowerCase();
item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()}));
}
function hideTOC() {

View File

@@ -906,10 +906,7 @@ RED.utils = (function() {
* @returns true if valid, String if invalid
*/
function validateTypedProperty(propertyValue, propertyType, opt) {
if (propertyValue && /^\${[^}]+}$/.test(propertyValue)) {
// Allow ${ENV_VAR} value
return true
}
let error
if (propertyType === 'json') {
try {

View File

@@ -646,128 +646,120 @@ RED.view = (function() {
}
d3.event = event;
var selected_tool = $(ui.draggable[0]).attr("data-palette-type");
var result = createNode(selected_tool);
if (!result) {
return;
}
var historyEvent = result.historyEvent;
var nn = RED.nodes.add(result.node);
var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
nn.l = showLabel;
}
var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
var helperWidth = ui.helper.width();
var helperHeight = ui.helper.height();
var mousePos = d3.touches(this)[0]||d3.mouse(this);
try {
var result = createNode(selected_tool);
if (!result) {
return;
}
var historyEvent = result.historyEvent;
var nn = RED.nodes.add(result.node);
var isLink = (nn.type === "link in" || nn.type === "link out")
var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink;
var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
nn.l = showLabel;
}
var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
var helperWidth = ui.helper.width();
var helperHeight = ui.helper.height();
var mousePos = d3.touches(this)[0]||d3.mouse(this);
try {
var isLink = (nn.type === "link in" || nn.type === "link out")
var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink;
var label = RED.utils.getNodeLabel(nn, nn.type);
var labelParts = getLabelParts(label, "red-ui-flow-node-label");
if (hideLabel) {
nn.w = node_height;
nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
} else {
nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
}
} catch(err) {
}
mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]);
mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]);
mousePos[1] /= scaleFactor;
mousePos[0] /= scaleFactor;
nn.x = mousePos[0];
nn.y = mousePos[1];
var minX = nn.w/2 -5;
if (nn.x < minX) {
nn.x = minX;
}
var minY = nn.h/2 -5;
if (nn.y < minY) {
nn.y = minY;
}
var maxX = space_width -nn.w/2 +5;
if (nn.x > maxX) {
nn.x = maxX;
}
var maxY = space_height -nn.h +5;
if (nn.y > maxY) {
nn.y = maxY;
}
if (snapGrid) {
var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn);
nn.x -= gridOffset.x;
nn.y -= gridOffset.y;
}
var linkToSplice = $(ui.helper).data("splice");
if (linkToSplice) {
spliceLink(linkToSplice, nn, historyEvent)
}
var group = $(ui.helper).data("group");
if (group) {
var oldX = group.x;
var oldY = group.y;
RED.group.addToGroup(group, nn);
var moveEvent = null;
if ((group.x !== oldX) ||
(group.y !== oldY)) {
moveEvent = {
t: "move",
nodes: [{n: group,
ox: oldX, oy: oldY,
dx: group.x -oldX,
dy: group.y -oldY}],
dirty: true
};
}
historyEvent = {
t: 'multi',
events: [historyEvent],
};
if (moveEvent) {
historyEvent.events.push(moveEvent)
}
historyEvent.events.push({
t: "addToGroup",
group: group,
nodes: nn
})
}
RED.history.push(historyEvent);
RED.editor.validateNode(nn);
RED.nodes.dirty(true);
// auto select dropped node - so info shows (if visible)
clearSelection();
nn.selected = true;
movingSet.add(nn);
updateActiveNodes();
updateSelection();
redraw();
if (nn._def.autoedit) {
RED.editor.edit(nn);
}
} catch (error) {
if (error.code != "NODE_RED") {
RED.notify(RED._("notification.error",{message:error.toString()}),"error");
var label = RED.utils.getNodeLabel(nn, nn.type);
var labelParts = getLabelParts(label, "red-ui-flow-node-label");
if (hideLabel) {
nn.w = node_height;
nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
} else {
RED.notify(RED._("notification.error",{message:error.message}),"error");
nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
}
} catch(err) {
}
mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]);
mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]);
mousePos[1] /= scaleFactor;
mousePos[0] /= scaleFactor;
nn.x = mousePos[0];
nn.y = mousePos[1];
var minX = nn.w/2 -5;
if (nn.x < minX) {
nn.x = minX;
}
var minY = nn.h/2 -5;
if (nn.y < minY) {
nn.y = minY;
}
var maxX = space_width -nn.w/2 +5;
if (nn.x > maxX) {
nn.x = maxX;
}
var maxY = space_height -nn.h +5;
if (nn.y > maxY) {
nn.y = maxY;
}
if (snapGrid) {
var gridOffset = RED.view.tools.calculateGridSnapOffsets(nn);
nn.x -= gridOffset.x;
nn.y -= gridOffset.y;
}
var linkToSplice = $(ui.helper).data("splice");
if (linkToSplice) {
spliceLink(linkToSplice, nn, historyEvent)
}
var group = $(ui.helper).data("group");
if (group) {
var oldX = group.x;
var oldY = group.y;
RED.group.addToGroup(group, nn);
var moveEvent = null;
if ((group.x !== oldX) ||
(group.y !== oldY)) {
moveEvent = {
t: "move",
nodes: [{n: group,
ox: oldX, oy: oldY,
dx: group.x -oldX,
dy: group.y -oldY}],
dirty: true
};
}
historyEvent = {
t: 'multi',
events: [historyEvent],
};
if (moveEvent) {
historyEvent.events.push(moveEvent)
}
historyEvent.events.push({
t: "addToGroup",
group: group,
nodes: nn
})
}
RED.history.push(historyEvent);
RED.editor.validateNode(nn);
RED.nodes.dirty(true);
// auto select dropped node - so info shows (if visible)
clearSelection();
nn.selected = true;
movingSet.add(nn);
updateActiveNodes();
updateSelection();
redraw();
if (nn._def.autoedit) {
RED.editor.edit(nn);
}
}
});
@@ -4164,7 +4156,7 @@ RED.view = (function() {
}
var width = img.width * scaleFactor;
if (width > 20) {
scaleFactor *= 20/width;
scalefactor *= 20/width;
width = 20;
}
var height = img.height * scaleFactor;
@@ -6071,19 +6063,14 @@ RED.view = (function() {
function createNode(type, x, y, z) {
const wasDirty = RED.nodes.dirty()
var m = /^subflow:(.+)$/.exec(type);
var activeSubflow = (z || RED.workspaces.active()) ? RED.nodes.subflow(z || RED.workspaces.active()) : null;
var activeSubflow = z ? RED.nodes.subflow(z) : null;
if (activeSubflow && m) {
var subflowId = m[1];
let err
if (subflowId === activeSubflow.id) {
err = new Error(RED._("notification.errors.cannotAddSubflowToItself"))
} else if (RED.nodes.subflowContains(m[1], activeSubflow.id)) {
err = new Error(RED._("notification.errors.cannotAddCircularReference"))
throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddSubflowToItself") }))
}
if (err) {
err.code = 'NODE_RED'
throw err
if (RED.nodes.subflowContains(m[1], activeSubflow.id)) {
throw new Error(RED._("notification.error", { message: RED._("notification.errors.cannotAddCircularReference") }))
}
}
@@ -6265,6 +6252,10 @@ RED.view = (function() {
}
})
}
if (selection.links) {
selectedLinks.clear();
selection.links.forEach(selectedLinks.add);
}
}
}
updateSelection();

View File

@@ -491,11 +491,6 @@ RED.workspaces = (function() {
createWorkspaceTabs();
RED.events.on("sidebar:resize",workspace_tabs.resize);
RED.events.on("workspace:clear", () => {
// Reset the index used to generate new flow names
workspaceIndex = 0
})
RED.actions.add("core:show-next-tab",function() {
var oldActive = activeWorkspace;
workspace_tabs.nextTab();
@@ -662,9 +657,6 @@ RED.workspaces = (function() {
RED.events.on("flows:change", (ws) => {
$("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
})
RED.events.on("subflows:change", (ws) => {
$("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
})
hideWorkspace();
}

View File

@@ -16,20 +16,8 @@
RED.validators = {
number: function(blankAllowed,mopt){
return function(v, opt) {
if (blankAllowed && (v === '' || v === undefined)) {
return true
}
if (v !== '') {
if (/^NaN$|^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$|^[+-]?(0b|0B)[01]+$|^[+-]?(0o|0O)[0-7]+$|^[+-]?(0x|0X)[0-9a-fA-F]+$/.test(v)) {
return true
}
if (/^\${[^}]+}$/.test(v)) {
// Allow ${ENV_VAR} value
return true
}
}
if (!isNaN(v)) {
return true
if ((blankAllowed&&(v===''||v===undefined)) || (v!=='' && !isNaN(v))) {
return true;
}
if (opt && opt.label) {
return RED._("validator.errors.invalid-num-prop", {

View File

@@ -227,42 +227,34 @@
name: {value:""},
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) {
if (!v || v.length === 0) { return true }
const errors = []
for (var i=0;i<v.length;i++) {
if (/^\${[^}]+}$/.test(v[i].v)) {
// Allow ${ENV_VAR} value
continue
}
if (/msg|flow|global/.test(v[i].vt)) {
if (!RED.utils.validatePropertyExpression(v[i].v)) {
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
}
} else if (v[i].vt === "jsonata") {
try{ jsonata(v[i].v); }
catch(e){
errors.push(RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message }))
return RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message });
}
} else if (v[i].vt === "json") {
try{ JSON.parse(v[i].v); }
catch(e){
errors.push(RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message }))
return RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message });
}
} else if (v[i].vt === "num"){
if (!/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v[i].v)) {
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
}
}
}
if (errors.length > 0) {
return errors
}
return true;
}
},
repeat: {
value:"", validate: function(v, opt) {
if ((v === "") ||
(RED.validators.number()(v) &&
(RED.validators.number(v) &&
(v >= 0) && (v <= 2147483))) {
return true;
}
@@ -271,7 +263,7 @@
},
crontab: {value:""},
once: {value:false},
onceDelay: {value:0.1, validate: RED.validators.number(true)},
onceDelay: {value:0.1},
topic: {value:""},
payload: {value:"", validate: RED.validators.typedInput("payloadType", false) },
payloadType: {value:"date"},

View File

@@ -378,7 +378,7 @@
return { id: id, label: RED.nodes.workspace(id).label } //flow id + name
} else {
const instanceNode = RED.nodes.node(id)
const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8))?.name || instanceNode.type)
const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8)).name)
return { id: id, label: pathLabel }
}
})

View File

@@ -5,6 +5,7 @@ module.exports = function(RED) {
const fs = require("fs-extra");
const path = require("path");
var debuglength = RED.settings.debugMaxLength || 1000;
var statuslength = RED.settings.debugStatusLength || 32;
var useColors = RED.settings.debugUseColors || false;
util.inspect.styles.boolean = "red";
const { hasOwnProperty } = Object.prototype;
@@ -164,7 +165,7 @@ module.exports = function(RED) {
}
}
if (st.length > 32) { st = st.substr(0,32) + "..."; }
if (st.length > statuslength) { st = st.substr(0,statuslength) + "..."; }
var newStatus = {fill:fill, shape:shape, text:st};
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to

View File

@@ -194,46 +194,27 @@
nodeMap[node.links[i]].new = true;
}
}
let editHistories = []
let n;
for (let id in nodeMap) {
var n;
for (var id in nodeMap) {
if (nodeMap.hasOwnProperty(id)) {
n = RED.nodes.node(id);
if (n) {
editHistories.push({
t:'edit',
node: n,
changes: {
links: [...n.links]
},
changed: n.changed
})
if (nodeMap[id].old && !nodeMap[id].new) {
// Removed id
i = n.links.indexOf(node.id);
if (i > -1) {
n.links.splice(i,1);
n.changed = true
n.dirty = true
}
} else if (!nodeMap[id].old && nodeMap[id].new) {
// Added id
i = n.links.indexOf(id);
if (i === -1) {
n.links.push(node.id);
n.changed = true
n.dirty = true
}
}
}
}
}
if (editHistories.length > 0) {
return {
history: editHistories
}
}
}
function onAdd() {
@@ -273,14 +254,13 @@
onEditPrepare(this,"link out");
},
oneditsave: function() {
const result = onEditSave(this);
onEditSave(this);
// In case the name has changed, ensure any link call nodes on this
// tab are redrawn with the updated name
var localCallNodes = RED.nodes.filterNodes({z:RED.workspaces.active(), type:"link call"});
localCallNodes.forEach(function(node) {
node.dirty = true;
});
return result
},
onadd: onAdd,
oneditresize: resizeNodeList
@@ -349,7 +329,7 @@
onEditPrepare(this,"link in");
},
oneditsave: function() {
return onEditSave(this);
onEditSave(this);
},
oneditresize: resizeNodeList
});
@@ -393,7 +373,7 @@
},
oneditsave: function() {
return onEditSave(this);
onEditSave(this);
},
onadd: onAdd,
oneditresize: resizeNodeList

View File

@@ -374,7 +374,7 @@ module.exports = function(RED) {
iniOpt.breakOnSigint = true;
}
}
node.script = new vm.Script(functionText, createVMOpt(node, ""));
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
var finText = `(function () {
var node = {
@@ -438,9 +438,10 @@ module.exports = function(RED) {
//store the error in msg to be used in flows
msg.error = err;
var line = 0;
var errorMessage;
if (stack.length > 0) {
let line = 0;
let errorMessage;
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
line++;
}
@@ -454,13 +455,11 @@ module.exports = function(RED) {
errorMessage += " (line "+lineno+", col "+cha+")";
}
}
if (errorMessage) {
err.message = errorMessage
}
}
// Pass the whole error object so any additional properties
// (such as cause) are preserved
done(err);
if (!errorMessage) {
errorMessage = err.toString();
}
done(errorMessage);
}
else if (typeof err === "string") {
done(err);

View File

@@ -117,7 +117,7 @@ module.exports = function(RED) {
});
return
} else if (rule.tot === 'date') {
value = Date.now();
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
} else if (rule.tot === 'jsonata') {
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
if (err) {
@@ -233,7 +233,9 @@ module.exports = function(RED) {
// only replace if they match exactly
RED.util.setMessageProperty(msg,property,value);
} else {
current = current.replace(fromRE,value);
// if target is boolean then just replace it
if (rule.tot === "bool") { current = value; }
else { current = current.replace(fromRE,value); }
RED.util.setMessageProperty(msg,property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {

View File

@@ -20,7 +20,6 @@ module.exports = function(RED) {
var exec = require('child_process').exec;
var fs = require('fs');
var isUtf8 = require('is-utf8');
const isWindows = process.platform === 'win32'
function ExecNode(n) {
RED.nodes.createNode(this,n);
@@ -86,12 +85,9 @@ module.exports = function(RED) {
}
});
var cmd = arg.shift();
// Since 18.20.2/20.12.2, it is invalid to call spawn on Windows with a .bat/.cmd file
// without using shell: true.
const opts = isWindows ? { ...node.spawnOpt, shell: true } : node.spawnOpt
/* istanbul ignore else */
node.debug(cmd+" ["+arg+"]");
child = spawn(cmd,arg,opts);
child = spawn(cmd,arg,node.spawnOpt);
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
var unknownCommand = (child.pid === undefined);
if (node.timer !== 0) {

View File

@@ -411,23 +411,33 @@ module.exports = function(RED) {
if (msg._session && msg._session.type == "tcp") {
var client = connectionPool[msg._session.id];
if (client) {
if (Buffer.isBuffer(msg.payload)) {
client.write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
client.write(Buffer.from(msg.payload,'base64'));
} else {
client.write(Buffer.from(""+msg.payload));
if (msg?.reset === true) {
client.destroy();
}
else {
if (Buffer.isBuffer(msg.payload)) {
client.write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
client.write(Buffer.from(msg.payload,'base64'));
} else {
client.write(Buffer.from(""+msg.payload));
}
}
}
}
else {
for (var i in connectionPool) {
if (Buffer.isBuffer(msg.payload)) {
connectionPool[i].write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
} else {
connectionPool[i].write(Buffer.from(""+msg.payload));
if (msg?.reset === true) {
connectionPool[i].destroy();
}
else {
if (Buffer.isBuffer(msg.payload)) {
connectionPool[i].write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
} else {
connectionPool[i].write(Buffer.from(""+msg.payload));
}
}
}
}
@@ -547,13 +557,33 @@ module.exports = function(RED) {
this.on("input", function(msg, nodeSend, nodeDone) {
var i = 0;
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
if (msg.payload !== undefined && (!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString();
}
var host = node.server || msg.host;
var port = node.port || msg.port;
if (node.out === "sit" && msg?.reset) {
if (msg.reset === true) { // kill all connections
for (var cl in clients) {
if (clients[cl].hasOwnProperty("client")) {
clients[cl].client.destroy();
delete clients[cl];
}
}
}
if (typeof(msg.reset) === "string" && msg.reset.includes(":")) { // just kill connection host:port
if (clients.hasOwnProperty(msg.reset) && clients[msg.reset].hasOwnProperty("client")) {
clients[msg.reset].client.destroy();
delete clients[msg.reset];
}
}
const cc = Object.keys(clients).length;
node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
}
// Store client information independently
// the clients object will have:
// clients[id].client, clients[id].msg, clients[id].timeout
@@ -621,13 +651,16 @@ module.exports = function(RED) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(connOpts, function() {
//node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
// node.status({fill:"green",shape:"dot",text:"common.status.connected"});
node.status({fill:"green",shape:"dot",text:RED._("tcpin.status.connections",{count:Object.keys(clients).length})});
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = true;
clients[connection_id].connecting = false;
let event;
while (event = dequeue(clients[connection_id].msgQueue)) {
clients[connection_id].client.write(event.msg.payload);
if (event.msg.payload !== undefined) {
clients[connection_id].client.write(event.msg.payload);
}
event.nodeDone();
}
if (node.out === "time" && node.splitc < 0) {
@@ -823,7 +856,9 @@ module.exports = function(RED) {
else if (!clients[connection_id].connecting && clients[connection_id].connected) {
if (clients[connection_id] && clients[connection_id].client) {
let event = dequeue(clients[connection_id].msgQueue)
clients[connection_id].client.write(event.msg.payload);
if (event.msg.payload !== undefined ) {
clients[connection_id].client.write(event.msg.payload);
}
event.nodeDone();
}
}

View File

@@ -17,7 +17,20 @@
</select>
<input style="width:40px;" type="text" id="node-input-sep" pattern=".">
</div>
<div class="form-row">
<label><i class="fa fa-code"></i> <span data-i18n="csv.label.spec"></span></label>
<div style="display: inline-grid;width: 70%;">
<select style="width:100%" id="csv-option-spec">
<option value="rfc" data-i18n="csv.spec.rfc"></option>
<option value="" data-i18n="csv.spec.legacy"></option>
</select>
<div>
<div class="form-tips csv-lecacy-warning" data-i18n="node-red:csv.spec.legacy_warning"
style="width: calc(100% - 18px); margin-top: 4px; max-width: unset;">
</div>
</div>
</div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@@ -60,10 +73,10 @@
<div class="form-row" style="padding-left:20px;">
<label></label>
<label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
<select style="width:150px;" id="node-input-ret">
<select style="width:calc(70% - 108px);" id="node-input-ret">
<option value='\r\n' data-i18n="csv.newline.windows"></option>
<option value='\n' data-i18n="csv.newline.linux"></option>
<option value='\r' data-i18n="csv.newline.mac"></option>
<option value='\r\n' data-i18n="csv.newline.windows"></option>
</select>
</div>
</script>
@@ -75,6 +88,7 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
spec: {value:"rfc"},
sep: {
value:',', required:true,
label:RED._("node-red:csv.label.separator"),
@@ -83,7 +97,7 @@
hdrin: {value:""},
hdrout: {value:"none"},
multi: {value:"one",required:true},
ret: {value:'\\n'},
ret: {value:'\\r\\n'}, // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
temp: {value:""},
skip: {value:"0"},
strings: {value:true},
@@ -123,6 +137,27 @@
$("#node-input-sep").hide();
}
});
$("#csv-option-spec").on("change", function() {
if ($("#csv-option-spec").val() == "rfc") {
$(".form-tips.csv-lecacy-warning").hide();
} else {
$(".form-tips.csv-lecacy-warning").show();
}
});
// new nodes will have `spec` set to "rfc" (default), but existing nodes will either not have
// a spec value or it will be empty - we need to maintain the legacy behaviour for existing
// flows but default to rfc for new nodes
let spec = !this.spec ? "" : "rfc"
$("#csv-option-spec").val(spec).trigger("change")
},
oneditsave: function() {
const specFormVal = $("#csv-option-spec").val() || '' // empty === legacy
const spectNodeVal = this.spec || '' // empty === legacy, null/undefined means in-place node upgrade (keep as is)
if (specFormVal !== spectNodeVal) {
// only update the flow value if changed (avoid marking the node dirty unnecessarily)
this.spec = specFormVal
}
}
});
</script>

View File

@@ -15,322 +15,674 @@
**/
module.exports = function(RED) {
const csv = require('./lib/csv')
"use strict";
function CSVNode(n) {
RED.nodes.createNode(this,n);
this.template = (n.temp || "");
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.quo = '"';
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.winflag = (this.ret === "\r\n");
this.lineend = "\n";
this.multi = n.multi || "one";
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || "none";
this.goodtmpl = true;
this.skip = parseInt(n.skip || 0);
this.store = [];
this.parsestrings = n.strings;
this.include_empty_strings = n.include_empty_strings || false;
this.include_null_values = n.include_null_values || false;
if (this.parsestrings === undefined) { this.parsestrings = true; }
if (this.hdrout === false) { this.hdrout = "none"; }
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
RED.nodes.createNode(this,n)
const node = this
const RFC4180Mode = n.spec === 'rfc'
const legacyMode = !RFC4180Mode
// pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
}
var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === '';
node.hdrSent = false;
node.status({}) // clear status
this.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false;
if (legacyMode) {
this.template = (n.temp || "");
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.quo = '"';
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.winflag = (this.ret === "\r\n");
this.lineend = "\n";
this.multi = n.multi || "one";
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || "none";
this.goodtmpl = true;
this.skip = parseInt(n.skip || 0);
this.store = [];
this.parsestrings = n.strings;
this.include_empty_strings = n.include_empty_strings || false;
this.include_null_values = n.include_null_values || false;
if (this.parsestrings === undefined) { this.parsestrings = true; }
if (this.hdrout === false) { this.hdrout = "none"; }
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
// var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
}
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload == "object") { // convert object to CSV string
try {
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = clean(node.template);
}
const ou = [];
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
if (node.hdrout !== "none" && node.hdrSent === false) {
if ((template.length === 1) && (template[0] === '')) {
if (msg.hasOwnProperty("columns")) {
template = clean(msg.columns || "",",");
}
else {
template = Object.keys(msg.payload[0]);
}
var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === '';
node.hdrSent = false;
this.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false;
}
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload == "object") { // convert object to CSV string
try {
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = clean(node.template);
}
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
if (node.hdrout === "once") { node.hdrSent = true; }
}
for (var s = 0; s < msg.payload.length; s++) {
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
for (var t = 0; t < msg.payload[s].length; t++) {
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
}
ou.push(msg.payload[s].join(node.sep));
}
else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || "",",");
}
const ou = [];
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
if (node.hdrout !== "none" && node.hdrSent === false) {
if ((template.length === 1) && (template[0] === '')) {
/* istanbul ignore else */
if (tmpwarn === true) { // just warn about missing template once
node.warn(RED._("csv.errors.obj_csv"));
tmpwarn = false;
if (msg.hasOwnProperty("columns")) {
template = clean(msg.columns || "",",");
}
const row = [];
for (var p in msg.payload[0]) {
else {
template = Object.keys(msg.payload[0]);
}
}
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
if (node.hdrout === "once") { node.hdrSent = true; }
}
for (var s = 0; s < msg.payload.length; s++) {
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
for (var t = 0; t < msg.payload[s].length; t++) {
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
}
}
ou.push(msg.payload[s].join(node.sep));
}
else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || "",",");
}
if ((template.length === 1) && (template[0] === '')) {
/* istanbul ignore else */
if (msg.payload[s].hasOwnProperty(p)) {
if (tmpwarn === true) { // just warn about missing template once
node.warn(RED._("csv.errors.obj_csv"));
tmpwarn = false;
}
const row = [];
for (var p in msg.payload[0]) {
/* istanbul ignore else */
if (typeof msg.payload[s][p] !== "object") {
// Fix to honour include null values flag
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
var q = "";
if (msg.payload[s][p] !== undefined) {
q += msg.payload[s][p];
if (msg.payload[s].hasOwnProperty(p)) {
/* istanbul ignore else */
if (typeof msg.payload[s][p] !== "object") {
// Fix to honour include null values flag
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
var q = "";
if (msg.payload[s][p] !== undefined) {
q += msg.payload[s][p];
}
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
row.push(node.quo + q + node.quo);
}
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + q + node.quo);
}
else { row.push(q); } // otherwise just add
}
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
row.push(node.quo + q + node.quo);
}
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + q + node.quo);
}
else { row.push(q); } // otherwise just add
}
}
ou.push(row.join(node.sep)); // add separator
}
ou.push(row.join(node.sep)); // add separator
else {
const row = [];
for (var t=0; t < template.length; t++) {
if (template[t] === '') {
row.push('');
}
else {
var tt = template[t];
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
else { tt = '"'+tt+'"'; }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
/* istanbul ignore else */
if (p === undefined) { p = ""; }
// fix to honour include null values flag
//if (p === null && node.include_null_values !== true) { p = "";}
p = RED.util.ensureString(p);
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
p = p.replace(/"/g, '""');
row.push(node.quo + p + node.quo);
}
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + p + node.quo);
}
else { row.push(p); } // otherwise just add
}
}
ou.push(row.join(node.sep)); // add separator
}
}
}
// join lines, don't forget to add the last new line
msg.payload = ou.join(node.ret) + node.ret;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
if (msg.payload !== '') {
send(msg);
}
done();
}
catch(e) { done(e); }
}
else if (typeof msg.payload == "string") { // convert CSV string to object
try {
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
var j = 0; // pointer into array of template items
var k = [""]; // array of data for each of the template items
var o = {}; // output object to build up
var a = []; // output array is needed for multiline option
var first = true; // is this the first line
var last = false;
var line = msg.payload;
var linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
}
// For now we are just going to assume that any \r or \n means an end of line...
// got to be a weird csv that has singleton \r \n in it for another reason...
// Now process the whole file/line
var nocr = (line.match(/[\r\n]/g)||[]).length;
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
continue;
}
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
template = clean(tmp,node.sep);
first = false;
}
else { tmp += line[i]; }
}
else {
const row = [];
for (var t=0; t < template.length; t++) {
if (template[t] === '') {
row.push('');
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
f = !f;
if (line[i-1] === node.quo) {
if (f === false) { k[j] += '\"'; }
} // if it's a quotequote then it's actually a quote
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
}
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
else {
var tt = template[t];
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
else { tt = '"'+tt+'"'; }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
/* istanbul ignore else */
if (p === undefined) { p = ""; }
// fix to honour include null values flag
//if (p === null && node.include_null_values !== true) { p = "";}
p = RED.util.ensureString(p);
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
p = p.replace(/"/g, '""');
row.push(node.quo + p + node.quo);
}
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
row.push(node.quo + p + node.quo);
}
else { row.push(p); } // otherwise just add
j += 1;
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
k[j] = line.length - 1 === i ? null : "";
}
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
//console.log(j,k,o,k[j]);
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
if (line[i-1] === node.sep) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
j = 0;
k = [""];
o = {};
f = true; // reset in/out flag ready for next line.
}
else { // just add to the part of the message
k[j] += line[i];
}
ou.push(row.join(node.sep)); // add separator
}
}
}
// join lines, don't forget to add the last new line
msg.payload = ou.join(node.ret) + node.ret;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
if (msg.payload !== '') { send(msg); }
done();
}
catch(e) { done(e); }
}
else if (typeof msg.payload == "string") { // convert CSV string to object
try {
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
var j = 0; // pointer into array of template items
var k = [""]; // array of data for each of the template items
var o = {}; // output object to build up
var a = []; // output array is needed for multiline option
var first = true; // is this the first line
var last = false;
var line = msg.payload;
var linecount = 0;
var tmp = "";
var has_parts = msg.hasOwnProperty("parts");
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
}
// Finished so finalize and send anything left
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
// For now we are just going to assume that any \r or \n means an end of line...
// got to be a weird csv that has singleton \r \n in it for another reason...
// Now process the whole file/line
var nocr = (line.match(/[\r\n]/g)||[]).length;
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
continue;
if ( template[j] && (template[j] !== "") ) {
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
template = clean(tmp,node.sep);
first = false;
}
else { tmp += line[i]; }
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
else {
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
f = !f;
if (line[i-1] === node.quo) {
if (f === false) { k[j] += '\"'; }
} // if it's a quotequote then it's actually a quote
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
}
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
j += 1;
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
k[j] = line.length - 1 === i ? null : "";
}
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
//console.log(j,k,o,k[j]);
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
if (line[i-1] === node.sep) k[j] = null;
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
j = 0;
k = [""];
o = {};
f = true; // reset in/out flag ready for next line.
}
else { // just add to the part of the message
k[j] += line[i];
}
}
}
// Finished so finalize and send anything left
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
if ( template[j] && (template[j] !== "") ) {
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
a.push(o); // add to the array
}
if (node.multi !== "one") {
msg.payload = a;
if (has_parts && nocr <= 1) {
if (JSON.stringify(o) !== "{}") {
node.store.push(o);
if (node.multi !== "one") {
msg.payload = a;
if (has_parts && nocr <= 1) {
if (JSON.stringify(o) !== "{}") {
node.store.push(o);
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store;
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
delete msg.parts;
send(msg);
node.store = [];
}
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store;
else {
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
delete msg.parts;
send(msg);
node.store = [];
send(msg); // finally send the array
}
}
else {
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
send(msg); // finally send the array
}
}
else {
var len = a.length;
for (var i = 0; i < len; i++) {
var newMessage = RED.util.cloneMessage(msg);
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
newMessage.payload = a[i];
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: i,
count: len
};
var len = a.length;
for (var i = 0; i < len; i++) {
var newMessage = RED.util.cloneMessage(msg);
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
newMessage.payload = a[i];
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: i,
count: len
};
}
else {
newMessage.parts.index -= node.skip;
newMessage.parts.count -= node.skip;
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1;
newMessage.parts.count -= 1;
}
}
if (last) { newMessage.complete = true; }
send(newMessage);
}
else {
newMessage.parts.index -= node.skip;
newMessage.parts.count -= node.skip;
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1;
newMessage.parts.count -= 1;
if (has_parts && last && len === 0) {
send({complete:true});
}
}
node.linecount = 0;
done();
}
catch(e) { done(e); }
}
else { node.warn(RED._("csv.errors.csv_js")); done(); }
}
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
done();
}
});
}
if(RFC4180Mode) {
node.template = (n.temp || "")
node.sep = (n.sep || ',').replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
node.quo = '"'
// default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
node.ret = (n.ret || "\r\n").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
node.multi = n.multi || "one"
node.hdrin = n.hdrin || false
node.hdrout = n.hdrout || "none"
node.goodtmpl = true
node.skip = parseInt(n.skip || 0)
node.store = []
node.parsestrings = n.strings
node.include_empty_strings = n.include_empty_strings || false
node.include_null_values = n.include_null_values || false
if (node.parsestrings === undefined) { node.parsestrings = true }
if (node.hdrout === false) { node.hdrout = "none" }
if (node.hdrout === true) { node.hdrout = "all" }
const dontSendHeaders = node.hdrout === "none"
const sendHeadersOnce = node.hdrout === "once"
const sendHeadersAlways = node.hdrout === "all"
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
const quoteables = [node.sep, node.quo, "\n", "\r"]
const templateQuoteables = [',', '"', "\n", "\r"]
let badTemplateWarnOnce = true
const columnStringToTemplateArray = function (col, sep) {
// NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
return parsed.headers.length ? parsed.headers : null
}
const templateArrayToColumnString = function (template, keepEmptyColumns) {
// NOTE: enforce strict column template parsing in RFC4180 mode
const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true })
return keepEmptyColumns
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
: parsed.header // exclues empty columns
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV
// CSV->JSON: empty columns are excluded
// JSON->CSV: empty columns are kept in some cases
}
function addQuotes(cell, options) {
options = options || {}
return csv.quoteCell(cell, {
quote: options.quote || node.quo || '"',
separator: options.separator || node.sep || ',',
quoteables: options.quoteables || quoteables
})
}
const hasTemplate = (t) => t?.length > 0 && !(t.length === 1 && t[0] === '')
let template
try {
template = columnStringToTemplateArray(node.template, ',') || ['']
} catch (e) {
node.warn(RED._("csv.errors.bad_template")) // is warning really necessary now we have status?
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
return // dont hook up the node
}
const noTemplate = hasTemplate(template) === false
node.hdrSent = false
node.on("input", function (msg, send, done) {
node.status({}) // clear status
if (msg.hasOwnProperty("reset")) {
node.hdrSent = false
}
if (msg.hasOwnProperty("payload")) {
let inputData = msg.payload
if (typeof inputData == "object") { // convert object to CSV string
try {
// first determine the payload kind. Array or objects? Array of primitives? Array of arrays? Just an object?
// then, if necessary, convert to an array of objects/arrays
let isObject = !Array.isArray(inputData) && typeof inputData === 'object'
let isArrayOfObjects = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] === 'object'
let isArrayOfArrays = Array.isArray(inputData) && inputData.length > 0 && Array.isArray(inputData[0])
let isArrayOfPrimitives = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] !== 'object'
if (isObject) {
inputData = [inputData]
isArrayOfObjects = true
isObject = false
} else if (isArrayOfPrimitives) {
inputData = [inputData]
isArrayOfArrays = true
isArrayOfPrimitives = false
}
const stringBuilder = []
if (!(noTemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = columnStringToTemplateArray(node.template) || ['']
}
// build header line
if (sendHeaders && node.hdrSent === false) {
if (hasTemplate(template) === false) {
if (msg.hasOwnProperty("columns")) {
template = columnStringToTemplateArray(msg.columns || "", ",") || ['']
}
else {
template = Object.keys(inputData[0]) || ['']
}
}
if (last) { newMessage.complete = true; }
send(newMessage);
stringBuilder.push(templateArrayToColumnString(template, true))
if (sendHeadersOnce) { node.hdrSent = true }
}
if (has_parts && last && len === 0) {
send({complete:true});
// build csv lines
for (let s = 0; s < inputData.length; s++) {
let row = inputData[s]
if (isArrayOfArrays) {
/*** row is an array of arrays ***/
const _hasTemplate = hasTemplate(template)
const len = _hasTemplate ? template.length : row.length
const result = []
for (let t = 0; t < len; t++) {
let cell = row[t]
if (cell === undefined) { cell = "" }
if(_hasTemplate) {
const header = template[t]
if (header) {
result[t] = addQuotes(RED.util.ensureString(cell))
}
} else {
result[t] = addQuotes(RED.util.ensureString(cell))
}
}
stringBuilder.push(result.join(node.sep))
} else {
/*** row is an object ***/
if (hasTemplate(template) === false && (msg.hasOwnProperty("columns"))) {
template = columnStringToTemplateArray(msg.columns || "", ",")
}
if (hasTemplate(template) === false) {
/*** row is an object but we still don't have a template ***/
if (badTemplateWarnOnce === true) {
node.warn(RED._("csv.errors.obj_csv"))
badTemplateWarnOnce = false
}
const rowData = []
for (let header in inputData[0]) {
if (row.hasOwnProperty(header)) {
const cell = row[header]
if (typeof cell !== "object") {
let cellValue = ""
if (cell !== undefined) {
cellValue += cell
}
rowData.push(addQuotes(cellValue))
}
}
}
stringBuilder.push(rowData.join(node.sep))
} else {
/*** row is an object and we have a template ***/
const rowData = []
for (let t = 0; t < template.length; t++) {
if (!template[t]) {
rowData.push('')
}
else {
let cellValue = inputData[s][template[t]]
if (cellValue === undefined) { cellValue = "" }
cellValue = RED.util.ensureString(cellValue)
rowData.push(addQuotes(cellValue))
}
}
stringBuilder.push(rowData.join(node.sep)); // add separator
}
}
}
// join lines, don't forget to add the last new line
msg.payload = stringBuilder.join(node.ret) + node.ret
msg.columns = templateArrayToColumnString(template)
if (msg.payload !== '') { send(msg) }
done()
}
catch (e) {
done(e)
}
node.linecount = 0;
done();
}
catch(e) { done(e); }
else if (typeof inputData == "string") { // convert CSV string to object
try {
let firstLine = true; // is this the first line
let last = false
let linecount = 0
const has_parts = msg.hasOwnProperty("parts")
// determine if this is a multi part message and if so what part we are processing
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index
if (msg.parts.index > node.skip) { firstLine = false }
if (msg.parts.hasOwnProperty("count") && (msg.parts.index + 1 >= msg.parts.count)) { last = true }
}
// If skip is set, compute the cursor position to start parsing from
let _cursor = 0
if (node.skip > 0 && linecount < node.skip) {
for (; _cursor < inputData.length; _cursor++) {
if (firstLine && (linecount < node.skip)) {
if (inputData[_cursor] === "\r" || inputData[_cursor] === "\n") {
linecount += 1
}
continue
}
break
}
if (_cursor >= inputData.length) {
return // skip this line
}
}
// count the number of line breaks in the string
const noofCR = ((_cursor ? inputData.slice(_cursor) : inputData).match(/[\r\n]/g) || []).length
// if we have `parts` and we are outputting multiple objects and we have more than one line
// then we need to set firstLine to true so that we process the header line
if (has_parts && node.multi === "mult" && noofCR > 1) {
firstLine = true
}
// if we are processing the first line and the node has been set to extract the header line
// update the template with the header line
if (firstLine && node.hdrin === true) {
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
const csvOptionsForHeaderRow = {
cursor: _cursor,
separator: node.sep,
quote: node.quo,
dataHasHeaderRow: true,
headersOnly: true,
outputStyle: 'array',
strict: true // enforce strict parsing of the header row
}
try {
const csvHeader = csv.parse(inputData, csvOptionsForHeaderRow)
template = csvHeader.headers
_cursor = csvHeader.cursor
} catch (e) {
// node.warn(RED._("csv.errors.bad_template")) // add warning?
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
throw e
}
}
// now we process the data lines
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
const csvOptions = {
cursor: _cursor,
separator: node.sep,
quote: node.quo,
dataHasHeaderRow: false,
headers: hasTemplate(template) ? template : null,
outputStyle: 'object',
includeNullValues: node.include_null_values,
includeEmptyStrings: node.include_empty_strings,
parseNumeric: node.parsestrings,
strict: false // relax the strictness of the parser for data rows
}
const csvParseResult = csv.parse(inputData, csvOptions)
const data = csvParseResult.data
// output results
if (node.multi !== "one") {
if (has_parts && noofCR <= 1) {
if (data.length > 0) {
node.store.push(...data)
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
delete msg.parts
send(msg)
node.store = []
}
}
else {
msg.columns = csvParseResult.header
// msg._mode = 'RFC4180 mode'
msg.payload = data
send(msg); // finally send the array
}
}
else {
const len = data.length
for (let row = 0; row < len; row++) {
const newMessage = RED.util.cloneMessage(msg)
newMessage.columns = csvParseResult.header
newMessage.payload = data[row]
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: row,
count: len
}
}
else {
newMessage.parts.index -= node.skip
newMessage.parts.count -= node.skip
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1
newMessage.parts.count -= 1
}
}
if (last) { newMessage.complete = true }
// newMessage._mode = 'RFC4180 mode'
send(newMessage)
}
if (has_parts && last && len === 0) {
// send({complete:true, _mode: 'RFC4180 mode'})
send({ complete: true })
}
}
node.linecount = 0
done()
}
catch (e) {
done(e)
}
}
else {
// RFC-vs-legacy mode difference: In RFC mode, we throw catchable errors and provide a status message
const err = new Error(RED._("csv.errors.csv_js"))
node.status({ fill: "red", shape: "dot", text: err.message })
done(err)
}
}
else { node.warn(RED._("csv.errors.csv_js")); done(); }
}
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
else {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
done()
}
done();
}
});
})
}
}
RED.nodes.registerType("csv",CSVNode);
RED.nodes.registerType("csv",CSVNode)
}

View File

@@ -0,0 +1,324 @@
/**
* @typedef {Object} CSVParseOptions
* @property {number} [cursor=0] - an index into the CSV to start parsing from
* @property {string} [separator=','] - the separator character
* @property {string} [quote='"'] - the quote character
* @property {boolean} [headersOnly=false] - only parse the headers and return them
* @property {string[]} [headers=[]] - an array of headers to use instead of the first row of the CSV data
* @property {boolean} [dataHasHeaderRow=true] - whether the CSV data to parse has a header row
* @property {boolean} [outputHeader=true] - whether the output data should include a header row (only applies to array output)
* @property {boolean} [parseNumeric=false] - parse numeric values into numbers
* @property {boolean} [includeNullValues=false] - include null values in the output
* @property {boolean} [includeEmptyStrings=true] - include empty strings in the output
* @property {string} [outputStyle='object'] - output an array of arrays or an array of objects
* @property {boolean} [strict=false] - throw an error if the CSV is malformed
*/
/**
* Parses a CSV string into an array of arrays or an array of objects.
*
* NOTES:
* * Deviations from the RFC4180 spec (for the sake of user fiendliness, system implementations and flexibility), this parser will:
* * accept any separator character, not just `,`
* * accept any quote character, not just `"`
* * parse `\r`, `\n` or `\r\n` as line endings (RRFC4180 2.1 states lines are separated by CRLF)
* * Only single character `quote` is supported
* * `quote` is `"` by default
* * Any cell that contains a `quote` or `separator` will be quoted
* * Any `quote` characters inside a cell will be escaped as per RFC 4180 2.6
* * Only single character `separator` is supported
* * Only `array` and `object` output styles are supported
* * `array` output style is an array of arrays [[],[],[]]
* * `object` output style is an array of objects [{},{},{}]
* * Only `headers` or `dataHasHeaderRow` are supported, not both
* @param {string} csvIn - the CSV string to parse
* @param {CSVParseOptions} parseOptions - options
* @throws {Error}
*/
function parse(csvIn, parseOptions) {
/* Normalise options */
parseOptions = parseOptions || {};
const separator = parseOptions.separator ?? ',';
const quote = parseOptions.quote ?? '"';
const headersOnly = parseOptions.headersOnly ?? false;
const headers = Array.isArray(parseOptions.headers) ? parseOptions.headers : []
const dataHasHeaderRow = parseOptions.dataHasHeaderRow ?? true;
const outputHeader = parseOptions.outputHeader ?? true;
const parseNumeric = parseOptions.parseNumeric ?? false;
const includeNullValues = parseOptions.includeNullValues ?? false;
const includeEmptyStrings = parseOptions.includeEmptyStrings ?? true;
const outputStyle = ['array', 'object'].includes(parseOptions.outputStyle) ? parseOptions.outputStyle : 'object'; // 'array [[],[],[]]' or 'object [{},{},{}]
const strict = parseOptions.strict ?? false
/* Local variables */
const cursorMax = csvIn.length;
const ouputArrays = outputStyle === 'array';
const headersSupplied = headers.length > 0
// The original regex was an "is-a-number" positive logic test. /^ *[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+ *$/i;
// Below, is less strict and inverted logic but coupled with +cast it is 13%+ faster than original regex+parsefloat
// and has the benefit of understanding hexadecimals, binary and octal numbers.
const skipNumberConversion = /^ *(\+|-0\d|0\d)/
const cellBuilder = []
let rowBuilder = []
let cursor = typeof parseOptions.cursor === 'number' ? parseOptions.cursor : 0;
let newCell = true, inQuote = false, closed = false, output = [];
/* inline helper functions */
const finaliseCell = () => {
let cell = cellBuilder.join('')
cellBuilder.length = 0
// push the cell:
// NOTE: if cell is empty but newCell==true, then this cell had zero chars - push `null`
// otherwise push empty string
return rowBuilder.push(cell || (newCell ? null : ''))
}
const finaliseRow = () => {
if (cellBuilder.length) {
finaliseCell()
}
if (rowBuilder.length) {
output.push(rowBuilder)
rowBuilder = []
}
}
/* Main parsing loop */
while (cursor < cursorMax) {
const char = csvIn[cursor]
if (inQuote) {
if (char === quote && csvIn[cursor + 1] === quote) {
cellBuilder.push(quote)
cursor += 2;
newCell = false;
closed = false;
} else if (char === quote) {
inQuote = false;
cursor += 1;
newCell = false;
closed = true;
} else {
cellBuilder.push(char)
newCell = false;
closed = false;
cursor++;
}
} else {
if (char === separator) {
finaliseCell()
cursor += 1;
newCell = true;
closed = false;
} else if (char === quote) {
if (newCell) {
inQuote = true;
cursor += 1;
newCell = false;
closed = false;
}
else if (strict) {
throw new UnquotedQuoteError(cursor)
} else {
// not strict, keep 1 quote if the next char is not a cell/record separator
cursor++
if (csvIn[cursor] && csvIn[cursor] !== '\n' && csvIn[cursor] !== '\r' && csvIn[cursor] !== separator) {
cellBuilder.push(char)
if (csvIn[cursor] === quote) {
cursor++ // skip the next quote
}
}
}
} else {
if (char === '\n' || char === '\r') {
finaliseRow()
if (csvIn[cursor + 1] === '\n') {
cursor += 2;
} else {
cursor++
}
newCell = true;
closed = false;
if (headersOnly) {
break
}
} else {
if (closed) {
if (strict) {
throw new DataAfterCloseError(cursor)
} else {
cursor--; // move back to grab the previously discarded char
closed = false
}
} else {
cellBuilder.push(char)
newCell = false;
cursor++;
}
}
}
}
}
if (strict && inQuote) {
throw new ParseError(`Missing quote, unclosed cell`, cursor)
}
// finalise the last cell/row
finaliseRow()
let firstRowIsHeader = false
// if no headers supplied, generate them
if (output.length >= 1) {
if (headersSupplied) {
// headers already supplied
} else if (dataHasHeaderRow) {
// take the first row as the headers
headers.push(...output[0])
firstRowIsHeader = true
} else {
// generate headers col1, col2, col3, etc
for (let i = 0; i < output[0].length; i++) {
headers.push("col" + (i + 1))
}
}
}
const finalResult = {
/** @type {String[]} headers as an array of string */
headers: headers,
/** @type {String} headers as a comma-separated string */
header: null,
/** @type {Any[]} Result Data (may include header row: check `firstRowIsHeader` flag) */
data: [],
/** @type {Boolean|undefined} flag to indicate if the first row is a header row (only applies when `outputStyle` is 'array') */
firstRowIsHeader: undefined,
/** @type {'array'|'object'} flag to indicate the output style */
outputStyle: outputStyle,
/** @type {Number} The current cursor position */
cursor: cursor,
}
const quotedHeaders = []
for (let i = 0; i < headers.length; i++) {
if (!headers[i]) {
continue
}
quotedHeaders.push(quoteCell(headers[i], { quote, separator: ',' }))
}
finalResult.header = quotedHeaders.join(',') // always quote headers and join with comma
// output is an array of arrays [[],[],[]]
if (ouputArrays || headersOnly) {
if (!firstRowIsHeader && !headersOnly && outputHeader && headers.length > 0) {
if (output.length > 0) {
output.unshift(headers)
} else {
output = [headers]
}
firstRowIsHeader = true
}
if (headersOnly) {
delete finalResult.firstRowIsHeader
return finalResult
}
finalResult.firstRowIsHeader = firstRowIsHeader
finalResult.data = (firstRowIsHeader && !outputHeader) ? output.slice(1) : output
return finalResult
}
// output is an array of objects [{},{},{}]
const outputObjects = []
let i = firstRowIsHeader ? 1 : 0
for (; i < output.length; i++) {
const rowObject = {}
let isEmpty = true
for (let j = 0; j < headers.length; j++) {
if (!headers[j]) {
continue
}
let v = output[i][j] === undefined ? null : output[i][j]
if (v === null && !includeNullValues) {
continue
} else if (v === "" && !includeEmptyStrings) {
continue
} else if (parseNumeric === true && v && !skipNumberConversion.test(v)) {
const vTemp = +v
const isNumber = !isNaN(vTemp)
if(isNumber) {
v = vTemp
}
}
rowObject[headers[j]] = v
isEmpty = false
}
// determine if this row is empty
if (!isEmpty) {
outputObjects.push(rowObject)
}
}
finalResult.data = outputObjects
delete finalResult.firstRowIsHeader
return finalResult
}
/**
* Quotes a cell in a CSV string if necessary. Addiionally, any double quotes inside the cell will be escaped as per RFC 4180 2.6 (https://datatracker.ietf.org/doc/html/rfc4180#section-2).
* @param {string} cell - the string to quote
* @param {*} options - options
* @param {string} [options.quote='"'] - the quote character
* @param {string} [options.separator=','] - the separator character
* @param {string[]} [options.quoteables] - an array of characters that, when encountered, will trigger the application of outer quotes
* @returns
*/
function quoteCell(cell, { quote = '"', separator = ",", quoteables } = {
quote: '"',
separator: ",",
quoteables: [quote, separator, '\r', '\n']
}) {
quoteables = quoteables || [quote, separator, '\r', '\n'];
let doubleUp = false;
if (cell.indexOf(quote) !== -1) { // add double quotes if any quotes
doubleUp = true;
}
const quoteChar = quoteables.some(q => cell.includes(q)) ? quote : '';
return quoteChar + (doubleUp ? cell.replace(/"/g, '""') : cell) + quoteChar;
}
// #region Custom Error Classes
class ParseError extends Error {
/**
* @param {string} message - the error message
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(message, cursor) {
super(message)
this.name = 'ParseError'
this.cursor = cursor
}
}
class UnquotedQuoteError extends ParseError {
/**
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(cursor) {
super('Quote found in the middle of an unquoted field', cursor)
this.name = 'UnquotedQuoteError'
}
}
class DataAfterCloseError extends ParseError {
/**
* @param {number} cursor - the cursor index where the error occurred
*/
constructor(cursor) {
super('Data found after closing quote', cursor)
this.name = 'DataAfterCloseError'
}
}
// #endregion
exports.parse = parse
exports.quoteCell = quoteCell
exports.ParseError = ParseError
exports.UnquotedQuoteError = UnquotedQuoteError
exports.DataAfterCloseError = DataAfterCloseError

View File

@@ -15,7 +15,11 @@
-->
<script type="text/html" data-template-name="split">
<div class="form-row"><span data-i18n="[html]split.intro"></span></div>
<!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> -->
<div class="form-row">
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
<div class="form-row">
<label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label>
@@ -39,10 +43,9 @@
<label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label>
<input type="text" id="node-input-addname" style="width:70%">
</div>
<hr/>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</script>
@@ -57,7 +60,8 @@
arraySplt: {value:1},
arraySpltType: {value:"len"},
stream: {value:false},
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })}
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
property: {value:"payload",required:true}
},
inputs:1,
outputs:1,
@@ -69,6 +73,10 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-splt").typedInput({
default: 'str',
typeField: $("#node-input-spltType"),

View File

@@ -19,13 +19,13 @@ module.exports = function(RED) {
function sendArray(node,msg,array,send) {
for (var i = 0; i < array.length-1; i++) {
msg.payload = array[i];
RED.util.setMessageProperty(msg,node.property,array[i]);
msg.parts.index = node.c++;
if (node.stream !== true) { msg.parts.count = array.length; }
send(RED.util.cloneMessage(msg));
}
if (node.stream !== true) {
msg.payload = array[i];
RED.util.setMessageProperty(msg,node.property,array[i]);
msg.parts.index = node.c++;
msg.parts.count = array.length;
send(RED.util.cloneMessage(msg));
@@ -40,10 +40,12 @@ module.exports = function(RED) {
node.stream = n.stream;
node.spltType = n.spltType || "str";
node.addname = n.addname || "";
node.property = n.property||"payload";
try {
if (node.spltType === "str") {
this.splt = (n.splt || "\\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t").replace(/\\e/g,"\e").replace(/\\f/g,"\f").replace(/\\0/g,"\0");
} else if (node.spltType === "bin") {
}
else if (node.spltType === "bin") {
var spltArray = JSON.parse(n.splt);
if (Array.isArray(spltArray)) {
this.splt = Buffer.from(spltArray);
@@ -51,7 +53,8 @@ module.exports = function(RED) {
throw new Error("not an array");
}
this.spltBuffer = spltArray;
} else if (node.spltType === "len") {
}
else if (node.spltType === "len") {
this.splt = parseInt(n.splt);
if (isNaN(this.splt) || this.splt < 1) {
throw new Error("invalid split length: "+n.splt);
@@ -69,18 +72,22 @@ module.exports = function(RED) {
node.buffer = Buffer.from([]);
node.pendingDones = [];
this.on("input", function(msg, send, done) {
if (msg.hasOwnProperty("payload")) {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack
else { msg.parts = {}; }
msg.parts.id = RED.util.generateId(); // generate a random id
if (node.property !== "payload") {
msg.parts.property = node.property;
}
delete msg._msgid;
if (typeof msg.payload === "string") { // Split String into array
msg.payload = (node.remainder || "") + msg.payload;
if (typeof value === "string") { // Split String into array
value = (node.remainder || "") + value;
msg.parts.type = "string";
if (node.spltType === "len") {
msg.parts.ch = "";
msg.parts.len = node.splt;
var count = msg.payload.length/node.splt;
var count = value.length/node.splt;
if (Math.floor(count) !== count) {
count = Math.ceil(count);
}
@@ -89,9 +96,9 @@ module.exports = function(RED) {
node.c = 0;
}
var pos = 0;
var data = msg.payload;
var data = value;
for (var i=0; i<count-1; i++) {
msg.payload = data.substring(pos,pos+node.splt);
RED.util.setMessageProperty(msg,node.property,data.substring(pos,pos+node.splt));
msg.parts.index = node.c++;
pos += node.splt;
send(RED.util.cloneMessage(msg));
@@ -102,7 +109,7 @@ module.exports = function(RED) {
}
node.remainder = data.substring(pos);
if ((node.stream !== true) || (node.remainder.length === node.splt)) {
msg.payload = node.remainder;
RED.util.setMessageProperty(msg,node.property,node.remainder);
msg.parts.index = node.c++;
send(RED.util.cloneMessage(msg));
node.pendingDones.forEach(d => d());
@@ -119,47 +126,48 @@ module.exports = function(RED) {
if (!node.spltBufferString) {
node.spltBufferString = node.splt.toString();
}
a = msg.payload.split(node.spltBufferString);
a = value.split(node.spltBufferString);
msg.parts.ch = node.spltBuffer; // pass the split char to other end for rejoin
} else if (node.spltType === "str") {
a = msg.payload.split(node.splt);
a = value.split(node.splt);
msg.parts.ch = node.splt; // pass the split char to other end for rejoin
}
sendArray(node,msg,a,send);
done();
}
}
else if (Array.isArray(msg.payload)) { // then split array into messages
else if (Array.isArray(value)) { // then split array into messages
msg.parts.type = "array";
var count = msg.payload.length/node.arraySplt;
var count = value.length/node.arraySplt;
if (Math.floor(count) !== count) {
count = Math.ceil(count);
}
msg.parts.count = count;
var pos = 0;
var data = msg.payload;
var data = value;
msg.parts.len = node.arraySplt;
for (var i=0; i<count; i++) {
msg.payload = data.slice(pos,pos+node.arraySplt);
var m = data.slice(pos,pos+node.arraySplt);
if (node.arraySplt === 1) {
msg.payload = msg.payload[0];
m = m[0];
}
RED.util.setMessageProperty(msg,node.property,m);
msg.parts.index = i;
pos += node.arraySplt;
send(RED.util.cloneMessage(msg));
}
done();
}
else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) {
else if ((typeof value === "object") && !Buffer.isBuffer(value)) {
var j = 0;
var l = Object.keys(msg.payload).length;
var pay = msg.payload;
var l = Object.keys(value).length;
var pay = value;
msg.parts.type = "object";
for (var p in pay) {
if (pay.hasOwnProperty(p)) {
msg.payload = pay[p];
RED.util.setMessageProperty(msg,node.property,pay[p]);
if (node.addname !== "") {
msg[node.addname] = p;
RED.util.setMessageProperty(msg,node.addname,p);
}
msg.parts.key = p;
msg.parts.index = j;
@@ -170,9 +178,9 @@ module.exports = function(RED) {
}
done();
}
else if (Buffer.isBuffer(msg.payload)) {
var len = node.buffer.length + msg.payload.length;
var buff = Buffer.concat([node.buffer, msg.payload], len);
else if (Buffer.isBuffer(value)) {
var len = node.buffer.length + value.length;
var buff = Buffer.concat([node.buffer, value], len);
msg.parts.type = "buffer";
if (node.spltType === "len") {
var count = buff.length/node.splt;
@@ -186,7 +194,7 @@ module.exports = function(RED) {
var pos = 0;
msg.parts.len = node.splt;
for (var i=0; i<count-1; i++) {
msg.payload = buff.slice(pos,pos+node.splt);
RED.util.setMessageProperty(msg,node.property,buff.slice(pos,pos+node.splt));
msg.parts.index = node.c++;
pos += node.splt;
send(RED.util.cloneMessage(msg));
@@ -197,7 +205,7 @@ module.exports = function(RED) {
}
node.buffer = buff.slice(pos);
if ((node.stream !== true) || (node.buffer.length === node.splt)) {
msg.payload = node.buffer;
RED.util.setMessageProperty(msg,node.property,node.buffer);
msg.parts.index = node.c++;
send(RED.util.cloneMessage(msg));
node.pendingDones.forEach(d => d());
@@ -230,7 +238,7 @@ module.exports = function(RED) {
var i = 0, p = 0;
pos = buff.indexOf(node.splt);
while (pos > -1) {
msg.payload = buff.slice(p,pos);
RED.util.setMessageProperty(msg,node.property,buff.slice(p,pos));
msg.parts.index = node.c++;
send(RED.util.cloneMessage(msg));
i++;
@@ -242,7 +250,7 @@ module.exports = function(RED) {
node.pendingDones = [];
}
if ((node.stream !== true) && (p < buff.length)) {
msg.payload = buff.slice(p,buff.length);
RED.util.setMessageProperty(msg,node.property,buff.slice(p,buff.length));
msg.parts.index = node.c++;
msg.parts.count = node.c++;
send(RED.util.cloneMessage(msg));
@@ -298,7 +306,6 @@ module.exports = function(RED) {
return exp
}
function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) {
var msgInfo = msgInfos.shift();
exp.assign("I", msgInfo.msg.parts.index);
@@ -330,6 +337,7 @@ module.exports = function(RED) {
}
});
}
function reduceAndSendGroup(node, group, done) {
var is_right = node.reduce_right;
var flag = is_right ? -1 : 1;
@@ -515,13 +523,13 @@ module.exports = function(RED) {
if (typeof group.joinChar !== 'string') {
groupJoinChar = group.joinChar.toString();
}
RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar));
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload.join(groupJoinChar));
}
else {
if (node.propertyType === 'full') {
group.msg = RED.util.cloneMessage(group.msg);
}
RED.util.setMessageProperty(group.msg,node.property,group.payload);
RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload);
}
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
group.msg.parts = group.msg.parts.parts;
@@ -589,7 +597,7 @@ module.exports = function(RED) {
}
if (node.mode === 'auto' && (!msg.hasOwnProperty("parts")||!msg.parts.hasOwnProperty("id"))) {
// if a blank reset messag erest it all.
// if a blank reset message reset it all.
if (msg.hasOwnProperty("reset")) {
if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) {
clearTimeout(inflight[partId].timeout);
@@ -603,6 +611,15 @@ module.exports = function(RED) {
return;
}
if (node.mode === 'custom' && msg.hasOwnProperty('parts')) {
if (msg.parts.hasOwnProperty('parts')) {
msg.parts = { parts: msg.parts.parts };
}
else {
delete msg.parts;
}
}
var payloadType;
var propertyKey;
var targetCount;
@@ -618,6 +635,7 @@ module.exports = function(RED) {
propertyKey = msg.parts.key;
arrayLen = msg.parts.len;
propertyIndex = msg.parts.index;
property = RED.util.getMessageProperty(msg,msg.parts.property||"payload");
}
else if (node.mode === 'reduce') {
return processReduceMessageQueue({msg, send, done});
@@ -719,6 +737,8 @@ module.exports = function(RED) {
completeSend(partId)
}, node.timer)
}
if (node.mode === "auto") { inflight[partId].prop = msg.parts.property; }
else { inflight[partId].prop = node.property; }
}
inflight[partId].dones.push(done);

View File

@@ -849,7 +849,13 @@
"newline": "Newline",
"usestrings": "parse numerical values",
"include_empty_strings": "include empty strings",
"include_null_values": "include null values"
"include_null_values": "include null values",
"spec": "Parser"
},
"spec": {
"rfc": "RFC4180",
"legacy": "Legacy",
"legacy_warning": "Legacy mode will be removed in a future release."
},
"placeholder": {
"columns": "comma-separated column names"
@@ -878,6 +884,7 @@
"once": "send headers once, until msg.reset"
},
"errors": {
"bad_template": "Malformed columns template.",
"csv_js": "This node only handles CSV strings or js objects.",
"obj_csv": "No columns template specified for object -> CSV.",
"bad_csv": "Malformed CSV data - output probably corrupt."
@@ -1001,7 +1008,7 @@
"tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process."
},
"split": {
"split": "split",
"split": "Split",
"intro": "Split <code>msg.payload</code> based on type:",
"object": "<b>Object</b>",
"objectSend": "Send a message for each key/value pair",

View File

@@ -30,6 +30,8 @@
before being sent.</p>
<p>If <code>msg._session</code> is not present the payload is
sent to <b>all</b> connected clients.</p>
<p>In Reply-to mode, setting <code>msg.reset = true</code> will reset the connection
specified by _session.id, or all connections if no _session.id is specified.</p>
<p><b>Note: </b>On some systems you may need root or administrator access
to access ports below 1024.</p>
</script>
@@ -40,6 +42,8 @@
returned characters into a fixed buffer, match a specified character before returning,
wait a fixed timeout from first reply and then return, sit and wait for data, or send then close the connection
immediately, without waiting for a reply.</p>
<p>If in sit and wait mode (remain connected) you can send <code>msg.reset = true</code> or <code>msg.reset = "host:port"</code> to force a break in
the connection and an automatic reconnection.</p>
<p>The response will be output in <code>msg.payload</code> as a buffer, so you may want to .toString() it.</p>
<p>If you leave tcp host or port blank they must be set by using the <code>msg.host</code> and <code>msg.port</code> properties in every message sent to the node.</p>
</script>

View File

@@ -36,7 +36,9 @@
</dl>
<h3>Details</h3>
<p>The column template can contain an ordered list of column names. When converting CSV to an object, the column names
will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV.</p>
will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV.
<p>When the RFC parser is selected, the column template must be compliant with RFC4180.</p>
</p>
<p>When converting to CSV, the columns template is used to identify which properties to extract from the object and in what order.</p>
<p>If the columns template is blank then you can use a simple comma separated list of properties supplied in <code>msg.columns</code> to
determine what to extract and in what order. If neither are present then all the object properties are output in the order
@@ -49,4 +51,5 @@
<p>If outputting multiple messages they will have their <code>parts</code> property set and form a complete message sequence.</p>
<p>If the node is set to only send column headers once, then setting <code>msg.reset</code> to any value will cause the node to resend the headers.</p>
<p><b>Note:</b> the column template must be comma separated - even if a different separator is chosen for the data.</p>
<p><b>Note:</b> in RFC mode, catchable errors will be thrown for malformed CSV headers and invalid input payload data</p>
</script>

View File

@@ -103,7 +103,7 @@
<h4>Automatic mode</h4>
<p>Automatic mode uses the <code>parts</code> property of incoming messages to
determine how the sequence should be joined. This allows it to automatically
reverse the action of a <b>split</b> node.</p>
reverse the action of a <b>split</b> node.
<h4>Manual mode</h4>
<p>When configured to join in manual mode, the node is able to join sequences

View File

@@ -1,3 +1,3 @@
<script type="text/html" data-help-name="global-config">
<p>大域的なフローの設定を保持するノード大域的な環境変数の定義を含みます</p>
</script>
</script>p

View File

@@ -23,7 +23,7 @@
<dt class="optional">template <span class="property-type">string</span></dt>
<dd><code>msg.payload</code>msg</dd>
</dl>
<h3>输出</h3>
<h3>Outputs</h3>
<dl class="message-properties">
<dt>msg <span class="property-type">object</span></dt>
<dd>由来自传入msg的属性来填充已配置的模板后输出的带有属性的msg</dd>
@@ -32,7 +32,7 @@
<p>默认情况下使用<i><a href="http://mustache.github.io/mustache.5.html" target="_blank">mustache</a></i>格式如有需要也可以切换其他格式</p>
<p>例如:
<pre>Hello {{payload.name}}. Today is {{date}}</pre>
<p>接收一条消息其中包含:
<p>receives a message containing:
<pre>{
date: "Monday",
payload: {

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "3.1.9",
"version": "4.0.0-dev",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -264,7 +264,7 @@ async function installModule(moduleDetails) {
"module": moduleDetails.module,
"version": moduleDetails.version,
"dir": installDir,
"args": ["--production","--engine-strict"]
"args": ["--omit=dev","--engine-strict"]
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
// preInstall passed
@@ -273,7 +273,7 @@ async function installModule(moduleDetails) {
let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installSpec]
log.trace(NPM_COMMAND + JSON.stringify(args));
return exec.run(NPM_COMMAND, args, { cwd: installDir, shell: true },true)
return exec.run(NPM_COMMAND, args, { cwd: installDir },true)
} else {
log.trace("skipping npm install");
}

View File

@@ -25,15 +25,12 @@ const registryUtil = require("./util");
const library = require("./library");
const {exec,log,events,hooks} = require("@node-red/util");
const child_process = require('child_process');
const isWindows = process.platform === 'win32'
const npmCommand = isWindows ? 'npm.cmd' : 'npm';
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
let installerEnabled = false;
let settings;
let settings;
const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/;
const slashRe = isWindows ? /\\|[/]/ : /[/]/;
const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/;
const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//;
const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/;
@@ -218,7 +215,7 @@ async function installModule(module,version,url) {
"dir": installDir,
"isExisting": isExisting,
"isUpgrade": isUpgrade,
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production','--engine-strict']
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--omit=dev','--engine-strict']
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
@@ -228,7 +225,7 @@ async function installModule(module,version,url) {
let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installName]
log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
return exec.run(npmCommand,args,{ cwd: installDir}, true)
} else {
log.trace("skipping npm install");
}
@@ -263,7 +260,7 @@ async function installModule(module,version,url) {
log.warn("------------------------------------------");
e = new Error(log._("server.install.install-failed")+": "+err.toString());
if (err.hook === "postInstall") {
return exec.run(npmCommand,["remove",module],{ cwd: installDir, shell: true }, false).finally(() => {
return exec.run(npmCommand,["remove",module],{ cwd: installDir}, false).finally(() => {
throw e;
})
}
@@ -359,7 +356,7 @@ async function getModuleVersionFromNPM(module, version) {
}
return new Promise((resolve, reject) => {
child_process.execFile(npmCommand,['info','--json',installName],{ shell: true },function(err,stdout,stderr) {
child_process.execFile(npmCommand,['info','--json',installName],function(err,stdout,stderr) {
try {
if (!stdout) {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
@@ -514,7 +511,7 @@ function uninstallModule(module) {
let extraArgs = triggerPayload.args || [];
let args = ['remove', ...extraArgs, module]
log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
return exec.run(npmCommand,args,{ cwd: installDir}, true)
} else {
log.trace("skipping npm uninstall");
}
@@ -581,7 +578,7 @@ async function checkPrereq() {
installerEnabled = false;
} else {
return new Promise(resolve => {
child_process.execFile(npmCommand,['-v'],{ shell: true },function(err,stdout) {
child_process.execFile(npmCommand,['-v'],function(err,stdout) {
if (err) {
log.info(log._("server.palette-editor.npm-not-found"));
installerEnabled = false;

View File

@@ -36,7 +36,7 @@ async function getFlowsFromPath(path) {
promises.push(getFlowsFromPath(fullPath));
} else if (/\.json$/.test(file)){
validFiles.push(file);
promises.push(Promise.resolve(file.replace(/\.json$/, '')))
promises.push(Promise.resolve(file.split(".")[0]))
}
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
"version": "3.1.9",
"version": "4.0.0-dev",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,11 +16,11 @@
}
],
"dependencies": {
"@node-red/util": "3.1.9",
"@node-red/util": "4.0.0-dev",
"clone": "2.1.2",
"fs-extra": "11.1.1",
"semver": "7.5.4",
"tar": "6.2.1",
"tar": "6.1.13",
"uglify-js": "3.17.4"
}
}

View File

@@ -68,6 +68,7 @@ var api = module.exports = {
* @param {String} opts.store - the context store
* @param {String} opts.key - the context key
* @param {Object} opts.req - the request to log (optional)
* @param {Boolean} opts.keysOnly - whether to return keys only
* @return {Promise} - the node information
* @memberof @node-red/runtime_context
*/
@@ -102,6 +103,15 @@ var api = module.exports = {
if (key) {
store = store || availableStores.default;
ctx.get(key,store,function(err, v) {
if (opts.keysOnly) {
if (Array.isArray(v)) {
resolve({ [store]: { format: `array[${v.length}]`}})
} else if (typeof v === 'object') {
resolve({ [store]: { keys: Object.keys(v), format: 'Object' } })
} else {
resolve({ [store]: { keys: [] }})
}
}
var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) {
encoded.store = store;
@@ -118,32 +128,58 @@ var api = module.exports = {
stores = [store];
}
var result = {};
var c = stores.length;
var errorReported = false;
stores.forEach(function(store) {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
if (opts.keysOnly) {
ctx.keys(store,function(err, keys) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return
}
result[store] = { keys }
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
}
}
})
} else {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return;
}
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
return;
}
}
});
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
resolve(result);
}
}
});
}
})
}
} else {

View File

@@ -485,7 +485,7 @@ class Flow {
}
if (!key.startsWith("$parent.")) {
if (this._env.hasOwnProperty(key)) {
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
}
} else {
key = key.substring(8);
@@ -678,9 +678,6 @@ class Flow {
if (logMessage.hasOwnProperty('stack')) {
errorMessage.error.stack = logMessage.stack;
}
if (logMessage.hasOwnProperty('cause')) {
errorMessage.error.cause = logMessage.cause;
}
targetCatchNode.receive(errorMessage);
handled = true;
});

View File

@@ -41,7 +41,7 @@ class Group {
}
if (!key.startsWith("$parent.")) {
if (this._env.hasOwnProperty(key)) {
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
}
} else {
key = key.substring(8);

View File

@@ -376,7 +376,7 @@ class Subflow extends Flow {
}
if (!key.startsWith("$parent.")) {
if (this._env.hasOwnProperty(key)) {
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
}
} else {
key = key.substring(8);

View File

@@ -106,22 +106,14 @@ async function evaluateEnvProperties(flow, env, credentials) {
result = { value: result, __clone__: true}
}
evaluatedEnv[name] = result
} else {
evaluatedEnv[name] = undefined
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
}
resolve()
});
}))
} else {
try {
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
if (typeof value === 'object') {
value = { value: value, __clone__: true}
}
} catch (err) {
value = undefined
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
if (typeof value === 'object') {
value = { value: value, __clone__: true}
}
}
evaluatedEnv[name] = value

View File

@@ -154,7 +154,7 @@ function start() {
log.info(log._("runtime.version",{component:"Node.js ",version:process.version}));
if (settings.UNSUPPORTED_VERSION) {
log.error("*****************************************************************");
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=8.9.0"})+" *");
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=18"})+" *");
log.error("*****************************************************************");
events.emit("runtime-event",{id:"runtime-unsupported-version",payload:{type:"error",text:"notification.errors.unsupportedVersion"},retain:true});
}

View File

@@ -77,7 +77,7 @@ var storageModuleInterface = {
flows: flows,
credentials: creds
};
result.rev = crypto.createHash('sha256').update(JSON.stringify(result.flows)).digest("hex");
result.rev = crypto.createHash('md5').update(JSON.stringify(result.flows)).digest("hex");
return result;
})
});
@@ -95,7 +95,7 @@ var storageModuleInterface = {
return credentialSavePromise.then(function() {
return storageModule.saveFlows(flows, user).then(function() {
return crypto.createHash('sha256').update(JSON.stringify(config.flows)).digest("hex");
return crypto.createHash('md5').update(JSON.stringify(config.flows)).digest("hex");
})
});
},

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
"version": "3.1.9",
"version": "4.0.0-dev",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,11 +16,11 @@
}
],
"dependencies": {
"@node-red/registry": "3.1.9",
"@node-red/util": "3.1.9",
"@node-red/registry": "4.0.0-dev",
"@node-red/util": "4.0.0-dev",
"async-mutex": "0.4.0",
"clone": "2.1.2",
"express": "4.19.2",
"express": "4.18.2",
"fs-extra": "11.1.1",
"json-stringify-safe": "5.0.1"
}

View File

@@ -636,7 +636,15 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
} else if (type === 're') {
result = new RegExp(value);
} else if (type === 'date') {
result = Date.now();
if (!value) {
result = Date.now();
} else if (value === 'object') {
result = new Date()
} else if (value === 'iso') {
result = (new Date()).toISOString()
} else {
result = moment().format(value)
}
} else if (type === 'bin') {
var data = JSON.parse(value);
if (Array.isArray(data) || (typeof(data) === "string")) {
@@ -769,12 +777,15 @@ function evaluateJSONataExpression(expr,msg,callback) {
});
}
} else {
log.warn('Deprecated API warning: Calls to RED.util.evaluateJSONataExpression must include a callback. '+
'This will not be optional in Node-RED 4.0. Please identify the node from the following stack '+
'and check for an update on npm. If none is available, please notify the node author.')
log.warn(new Error().stack)
const error = new Error('Calls to RED.util.evaluateJSONataExpression must include a callback.')
throw error
}
return expr.evaluate(context, bindings, callback);
expr.evaluate(context, bindings).then(result => {
callback(null, result)
}).catch(err => {
callback(err)
})
}
/**

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/util",
"version": "3.1.9",
"version": "4.0.0-dev",
"license": "Apache-2.0",
"repository": {
"type": "git",
@@ -18,7 +18,7 @@
"fs-extra": "11.1.1",
"i18next": "21.10.0",
"json-stringify-safe": "5.0.1",
"jsonata": "1.8.7",
"jsonata": "2.0.4",
"lodash.clonedeep": "^4.5.0",
"moment": "2.29.4",
"moment-timezone": "0.5.43"

View File

@@ -33,8 +33,7 @@ if (NODE_MAJOR_VERSION >= 16) {
function checkVersion(userSettings) {
var semver = require('semver');
if (!semver.satisfies(process.version,">=14.0.0")) {
// TODO: in the future, make this a hard error.
if (!semver.satisfies(process.version,">=18.0.0")) {
// var e = new Error("Unsupported version of Node.js");
// e.code = "unsupported_version";
// throw e;

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "3.1.9",
"version": "4.0.0-dev",
"description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org",
"license": "Apache-2.0",
@@ -31,15 +31,15 @@
"flow"
],
"dependencies": {
"@node-red/editor-api": "3.1.9",
"@node-red/runtime": "3.1.9",
"@node-red/util": "3.1.9",
"@node-red/nodes": "3.1.9",
"@node-red/editor-api": "4.0.0-dev",
"@node-red/runtime": "4.0.0-dev",
"@node-red/util": "4.0.0-dev",
"@node-red/nodes": "4.0.0-dev",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.19.2",
"express": "4.18.2",
"fs-extra": "11.1.1",
"node-red-admin": "^3.1.3",
"node-red-admin": "^3.1.2",
"nopt": "5.0.0",
"semver": "7.5.4"
},
@@ -47,6 +47,6 @@
"bcrypt": "5.1.0"
},
"engines": {
"node": ">=14"
"node": ">=18"
}
}

View File

@@ -26,6 +26,13 @@ if (process.argv[2] === 'admin') {
return;
}
var semver = require('semver');
if (!semver.satisfies(process.version, ">=18.0.0")) {
console.log("Unsupported version of Node.js:", process.version);
console.log("Node-RED requires Node.js v18 or later");
process.exit(1)
}
var http = require('http');
var https = require('https');
var util = require("util");
@@ -346,7 +353,7 @@ httpsPromise.then(function(startupHttps) {
} catch(err) {
if (err.code == "unsupported_version") {
console.log("Unsupported version of Node.js:",process.version);
console.log("Node-RED requires Node.js v8.9.0 or later");
console.log("Node-RED requires Node.js v18 or later");
} else {
console.log("Failed to start server:");
if (err.stack) {
@@ -408,9 +415,15 @@ httpsPromise.then(function(startupHttps) {
if (settings.httpAdminRoot !== false) {
app.use(settings.httpAdminRoot,RED.httpAdmin);
}
if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass));
if (typeof settings.httpNodeAuth === "function" || Array.isArray(settings.httpNodeAuth)) {
app.use(settings.httpNodeRoot, settings.httpNodeAuth);
} else {
app.use(settings.httpNodeRoot, basicAuthMiddleware(settings.httpNodeAuth.user, settings.httpNodeAuth.pass));
}
}
if (settings.httpNodeRoot !== false) {
app.use(settings.httpNodeRoot,RED.httpNode);
}

View File

@@ -449,6 +449,7 @@ module.exports = {
* - ui (for use with Node-RED Dashboard)
* - debugUseColors
* - debugMaxLength
* - debugStatusLength
* - execMaxBufferSize
* - httpRequestTimeout
* - mqttReconnectTime
@@ -504,6 +505,9 @@ module.exports = {
/** The maximum length, in characters, of any message sent to the debug sidebar tab */
debugMaxLength: 1000,
/** The maximum length, in characters, of status messages under the debug node */
//debugStatusLength: 32,
/** Maximum buffer size for the exec node. Defaults to 10Mb */
//execMaxBufferSize: 10000000,

View File

@@ -390,8 +390,7 @@ describe('function node', function() {
msg.should.have.property('level', helper.log().ERROR);
msg.should.have.property('id', 'n1');
msg.should.have.property('type', 'function');
msg.should.have.property('msg')
msg.msg.message.should.equal('ReferenceError: retunr is not defined (line 2, col 1)');
msg.should.have.property('msg', 'ReferenceError: retunr is not defined (line 2, col 1)');
done();
} catch(err) {
done(err);
@@ -660,8 +659,7 @@ describe('function node', function() {
msg.should.have.property('level', helper.log().ERROR);
msg.should.have.property('id', name);
msg.should.have.property('type', 'function');
msg.should.have.property('msg')
msg.msg.message.should.equal('Callback must be a function');
msg.should.have.property('msg', 'Error: Callback must be a function');
done();
}
catch (e) {
@@ -1720,9 +1718,13 @@ describe('function node', function() {
describe("init function", function() {
it('should delay handling messages until init completes', function(done) {
const timeoutMS = 200;
// Since helper.load uses process.nextTick timers might occasionally finish
// a couple of milliseconds too early, so give some leeway to the check.
const timeoutCheckMargin = 5;
var flow = [{id:"n1",type:"function",wires:[["n2"]],initialize: `
return new Promise((resolve,reject) => {
setTimeout(resolve,200)
setTimeout(resolve, ${timeoutMS});
})`,
func:"return msg;"
},
@@ -1735,9 +1737,10 @@ describe('function node', function() {
msg.delta = Date.now() - msg.payload;
receivedMsgs.push(msg)
if (receivedMsgs.length === 5) {
var errors = receivedMsgs.filter(msg => msg.delta < 200)
let deltas = receivedMsgs.map(msg => msg.delta);
var errors = deltas.filter(delta => delta < (timeoutMS - timeoutCheckMargin))
if (errors.length > 0) {
done(new Error(`Message received before init completed - was ${msg.delta} expected >300`))
done(new Error(`Message received before init completed - delta values ${JSON.stringify(deltas)} expected to be > ${timeoutMS - timeoutCheckMargin}`))
} else {
done();
}

View File

@@ -60,7 +60,6 @@ describe('HTTP Request Node', function() {
function startServer(done) {
testPort += 1;
testServer = stoppable(http.createServer(testApp));
const promises = []
testServer.listen(testPort,function(err) {
testSslPort += 1;
console.log("ssl port", testSslPort);
@@ -82,17 +81,13 @@ describe('HTTP Request Node', function() {
*/
};
testSslServer = stoppable(https.createServer(sslOptions,testApp));
console.log('> start testSslServer')
promises.push(new Promise((resolve, reject) => {
testSslServer.listen(testSslPort, function(err){
console.log(' done testSslServer')
if (err) {
reject(err)
} else {
resolve()
}
});
}))
testSslServer.listen(testSslPort, function(err){
if (err) {
console.log(err);
} else {
console.log("started testSslServer");
}
});
testSslClientPort += 1;
var sslClientOptions = {
@@ -102,17 +97,10 @@ describe('HTTP Request Node', function() {
requestCert: true
};
testSslClientServer = stoppable(https.createServer(sslClientOptions, testApp));
console.log('> start testSslClientServer')
promises.push(new Promise((resolve, reject) => {
testSslClientServer.listen(testSslClientPort, function(err){
console.log(' done testSslClientServer')
if (err) {
reject(err)
} else {
resolve()
}
});
}))
testSslClientServer.listen(testSslClientPort, function(err){
console.log("ssl-client", err)
});
testProxyPort += 1;
testProxyServer = stoppable(httpProxy(http.createServer()))
@@ -121,17 +109,7 @@ describe('HTTP Request Node', function() {
res.setHeader("x-testproxy-header", "foobar")
}
})
console.log('> testProxyServer')
promises.push(new Promise((resolve, reject) => {
testProxyServer.listen(testProxyPort, function(err) {
console.log(' done testProxyServer')
if (err) {
reject(err)
} else {
resolve()
}
})
}))
testProxyServer.listen(testProxyPort)
testProxyAuthPort += 1
testProxyServerAuth = stoppable(httpProxy(http.createServer()))
@@ -153,19 +131,9 @@ describe('HTTP Request Node', function() {
res.setHeader("x-testproxy-header", "foobar")
}
})
console.log('> testProxyServerAuth')
promises.push(new Promise((resolve, reject) => {
testProxyServerAuth.listen(testProxyAuthPort, function(err) {
console.log(' done testProxyServerAuth')
if (err) {
reject(err)
} else {
resolve()
}
})
}))
testProxyServerAuth.listen(testProxyAuthPort)
Promise.all(promises).then(() => { done() }).catch(done)
done(err);
});
}
@@ -461,11 +429,7 @@ describe('HTTP Request Node', function() {
if (err) {
done(err);
}
console.log('> helper.startServer')
helper.startServer(function(err) {
console.log('> helper started')
done(err)
});
helper.startServer(done);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -66,6 +66,27 @@ describe('SPLIT node', function() {
});
});
it('should split an array on a sub-property into multiple messages', function(done) {
var flow = [{id:"sn1", type:"split", property:"foo", wires:[["sn2"]]},
{id:"sn2", type:"helper"}];
helper.load(splitNode, flow, function() {
var sn1 = helper.getNode("sn1");
var sn2 = helper.getNode("sn2");
sn2.on("input", function(msg) {
msg.should.have.property("parts");
msg.parts.should.have.property("count",4);
msg.parts.should.have.property("type","array");
msg.parts.should.have.property("index");
msg.parts.should.have.property("property","foo");
if (msg.parts.index === 0) { msg.foo.should.equal(1); }
if (msg.parts.index === 1) { msg.foo.should.equal(2); }
if (msg.parts.index === 2) { msg.foo.should.equal(3); }
if (msg.parts.index === 3) { msg.foo.should.equal(4); done(); }
});
sn1.receive({foo:[1,2,3,4]});
});
});
it('should split an array into multiple messages of a specified size', function(done) {
var flow = [{id:"sn1", type:"split", wires:[["sn2"]], arraySplt:3, arraySpltType:"len"},
{id:"sn2", type:"helper"}];
@@ -108,6 +129,31 @@ describe('SPLIT node', function() {
});
});
it('should split an object sub property into pieces', function(done) {
var flow = [{id:"sn1", type:"split", property:"foo.bar",wires:[["sn2"]]},
{id:"sn2", type:"helper"}];
helper.load(splitNode, flow, function() {
var sn1 = helper.getNode("sn1");
var sn2 = helper.getNode("sn2");
var count = 0;
sn2.on("input", function(msg) {
msg.should.have.property("foo");
msg.foo.should.have.property("bar");
msg.should.have.property("parts");
msg.parts.should.have.property("type","object");
msg.parts.should.have.property("key");
msg.parts.should.have.property("count");
msg.parts.should.have.property("index");
msg.parts.should.have.property("property","foo.bar");
msg.topic.should.equal("foo");
if (msg.parts.index === 0) { msg.foo.bar.should.equal(1); }
if (msg.parts.index === 1) { msg.foo.bar.should.equal("2"); }
if (msg.parts.index === 2) { msg.foo.bar.should.equal(true); done(); }
});
sn1.receive({topic:"foo",foo:{bar:{a:1,b:"2",c:true}}});
});
});
it('should split an object into pieces and overwrite their topics', function(done) {
var flow = [{id:"sn1", type:"split", addname:"topic", wires:[["sn2"]]},
{id:"sn2", type:"helper"}];
@@ -516,6 +562,7 @@ describe('JOIN node', function() {
n1.receive({payload:{a:1}});
});
});
it('should join things into an array ignoring msg.parts.index in manual mode', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",",mode:"custom"},
{id:"n2", type:"helper"}];
@@ -562,6 +609,32 @@ describe('JOIN node', function() {
});
});
it('should join things into an array on a sub property in auto mode', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",", mode:"auto"},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("foo");
msg.foo.should.have.property("bar");
msg.foo.bar.should.be.an.Array();
msg.foo.bar[0].should.equal("A");
msg.foo.bar[1].should.equal("B");
//msg.payload[2].a.should.equal(1);
done();
}
catch(e) {done(e);}
});
n1.receive({foo:{bar:"A"}, parts:{id:1, type:"array", len:1, index:0, count:4, property:"foo.bar"}});
n1.receive({foo:{bar:"B"}, parts:{id:1, type:"array", len:1, index:1, count:4, property:"foo.bar"}});
n1.receive({foo:{bar:"C"}, parts:{id:1, type:"array", len:1, index:2, count:4, property:"foo.bar"}});
n1.receive({foo:{bar:"D"}, parts:{id:1, type:"array", len:1, index:3, count:4, property:"foo.bar"}});
});
});
it('should join strings into a buffer after a count', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:2, build:"buffer", joinerType:"bin", joiner:"", mode:"custom"},
{id:"n2", type:"helper"}];
@@ -639,6 +712,35 @@ describe('JOIN node', function() {
});
});
it('should merge sub property objects', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:5, property:"foo.bar", build:"merged", mode:"custom"},
{id:"n2", type:"helper"}];
helper.load(joinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("foo");
msg.foo.should.have.property("bar");
msg.foo.bar.should.have.property("a",1);
msg.foo.bar.should.have.property("b",2);
msg.foo.bar.should.have.property("c",3);
msg.foo.bar.should.have.property("d",4);
msg.foo.bar.should.have.property("e",5);
done();
}
catch(e) { done(e)}
});
n1.receive({foo:{bar:{a:9}, topic:"f"}});
n1.receive({foo:{bar:{a:1}, topic:"a"}});
n1.receive({foo:{bar:{b:9}, topic:"b"}});
n1.receive({foo:{bar:{b:2}, topic:"b"}});
n1.receive({foo:{bar:{c:3}, topic:"c"}});
n1.receive({foo:{bar:{d:4}, topic:"d"}});
n1.receive({foo:{bar:{e:5}, topic:"e"}});
});
});
it('should merge full msg objects', function(done) {
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:6, build:"merged", mode:"custom", propertyType:"full", property:""},
{id:"n2", type:"helper"}];

View File

@@ -33,15 +33,16 @@ describe("library api", function() {
should.not.exist(library.getExampleFlowPath('foo','bar'));
});
it('returns valid example paths', function(done) {
it('returns a valid example path', function(done) {
library.init();
library.addExamplesDir("test-module",path.resolve(__dirname+'/resources/examples')).then(function() {
try {
var flows = library.getExampleFlows();
flows.should.deepEqual({"test-module":{"f":["1.2.3","one"]}});
flows.should.deepEqual({"test-module":{"f":["one"]}});
var examplePath = library.getExampleFlowPath('test-module','one');
examplePath.should.eql(path.resolve(__dirname+'/resources/examples/one.json'));
examplePath.should.eql(path.resolve(__dirname+'/resources/examples/one.json'))
library.removeExamplesDir('test-module');
@@ -56,5 +57,6 @@ describe("library api", function() {
done(err);
}
});
});
})
});

View File

@@ -379,10 +379,17 @@ describe("@node-red/util/util", function() {
result = util.evaluateNodeProperty('','bool');
result.should.be.false();
});
it('returns date',function() {
it('returns date - default format',function() {
var result = util.evaluateNodeProperty('','date');
(Date.now() - result).should.be.approximately(0,50);
});
it('returns date - iso format',function() {
var result = util.evaluateNodeProperty('iso','date');
// 2023-12-04T16:51:04.429Z
/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z$/.test(result).should.be.true()
});
it('returns bin', function () {
var result = util.evaluateNodeProperty('[1, 2]','bin');
result[0].should.eql(1);
@@ -441,9 +448,16 @@ describe("@node-red/util/util", function() {
},{});
result.should.eql("123");
});
it('returns jsonata result', function () {
var result = util.evaluateNodeProperty('$abs(-1)','jsonata',{},{});
result.should.eql(1);
it('returns jsonata result', function (done) {
util.evaluateNodeProperty('$abs(-1)','jsonata',{},{}, (err, result) => {
try {
result.should.eql(1);
done()
} catch (error) {
done(error)
}
});
});
it('returns null', function() {
var result = util.evaluateNodeProperty(null,'null');
@@ -601,51 +615,105 @@ describe("@node-red/util/util", function() {
});
});
describe('evaluateJSONataExpression', function() {
it('evaluates an expression', function() {
it('evaluates an expression', function(done) {
var expr = util.prepareJSONataExpression('payload',{});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
result.should.eql("hello");
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
result.should.eql("hello");
done()
} catch (error) {
done(error)
}
});
});
it('evaluates a legacyMode expression', function() {
var expr = util.prepareJSONataExpression('msg.payload',{});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
result.should.eql("hello");
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
result.should.eql("hello");
done()
} catch (error) {
done(error)
}
});
});
it('accesses flow context from an expression', function() {
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
result.should.eql("bar");
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
result.should.eql("bar");
done()
} catch (error) {
done(error)
}
});
});
it('accesses undefined environment variable from an expression', function() {
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('');
});
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("");
done()
} catch (error) {
done(error)
}
});
});
it('accesses environment variable from an expression', function() {
process.env.UTIL_ENV = 'foo';
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('foo');
});
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("foo");
done()
} catch (error) {
done(error)
}
});
});
it('accesses moment from an expression', function() {
var expr = util.prepareJSONataExpression('$moment("2020-05-27", "YYYY-MM-DD").add(7, "days").add(1, "months").format("YYYY-MM-DD")',{});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('2020-07-03');
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("2020-07-03");
done()
} catch (error) {
done(error)
}
});
});
it('accesses moment-timezone from an expression', function() {
var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{});
var result = util.evaluateJSONataExpression(expr,{});
result.should.eql('2013-11-18T19:55:00+08:00');
util.evaluateJSONataExpression(expr,{}, (err, result) => {
try {
result.should.eql("2013-11-18T19:55:00+08:00");
done()
} catch (error) {
done(error)
}
});
});
it('handles non-existant flow context variable', function() {
var expr = util.prepareJSONataExpression('$flowContext("nonExistant")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
should.not.exist(result);
});
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
should.not.exist(result);
done()
} catch (error) {
done(error)
}
});
});
it('handles non-existant global context variable', function() {
var expr = util.prepareJSONataExpression('$globalContext("nonExistant")',{context:function() { return {global:{get: function(key) { return {'foo':'bar'}[key]}}}}});
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
should.not.exist(result);
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
try {
should.not.exist(result);
done()
} catch (error) {
done(error)
}
});
});
it('handles async flow context access', function(done) {
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,store,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});