mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Compare commits
55 Commits
4704-node-
...
Fix-join-t
Author | SHA1 | Date | |
---|---|---|---|
|
d5b424910f | ||
|
d94d13737f | ||
|
b1fa4918e3 | ||
|
742aa2fa0d | ||
|
ce133c1c04 | ||
|
e4dc1779c3 | ||
|
22b4ab6bb2 | ||
|
2dcff51125 | ||
|
b50e0533eb | ||
|
711545539f | ||
|
a6cbceed28 | ||
|
6802539ccc | ||
|
74efaa3c2d | ||
|
a5223709ba | ||
|
2291dc6132 | ||
|
b2548c158d | ||
|
5a48d6d4cd | ||
|
7ee2b93b10 | ||
|
cc611a7a02 | ||
|
1a9c34fe40 | ||
|
ff8eb0ec2b | ||
|
f66b48e586 | ||
|
931a2344b4 | ||
|
dd3c75d298 | ||
|
4a4a15de93 | ||
|
a007ab7f2e | ||
|
54e6d60fe5 | ||
|
c2710f4f6f | ||
|
20187b51b1 | ||
|
4be6d57d98 | ||
|
a77f8cc3e9 | ||
|
ea4c0cdbee | ||
|
7197153fd5 | ||
|
b9c1dedab3 | ||
|
918943816f | ||
|
33cf34f7c7 | ||
|
5b5b06cc06 | ||
|
f49f692ffa | ||
|
10ce681d46 | ||
|
08c6ea94cb | ||
|
fea1da5542 | ||
|
32e8f4eac6 | ||
|
bfe5a8a986 | ||
|
f2cb5ea44e | ||
|
c7335ed25b | ||
|
5fda57c730 | ||
|
9fd929ac1e | ||
|
eb940d6d57 | ||
|
9091935d77 | ||
|
34e8d2b051 | ||
|
0c2ab13c48 | ||
|
9489953a8f | ||
|
54d4079457 | ||
|
cef3a01042 | ||
|
0c042abcab |
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -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 }}
|
||||
|
39
CHANGELOG.md
39
CHANGELOG.md
@@ -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
|
||||
|
14
package.json
14
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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");
|
||||
|
@@ -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
|
||||
|
@@ -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");
|
||||
|
@@ -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");
|
||||
|
@@ -15,6 +15,8 @@
|
||||
**/
|
||||
|
||||
var apiUtils = require("../util");
|
||||
var fs = require('fs');
|
||||
var fspath = require('path');
|
||||
|
||||
var runtimeAPI;
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -15,6 +15,7 @@
|
||||
**/
|
||||
|
||||
var apiUtils = require("../util");
|
||||
var express = require("express");
|
||||
var runtimeAPI;
|
||||
var settings;
|
||||
|
||||
|
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var express = require("express");
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
|
@@ -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;
|
||||
|
@@ -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");
|
||||
|
@@ -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",
|
||||
|
@@ -925,6 +925,12 @@
|
||||
"jsonata": "expression",
|
||||
"env": "env variable",
|
||||
"cred": "credential"
|
||||
},
|
||||
"date": {
|
||||
"format": {
|
||||
"timestamp": "milliseconds since epoch",
|
||||
"object": "JavaScript Date Object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editableList": {
|
||||
|
@@ -303,8 +303,7 @@
|
||||
"missingType": "不正なフロー - __index__ 番目の要素に'type'プロパティがありません"
|
||||
},
|
||||
"conflictNotification1": "読み込もうとしているノードのいくつかは、既にワークスペース内に存在しています。",
|
||||
"conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。",
|
||||
"alreadyExists": "本ノードは既に存在"
|
||||
"conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。"
|
||||
},
|
||||
"copyMessagePath": "パスをコピーしました",
|
||||
"copyMessageValue": "値をコピーしました",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
@@ -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") },
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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 {
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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", {
|
||||
|
@@ -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"},
|
||||
|
@@ -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 }
|
||||
}
|
||||
})
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
@@ -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') {
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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)
|
||||
}
|
||||
|
324
packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
vendored
Normal file
324
packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
vendored
Normal 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
|
@@ -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"),
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<script type="text/html" data-help-name="global-config">
|
||||
<p>大域的なフローの設定を保持するノード。大域的な環境変数の定義を含みます。</p>
|
||||
</script>
|
||||
</script>p
|
||||
|
@@ -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: {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/nodes",
|
||||
"version": "3.1.9",
|
||||
"version": "4.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -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");
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
});
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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});
|
||||
}
|
||||
|
@@ -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");
|
||||
})
|
||||
});
|
||||
},
|
||||
|
@@ -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"
|
||||
}
|
||||
|
23
packages/node_modules/@node-red/util/lib/util.js
vendored
23
packages/node_modules/@node-red/util/lib/util.js
vendored
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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"
|
||||
|
3
packages/node_modules/node-red/lib/red.js
vendored
3
packages/node_modules/node-red/lib/red.js
vendored
@@ -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;
|
||||
|
16
packages/node_modules/node-red/package.json
vendored
16
packages/node_modules/node-red/package.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
|
17
packages/node_modules/node-red/red.js
vendored
17
packages/node_modules/node-red/red.js
vendored
@@ -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);
|
||||
}
|
||||
|
4
packages/node_modules/node-red/settings.js
vendored
4
packages/node_modules/node-red/settings.js
vendored
@@ -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,
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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
@@ -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"}];
|
||||
|
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
|
@@ -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)}}}}});
|
||||
|
Reference in New Issue
Block a user