mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Merge branch 'dev' into config-in-subflow
This commit is contained in:
commit
3e2508c740
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,3 +1,26 @@
|
|||||||
|
#### 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
|
#### 3.1.5: Maintenance Release
|
||||||
|
|
||||||
Runtime
|
Runtime
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
"is-utf8": "0.2.1",
|
"is-utf8": "0.2.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"json-stringify-safe": "5.0.1",
|
"json-stringify-safe": "5.0.1",
|
||||||
"jsonata": "1.8.6",
|
"jsonata": "2.0.4",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"media-typer": "1.1.0",
|
"media-typer": "1.1.0",
|
||||||
"memorystore": "1.6.7",
|
"memorystore": "1.6.7",
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
**/
|
**/
|
||||||
var apiUtils = require("../util");
|
|
||||||
var runtimeAPI;
|
var runtimeAPI;
|
||||||
var settings;
|
var settings;
|
||||||
var theme = require("../editor/theme");
|
var theme = require("../editor/theme");
|
||||||
|
@ -18,7 +18,6 @@ var BearerStrategy = require('passport-http-bearer').Strategy;
|
|||||||
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy;
|
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy;
|
||||||
|
|
||||||
var passport = require("passport");
|
var passport = require("passport");
|
||||||
var crypto = require("crypto");
|
|
||||||
var util = require("util");
|
var util = require("util");
|
||||||
|
|
||||||
var Tokens = require("./tokens");
|
var Tokens = require("./tokens");
|
||||||
|
@ -14,11 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
**/
|
**/
|
||||||
|
|
||||||
var express = require("express");
|
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
|
||||||
var comms = require("./comms");
|
var comms = require("./comms");
|
||||||
var library = require("./library");
|
|
||||||
var info = require("./settings");
|
var info = require("./settings");
|
||||||
|
|
||||||
var auth = require("../auth");
|
var auth = require("../auth");
|
||||||
|
@ -15,8 +15,6 @@
|
|||||||
**/
|
**/
|
||||||
|
|
||||||
var apiUtils = require("../util");
|
var apiUtils = require("../util");
|
||||||
var fs = require('fs');
|
|
||||||
var fspath = require('path');
|
|
||||||
|
|
||||||
var runtimeAPI;
|
var runtimeAPI;
|
||||||
|
|
||||||
|
@ -13,9 +13,6 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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
|
var i18n = require("@node-red/util").i18n; // TODO: separate module
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
**/
|
**/
|
||||||
|
|
||||||
var apiUtils = require("../util");
|
var apiUtils = require("../util");
|
||||||
var express = require("express");
|
|
||||||
var runtimeAPI;
|
var runtimeAPI;
|
||||||
var settings;
|
var settings;
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
**/
|
**/
|
||||||
|
|
||||||
var express = require("express");
|
|
||||||
var util = require("util");
|
var util = require("util");
|
||||||
var path = require("path");
|
var path = require("path");
|
||||||
var fs = require("fs");
|
var fs = require("fs");
|
||||||
|
@ -99,7 +99,7 @@ module.exports = {
|
|||||||
// settings.instanceId is set asynchronously to the editor-api
|
// settings.instanceId is set asynchronously to the editor-api
|
||||||
// being initiaised. So we defer calculating the cacheBuster hash
|
// being initiaised. So we defer calculating the cacheBuster hash
|
||||||
// until the first load of the editor
|
// until the first load of the editor
|
||||||
cacheBuster = crypto.createHash('md5').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12)
|
cacheBuster = crypto.createHash('sha1').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12)
|
||||||
}
|
}
|
||||||
|
|
||||||
let sessionMessages;
|
let sessionMessages;
|
||||||
|
@ -24,11 +24,8 @@
|
|||||||
* @namespace @node-red/editor-api
|
* @namespace @node-red/editor-api
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var express = require("express");
|
|
||||||
var bodyParser = require("body-parser");
|
var bodyParser = require("body-parser");
|
||||||
var util = require('util');
|
|
||||||
var passport = require('passport');
|
var passport = require('passport');
|
||||||
var cors = require('cors');
|
|
||||||
|
|
||||||
var auth = require("./auth");
|
var auth = require("./auth");
|
||||||
var apiUtil = require("./util");
|
var apiUtil = require("./util");
|
||||||
|
@ -926,6 +926,12 @@
|
|||||||
"env": "env variable",
|
"env": "env variable",
|
||||||
"cred": "credential",
|
"cred": "credential",
|
||||||
"conf-types": "config node"
|
"conf-types": "config node"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"format": {
|
||||||
|
"timestamp": "milliseconds since epoch",
|
||||||
|
"object": "JavaScript Date Object"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editableList": {
|
"editableList": {
|
||||||
|
@ -303,7 +303,8 @@
|
|||||||
"missingType": "不正なフロー - __index__ 番目の要素に'type'プロパティがありません"
|
"missingType": "不正なフロー - __index__ 番目の要素に'type'プロパティがありません"
|
||||||
},
|
},
|
||||||
"conflictNotification1": "読み込もうとしているノードのいくつかは、既にワークスペース内に存在しています。",
|
"conflictNotification1": "読み込もうとしているノードのいくつかは、既にワークスペース内に存在しています。",
|
||||||
"conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。"
|
"conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。",
|
||||||
|
"alreadyExists": "本ノードは既に存在"
|
||||||
},
|
},
|
||||||
"copyMessagePath": "パスをコピーしました",
|
"copyMessagePath": "パスをコピーしました",
|
||||||
"copyMessageValue": "値をコピーしました",
|
"copyMessageValue": "値をコピーしました",
|
||||||
|
@ -408,7 +408,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
|
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: {
|
jsonata: {
|
||||||
value: "jsonata",
|
value: "jsonata",
|
||||||
label: "expression",
|
label: "expression",
|
||||||
@ -709,6 +727,10 @@
|
|||||||
allOptions.flow.options = contextStoreOptions;
|
allOptions.flow.options = contextStoreOptions;
|
||||||
allOptions.global.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;
|
nlsd = true;
|
||||||
var that = this;
|
var that = this;
|
||||||
|
@ -483,6 +483,16 @@ RED.utils = (function() {
|
|||||||
$('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e);
|
$('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj);
|
||||||
|
if (n) {
|
||||||
|
if (options.nodeSelector && "function" == typeof options.nodeSelector) {
|
||||||
|
e.css('cursor', 'pointer').on("click", function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
options.nodeSelector(n.id);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else if (typeof obj === 'number') {
|
} else if (typeof obj === 'number') {
|
||||||
e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);
|
e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);
|
||||||
|
|
||||||
@ -589,6 +599,7 @@ RED.utils = (function() {
|
|||||||
exposeApi: exposeApi,
|
exposeApi: exposeApi,
|
||||||
// tools: tools // Do not pass tools down as we
|
// tools: tools // Do not pass tools down as we
|
||||||
// keep them attached to the top-level header
|
// keep them attached to the top-level header
|
||||||
|
nodeSelector: options.nodeSelector,
|
||||||
}
|
}
|
||||||
).appendTo(row);
|
).appendTo(row);
|
||||||
}
|
}
|
||||||
@ -619,6 +630,7 @@ RED.utils = (function() {
|
|||||||
exposeApi: exposeApi,
|
exposeApi: exposeApi,
|
||||||
// tools: tools // Do not pass tools down as we
|
// tools: tools // Do not pass tools down as we
|
||||||
// keep them attached to the top-level header
|
// keep them attached to the top-level header
|
||||||
|
nodeSelector: options.nodeSelector,
|
||||||
}
|
}
|
||||||
).appendTo(row);
|
).appendTo(row);
|
||||||
}
|
}
|
||||||
@ -675,6 +687,7 @@ RED.utils = (function() {
|
|||||||
exposeApi: exposeApi,
|
exposeApi: exposeApi,
|
||||||
// tools: tools // Do not pass tools down as we
|
// tools: tools // Do not pass tools down as we
|
||||||
// keep them attached to the top-level header
|
// keep them attached to the top-level header
|
||||||
|
nodeSelector: options.nodeSelector,
|
||||||
}
|
}
|
||||||
).appendTo(row);
|
).appendTo(row);
|
||||||
}
|
}
|
||||||
@ -906,7 +919,10 @@ RED.utils = (function() {
|
|||||||
* @returns true if valid, String if invalid
|
* @returns true if valid, String if invalid
|
||||||
*/
|
*/
|
||||||
function validateTypedProperty(propertyValue, propertyType, opt) {
|
function validateTypedProperty(propertyValue, propertyType, opt) {
|
||||||
|
if (propertyValue && /^\${[^}]+}$/.test(propertyValue)) {
|
||||||
|
// Allow ${ENV_VAR} value
|
||||||
|
return true
|
||||||
|
}
|
||||||
let error
|
let error
|
||||||
if (propertyType === 'json') {
|
if (propertyType === 'json') {
|
||||||
try {
|
try {
|
||||||
|
@ -4156,7 +4156,7 @@ RED.view = (function() {
|
|||||||
}
|
}
|
||||||
var width = img.width * scaleFactor;
|
var width = img.width * scaleFactor;
|
||||||
if (width > 20) {
|
if (width > 20) {
|
||||||
scalefactor *= 20/width;
|
scaleFactor *= 20/width;
|
||||||
width = 20;
|
width = 20;
|
||||||
}
|
}
|
||||||
var height = img.height * scaleFactor;
|
var height = img.height * scaleFactor;
|
||||||
|
@ -16,8 +16,20 @@
|
|||||||
RED.validators = {
|
RED.validators = {
|
||||||
number: function(blankAllowed,mopt){
|
number: function(blankAllowed,mopt){
|
||||||
return function(v, opt) {
|
return function(v, opt) {
|
||||||
if ((blankAllowed&&(v===''||v===undefined)) || (v!=='' && !isNaN(v))) {
|
if (blankAllowed && (v === '' || v === undefined)) {
|
||||||
return true;
|
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 (opt && opt.label) {
|
if (opt && opt.label) {
|
||||||
return RED._("validator.errors.invalid-num-prop", {
|
return RED._("validator.errors.invalid-num-prop", {
|
||||||
|
@ -38,7 +38,7 @@ body {
|
|||||||
}
|
}
|
||||||
#red-ui-main-container {
|
#red-ui-main-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top:40px; left:0; bottom: 0; right:0;
|
top: var(--red-ui-header-height); left:0; bottom: 0; right:0;
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +259,8 @@ $deploy-button-background-disabled-hover: #555;
|
|||||||
|
|
||||||
$header-background: #000;
|
$header-background: #000;
|
||||||
$header-button-background-active: #121212;
|
$header-button-background-active: #121212;
|
||||||
$header-menu-color: #C7C7C7;
|
$header-accent: #d41313;
|
||||||
|
$header-menu-color: #eee;
|
||||||
$header-menu-color-disabled: #666;
|
$header-menu-color-disabled: #666;
|
||||||
$header-menu-heading-color: #fff;
|
$header-menu-heading-color: #fff;
|
||||||
$header-menu-sublabel-color: #aeaeae;
|
$header-menu-sublabel-color: #aeaeae;
|
||||||
|
@ -23,16 +23,20 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
height: var(--red-ui-header-height);
|
||||||
background: var(--red-ui-header-background);
|
background: var(--red-ui-header-background);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0px 0px 0px 20px;
|
padding: 0px 0px 0px 20px;
|
||||||
color: var(--red-ui-header-menu-color);
|
color: var(--red-ui-header-menu-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2px solid var(--red-ui-header-accent);
|
||||||
|
padding-top: 2px;
|
||||||
|
|
||||||
span.red-ui-header-logo {
|
span.red-ui-header-logo {
|
||||||
float: left;
|
float: left;
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -42,7 +46,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
margin-left: 5px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
|
17
packages/node_modules/@node-red/editor-client/src/sass/sizes.scss
vendored
Normal file
17
packages/node_modules/@node-red/editor-client/src/sass/sizes.scss
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
**/
|
||||||
|
|
||||||
|
$header-height: 48px;
|
@ -15,4 +15,5 @@
|
|||||||
**/
|
**/
|
||||||
|
|
||||||
@import "colors";
|
@import "colors";
|
||||||
|
@import "sizes";
|
||||||
@import "variables";
|
@import "variables";
|
@ -15,6 +15,7 @@
|
|||||||
**/
|
**/
|
||||||
|
|
||||||
@import "colors";
|
@import "colors";
|
||||||
|
@import "sizes";
|
||||||
@import "variables";
|
@import "variables";
|
||||||
@import "mixins";
|
@import "mixins";
|
||||||
|
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
--red-ui-shadow: #{$shadow};
|
--red-ui-shadow: #{$shadow};
|
||||||
|
|
||||||
|
// Header Height
|
||||||
|
--red-ui-header-height: #{$header-height};
|
||||||
|
|
||||||
// Main body text
|
// Main body text
|
||||||
--red-ui-primary-text-color: #{$primary-text-color};
|
--red-ui-primary-text-color: #{$primary-text-color};
|
||||||
// UI control label text
|
// UI control label text
|
||||||
@ -240,6 +243,7 @@
|
|||||||
|
|
||||||
|
|
||||||
--red-ui-header-background: #{$header-background};
|
--red-ui-header-background: #{$header-background};
|
||||||
|
--red-ui-header-accent: #{$header-accent};
|
||||||
--red-ui-header-button-background-active: #{$header-button-background-active};
|
--red-ui-header-button-background-active: #{$header-button-background-active};
|
||||||
--red-ui-header-menu-color: #{$header-menu-color};
|
--red-ui-header-menu-color: #{$header-menu-color};
|
||||||
--red-ui-header-menu-color-disabled: #{$header-menu-color-disabled};
|
--red-ui-header-menu-color-disabled: #{$header-menu-color-disabled};
|
||||||
|
@ -227,34 +227,42 @@
|
|||||||
name: {value:""},
|
name: {value:""},
|
||||||
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) {
|
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) {
|
||||||
if (!v || v.length === 0) { return true }
|
if (!v || v.length === 0) { return true }
|
||||||
|
const errors = []
|
||||||
for (var i=0;i<v.length;i++) {
|
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 (/msg|flow|global/.test(v[i].vt)) {
|
||||||
if (!RED.utils.validatePropertyExpression(v[i].v)) {
|
if (!RED.utils.validatePropertyExpression(v[i].v)) {
|
||||||
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
|
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
|
||||||
}
|
}
|
||||||
} else if (v[i].vt === "jsonata") {
|
} else if (v[i].vt === "jsonata") {
|
||||||
try{ jsonata(v[i].v); }
|
try{ jsonata(v[i].v); }
|
||||||
catch(e){
|
catch(e){
|
||||||
return RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message });
|
errors.push(RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message }))
|
||||||
}
|
}
|
||||||
} else if (v[i].vt === "json") {
|
} else if (v[i].vt === "json") {
|
||||||
try{ JSON.parse(v[i].v); }
|
try{ JSON.parse(v[i].v); }
|
||||||
catch(e){
|
catch(e){
|
||||||
return RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message });
|
errors.push(RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message }))
|
||||||
}
|
}
|
||||||
} else if (v[i].vt === "num"){
|
} else if (v[i].vt === "num"){
|
||||||
if (!/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v[i].v)) {
|
if (!/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v[i].v)) {
|
||||||
return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v });
|
errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return errors
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
repeat: {
|
repeat: {
|
||||||
value:"", validate: function(v, opt) {
|
value:"", validate: function(v, opt) {
|
||||||
if ((v === "") ||
|
if ((v === "") ||
|
||||||
(RED.validators.number(v) &&
|
(RED.validators.number()(v) &&
|
||||||
(v >= 0) && (v <= 2147483))) {
|
(v >= 0) && (v <= 2147483))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -263,7 +271,7 @@
|
|||||||
},
|
},
|
||||||
crontab: {value:""},
|
crontab: {value:""},
|
||||||
once: {value:false},
|
once: {value:false},
|
||||||
onceDelay: {value:0.1},
|
onceDelay: {value:0.1, validate: RED.validators.number(true)},
|
||||||
topic: {value:""},
|
topic: {value:""},
|
||||||
payload: {value:"", validate: RED.validators.typedInput("payloadType", false) },
|
payload: {value:"", validate: RED.validators.typedInput("payloadType", false) },
|
||||||
payloadType: {value:"date"},
|
payloadType: {value:"date"},
|
||||||
|
@ -512,7 +512,8 @@ RED.debug = (function() {
|
|||||||
hideKey: false,
|
hideKey: false,
|
||||||
path: path,
|
path: path,
|
||||||
sourceId: sourceNode&&sourceNode.id,
|
sourceId: sourceNode&&sourceNode.id,
|
||||||
rootPath: path
|
rootPath: path,
|
||||||
|
nodeSelector: config.messageSourceClick,
|
||||||
});
|
});
|
||||||
// Do this in a separate step so the element functions aren't stripped
|
// Do this in a separate step so the element functions aren't stripped
|
||||||
debugMessage.appendTo(el);
|
debugMessage.appendTo(el);
|
||||||
|
@ -117,7 +117,7 @@ module.exports = function(RED) {
|
|||||||
});
|
});
|
||||||
return
|
return
|
||||||
} else if (rule.tot === 'date') {
|
} else if (rule.tot === 'date') {
|
||||||
value = Date.now();
|
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
|
||||||
} else if (rule.tot === 'jsonata') {
|
} else if (rule.tot === 'jsonata') {
|
||||||
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
|
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -582,6 +582,7 @@ module.exports = function(RED) {
|
|||||||
const cc = Object.keys(clients).length;
|
const cc = Object.keys(clients).length;
|
||||||
node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
|
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; }
|
if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
|
||||||
|
if (!msg.hasOwnProperty("payload")) { return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store client information independently
|
// Store client information independently
|
||||||
|
@ -17,7 +17,20 @@
|
|||||||
</select>
|
</select>
|
||||||
<input style="width:40px;" type="text" id="node-input-sep" pattern=".">
|
<input style="width:40px;" type="text" id="node-input-sep" pattern=".">
|
||||||
</div>
|
</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">
|
<div class="form-row">
|
||||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
<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">
|
<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;">
|
<div class="form-row" style="padding-left:20px;">
|
||||||
<label></label>
|
<label></label>
|
||||||
<label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></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='\n' data-i18n="csv.newline.linux"></option>
|
||||||
<option value='\r' data-i18n="csv.newline.mac"></option>
|
<option value='\r' data-i18n="csv.newline.mac"></option>
|
||||||
<option value='\r\n' data-i18n="csv.newline.windows"></option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
@ -75,6 +88,7 @@
|
|||||||
color:"#DEBD5C",
|
color:"#DEBD5C",
|
||||||
defaults: {
|
defaults: {
|
||||||
name: {value:""},
|
name: {value:""},
|
||||||
|
spec: {value:"rfc"},
|
||||||
sep: {
|
sep: {
|
||||||
value:',', required:true,
|
value:',', required:true,
|
||||||
label:RED._("node-red:csv.label.separator"),
|
label:RED._("node-red:csv.label.separator"),
|
||||||
@ -83,7 +97,7 @@
|
|||||||
hdrin: {value:""},
|
hdrin: {value:""},
|
||||||
hdrout: {value:"none"},
|
hdrout: {value:"none"},
|
||||||
multi: {value:"one",required:true},
|
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:""},
|
temp: {value:""},
|
||||||
skip: {value:"0"},
|
skip: {value:"0"},
|
||||||
strings: {value:true},
|
strings: {value:true},
|
||||||
@ -123,6 +137,27 @@
|
|||||||
$("#node-input-sep").hide();
|
$("#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>
|
</script>
|
||||||
|
@ -15,322 +15,674 @@
|
|||||||
**/
|
**/
|
||||||
|
|
||||||
module.exports = function(RED) {
|
module.exports = function(RED) {
|
||||||
|
const csv = require('./lib/csv')
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
function CSVNode(n) {
|
function CSVNode(n) {
|
||||||
RED.nodes.createNode(this,n);
|
RED.nodes.createNode(this,n)
|
||||||
this.template = (n.temp || "");
|
const node = this
|
||||||
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
|
const RFC4180Mode = n.spec === 'rfc'
|
||||||
this.quo = '"';
|
const legacyMode = !RFC4180Mode
|
||||||
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
|
node.status({}) // clear status
|
||||||
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;
|
|
||||||
|
|
||||||
this.on("input", function(msg, send, done) {
|
if (legacyMode) {
|
||||||
if (msg.hasOwnProperty("reset")) {
|
this.template = (n.temp || "");
|
||||||
node.hdrSent = false;
|
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")) {
|
var template = clean(node.template,',');
|
||||||
if (typeof msg.payload == "object") { // convert object to CSV string
|
var notemplate = template.length === 1 && template[0] === '';
|
||||||
try {
|
node.hdrSent = false;
|
||||||
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
|
||||||
template = clean(node.template);
|
this.on("input", function(msg, send, done) {
|
||||||
}
|
if (msg.hasOwnProperty("reset")) {
|
||||||
const ou = [];
|
node.hdrSent = false;
|
||||||
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
|
}
|
||||||
if (node.hdrout !== "none" && node.hdrSent === false) {
|
if (msg.hasOwnProperty("payload")) {
|
||||||
if ((template.length === 1) && (template[0] === '')) {
|
if (typeof msg.payload == "object") { // convert object to CSV string
|
||||||
if (msg.hasOwnProperty("columns")) {
|
try {
|
||||||
template = clean(msg.columns || "",",");
|
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
||||||
}
|
template = clean(node.template);
|
||||||
else {
|
|
||||||
template = Object.keys(msg.payload[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
|
const ou = [];
|
||||||
if (node.hdrout === "once") { node.hdrSent = true; }
|
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
|
||||||
}
|
if (node.hdrout !== "none" && node.hdrSent === false) {
|
||||||
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] === '')) {
|
if ((template.length === 1) && (template[0] === '')) {
|
||||||
/* istanbul ignore else */
|
if (msg.hasOwnProperty("columns")) {
|
||||||
if (tmpwarn === true) { // just warn about missing template once
|
template = clean(msg.columns || "",",");
|
||||||
node.warn(RED._("csv.errors.obj_csv"));
|
|
||||||
tmpwarn = false;
|
|
||||||
}
|
}
|
||||||
const row = [];
|
else {
|
||||||
for (var p in msg.payload[0]) {
|
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 */
|
/* 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 */
|
/* istanbul ignore else */
|
||||||
if (typeof msg.payload[s][p] !== "object") {
|
if (msg.payload[s].hasOwnProperty(p)) {
|
||||||
// Fix to honour include null values flag
|
/* istanbul ignore else */
|
||||||
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
|
if (typeof msg.payload[s][p] !== "object") {
|
||||||
var q = "";
|
// Fix to honour include null values flag
|
||||||
if (msg.payload[s][p] !== undefined) {
|
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
|
||||||
q += msg.payload[s][p];
|
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 {
|
else {
|
||||||
const row = [];
|
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
|
||||||
for (var t=0; t < template.length; t++) {
|
f = !f;
|
||||||
if (template[t] === '') {
|
if (line[i-1] === node.quo) {
|
||||||
row.push('');
|
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 {
|
j += 1;
|
||||||
var tt = template[t];
|
// 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",'
|
||||||
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
|
k[j] = line.length - 1 === i ? null : "";
|
||||||
else { tt = '"'+tt+'"'; }
|
}
|
||||||
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
|
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
|
||||||
/* istanbul ignore else */
|
//console.log(j,k,o,k[j]);
|
||||||
if (p === undefined) { p = ""; }
|
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||||
// fix to honour include null values flag
|
if ( template[j] && (template[j] !== "") ) {
|
||||||
//if (p === null && node.include_null_values !== true) { p = "";}
|
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
|
||||||
p = RED.util.ensureString(p);
|
if (line[i-1] === node.sep) k[j] = null;
|
||||||
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||||
p = p.replace(/"/g, '""');
|
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||||
row.push(node.quo + p + node.quo);
|
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];
|
||||||
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||||
row.push(node.quo + p + node.quo);
|
|
||||||
}
|
|
||||||
else { row.push(p); } // otherwise just add
|
|
||||||
}
|
}
|
||||||
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Finished so finalize and send anything left
|
||||||
// join lines, don't forget to add the last new line
|
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
|
||||||
msg.payload = ou.join(node.ret) + node.ret;
|
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||||
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...
|
if ( template[j] && (template[j] !== "") ) {
|
||||||
// got to be a weird csv that has singleton \r \n in it for another reason...
|
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$/,''); }
|
||||||
// Now process the whole file/line
|
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||||
var nocr = (line.match(/[\r\n]/g)||[]).length;
|
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||||
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
|
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||||
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 (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||||
if (line.length - i === 1) { tmp += line[i]; }
|
a.push(o); // add to the array
|
||||||
template = clean(tmp,node.sep);
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
else { tmp += line[i]; }
|
|
||||||
}
|
}
|
||||||
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 (node.multi !== "one") {
|
||||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
msg.payload = a;
|
||||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
if (has_parts && nocr <= 1) {
|
||||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
if (JSON.stringify(o) !== "{}") {
|
||||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
node.store.push(o);
|
||||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
}
|
||||||
}
|
if (msg.parts.index + 1 === msg.parts.count) {
|
||||||
|
msg.payload = node.store;
|
||||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||||
a.push(o); // add to the array
|
delete msg.parts;
|
||||||
}
|
send(msg);
|
||||||
|
node.store = [];
|
||||||
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) {
|
else {
|
||||||
msg.payload = node.store;
|
|
||||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||||
delete msg.parts;
|
send(msg); // finally send the array
|
||||||
send(msg);
|
|
||||||
node.store = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
var len = a.length;
|
||||||
send(msg); // finally send the array
|
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(',');
|
||||||
else {
|
newMessage.payload = a[i];
|
||||||
var len = a.length;
|
if (!has_parts) {
|
||||||
for (var i = 0; i < len; i++) {
|
newMessage.parts = {
|
||||||
var newMessage = RED.util.cloneMessage(msg);
|
id: msg._msgid,
|
||||||
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
index: i,
|
||||||
newMessage.payload = a[i];
|
count: len
|
||||||
if (!has_parts) {
|
};
|
||||||
newMessage.parts = {
|
}
|
||||||
id: msg._msgid,
|
else {
|
||||||
index: i,
|
newMessage.parts.index -= node.skip;
|
||||||
count: len
|
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 {
|
if (has_parts && last && len === 0) {
|
||||||
newMessage.parts.index -= node.skip;
|
send({complete:true});
|
||||||
newMessage.parts.count -= node.skip;
|
}
|
||||||
if (node.hdrin) { // if we removed the header line then shift the counts by 1
|
}
|
||||||
newMessage.parts.index -= 1;
|
node.linecount = 0;
|
||||||
newMessage.parts.count -= 1;
|
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; }
|
stringBuilder.push(templateArrayToColumnString(template, true))
|
||||||
send(newMessage);
|
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")) {
|
||||||
else {
|
node.send(msg); // If no payload and not reset - just pass it on.
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
<option value="html" data-i18n="html.output.html"></option>
|
<option value="html" data-i18n="html.output.html"></option>
|
||||||
<option value="text" data-i18n="html.output.text"></option>
|
<option value="text" data-i18n="html.output.text"></option>
|
||||||
<option value="attr" data-i18n="html.output.attr"></option>
|
<option value="attr" data-i18n="html.output.attr"></option>
|
||||||
|
<option value="compl" data-i18n="html.output.compl"></option>
|
||||||
<!-- <option value="val">return the value from a form element</option> -->
|
<!-- <option value="val">return the value from a form element</option> -->
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -28,6 +29,10 @@
|
|||||||
<label for="node-input-outproperty"> </label>
|
<label for="node-input-outproperty"> </label>
|
||||||
<span data-i18n="html.label.in" style="padding-left:8px; padding-right:2px; vertical-align:-1px;"></span> <input type="text" id="node-input-outproperty" style="width:64%">
|
<span data-i18n="html.label.in" style="padding-left:8px; padding-right:2px; vertical-align:-1px;"></span> <input type="text" id="node-input-outproperty" style="width:64%">
|
||||||
</div>
|
</div>
|
||||||
|
<div id='html-prefix-row' class="form-row" style="display: none;">
|
||||||
|
<label for="node-input-chr" style="width: 230px;"><i class="fa fa-tag"></i> <span data-i18n="html.label.prefix"></span></label>
|
||||||
|
<input type="text" id="node-input-chr" style="text-align:center; width: 40px;" placeholder="_">
|
||||||
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||||
@ -45,7 +50,8 @@
|
|||||||
outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
|
outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
|
||||||
tag: {value:""},
|
tag: {value:""},
|
||||||
ret: {value:"html"},
|
ret: {value:"html"},
|
||||||
as: {value:"single"}
|
as: {value:"single"},
|
||||||
|
chr: { value: "_" }
|
||||||
},
|
},
|
||||||
inputs:1,
|
inputs:1,
|
||||||
outputs:1,
|
outputs:1,
|
||||||
@ -59,6 +65,13 @@
|
|||||||
oneditprepare: function() {
|
oneditprepare: function() {
|
||||||
$("#node-input-property").typedInput({default:'msg',types:['msg']});
|
$("#node-input-property").typedInput({default:'msg',types:['msg']});
|
||||||
$("#node-input-outproperty").typedInput({default:'msg',types:['msg']});
|
$("#node-input-outproperty").typedInput({default:'msg',types:['msg']});
|
||||||
|
$('#node-input-ret').on( 'change', () => {
|
||||||
|
if ( $('#node-input-ret').val() == "compl" ) {
|
||||||
|
$('#html-prefix-row').show()
|
||||||
|
} else {
|
||||||
|
$('#html-prefix-row').hide()
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -25,6 +25,7 @@ module.exports = function(RED) {
|
|||||||
this.tag = n.tag;
|
this.tag = n.tag;
|
||||||
this.ret = n.ret || "html";
|
this.ret = n.ret || "html";
|
||||||
this.as = n.as || "single";
|
this.as = n.as || "single";
|
||||||
|
this.chr = n.chr || "_";
|
||||||
var node = this;
|
var node = this;
|
||||||
this.on("input", function(msg,send,done) {
|
this.on("input", function(msg,send,done) {
|
||||||
var value = RED.util.getMessageProperty(msg,node.property);
|
var value = RED.util.getMessageProperty(msg,node.property);
|
||||||
@ -47,6 +48,11 @@ module.exports = function(RED) {
|
|||||||
if (node.ret === "attr") {
|
if (node.ret === "attr") {
|
||||||
pay2 = Object.assign({},this.attribs);
|
pay2 = Object.assign({},this.attribs);
|
||||||
}
|
}
|
||||||
|
if (node.ret === "compl") {
|
||||||
|
var bse = {}
|
||||||
|
bse[node.chr] = $(this).html().trim()
|
||||||
|
pay2 = Object.assign(bse, this.attribs);
|
||||||
|
}
|
||||||
//if (node.ret === "val") { pay2 = $(this).val(); }
|
//if (node.ret === "val") { pay2 = $(this).val(); }
|
||||||
/* istanbul ignore else */
|
/* istanbul ignore else */
|
||||||
if (pay2) {
|
if (pay2) {
|
||||||
@ -69,6 +75,11 @@ module.exports = function(RED) {
|
|||||||
var attribs = Object.assign({},this.attribs);
|
var attribs = Object.assign({},this.attribs);
|
||||||
pay.push( attribs );
|
pay.push( attribs );
|
||||||
}
|
}
|
||||||
|
if (node.ret === "compl") {
|
||||||
|
var bse = {}
|
||||||
|
bse[node.chr] = $(this).html().trim()
|
||||||
|
pay.push( Object.assign(bse, this.attribs) )
|
||||||
|
}
|
||||||
//if (node.ret === "val") { pay.push( $(this).val() ); }
|
//if (node.ret === "val") { pay.push( $(this).val() ); }
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
|
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">
|
<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"><span data-i18n="[html]split.strBuff"></span></div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label>
|
<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>
|
<label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label>
|
||||||
<input type="text" id="node-input-addname" style="width:70%">
|
<input type="text" id="node-input-addname" style="width:70%">
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
<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]common.label.name">
|
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -57,7 +60,8 @@
|
|||||||
arraySplt: {value:1},
|
arraySplt: {value:1},
|
||||||
arraySpltType: {value:"len"},
|
arraySpltType: {value:"len"},
|
||||||
stream: {value:false},
|
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,
|
inputs:1,
|
||||||
outputs:1,
|
outputs:1,
|
||||||
@ -69,6 +73,10 @@
|
|||||||
return this.name?"node_label_italic":"";
|
return this.name?"node_label_italic":"";
|
||||||
},
|
},
|
||||||
oneditprepare: function() {
|
oneditprepare: function() {
|
||||||
|
if (this.property === undefined) {
|
||||||
|
$("#node-input-property").val("payload");
|
||||||
|
}
|
||||||
|
$("#node-input-property").typedInput({default:'msg',types:['msg']});
|
||||||
$("#node-input-splt").typedInput({
|
$("#node-input-splt").typedInput({
|
||||||
default: 'str',
|
default: 'str',
|
||||||
typeField: $("#node-input-spltType"),
|
typeField: $("#node-input-spltType"),
|
||||||
|
@ -19,13 +19,13 @@ module.exports = function(RED) {
|
|||||||
|
|
||||||
function sendArray(node,msg,array,send) {
|
function sendArray(node,msg,array,send) {
|
||||||
for (var i = 0; i < array.length-1; i++) {
|
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++;
|
msg.parts.index = node.c++;
|
||||||
if (node.stream !== true) { msg.parts.count = array.length; }
|
if (node.stream !== true) { msg.parts.count = array.length; }
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
}
|
}
|
||||||
if (node.stream !== true) {
|
if (node.stream !== true) {
|
||||||
msg.payload = array[i];
|
RED.util.setMessageProperty(msg,node.property,array[i]);
|
||||||
msg.parts.index = node.c++;
|
msg.parts.index = node.c++;
|
||||||
msg.parts.count = array.length;
|
msg.parts.count = array.length;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
@ -40,10 +40,12 @@ module.exports = function(RED) {
|
|||||||
node.stream = n.stream;
|
node.stream = n.stream;
|
||||||
node.spltType = n.spltType || "str";
|
node.spltType = n.spltType || "str";
|
||||||
node.addname = n.addname || "";
|
node.addname = n.addname || "";
|
||||||
|
node.property = n.property||"payload";
|
||||||
try {
|
try {
|
||||||
if (node.spltType === "str") {
|
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");
|
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);
|
var spltArray = JSON.parse(n.splt);
|
||||||
if (Array.isArray(spltArray)) {
|
if (Array.isArray(spltArray)) {
|
||||||
this.splt = Buffer.from(spltArray);
|
this.splt = Buffer.from(spltArray);
|
||||||
@ -51,7 +53,8 @@ module.exports = function(RED) {
|
|||||||
throw new Error("not an array");
|
throw new Error("not an array");
|
||||||
}
|
}
|
||||||
this.spltBuffer = spltArray;
|
this.spltBuffer = spltArray;
|
||||||
} else if (node.spltType === "len") {
|
}
|
||||||
|
else if (node.spltType === "len") {
|
||||||
this.splt = parseInt(n.splt);
|
this.splt = parseInt(n.splt);
|
||||||
if (isNaN(this.splt) || this.splt < 1) {
|
if (isNaN(this.splt) || this.splt < 1) {
|
||||||
throw new Error("invalid split length: "+n.splt);
|
throw new Error("invalid split length: "+n.splt);
|
||||||
@ -69,18 +72,22 @@ module.exports = function(RED) {
|
|||||||
node.buffer = Buffer.from([]);
|
node.buffer = Buffer.from([]);
|
||||||
node.pendingDones = [];
|
node.pendingDones = [];
|
||||||
this.on("input", function(msg, send, done) {
|
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
|
if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack
|
||||||
else { msg.parts = {}; }
|
else { msg.parts = {}; }
|
||||||
msg.parts.id = RED.util.generateId(); // generate a random id
|
msg.parts.id = RED.util.generateId(); // generate a random id
|
||||||
|
if (node.property !== "payload") {
|
||||||
|
msg.parts.property = node.property;
|
||||||
|
}
|
||||||
delete msg._msgid;
|
delete msg._msgid;
|
||||||
if (typeof msg.payload === "string") { // Split String into array
|
if (typeof value === "string") { // Split String into array
|
||||||
msg.payload = (node.remainder || "") + msg.payload;
|
value = (node.remainder || "") + value;
|
||||||
msg.parts.type = "string";
|
msg.parts.type = "string";
|
||||||
if (node.spltType === "len") {
|
if (node.spltType === "len") {
|
||||||
msg.parts.ch = "";
|
msg.parts.ch = "";
|
||||||
msg.parts.len = node.splt;
|
msg.parts.len = node.splt;
|
||||||
var count = msg.payload.length/node.splt;
|
var count = value.length/node.splt;
|
||||||
if (Math.floor(count) !== count) {
|
if (Math.floor(count) !== count) {
|
||||||
count = Math.ceil(count);
|
count = Math.ceil(count);
|
||||||
}
|
}
|
||||||
@ -89,9 +96,9 @@ module.exports = function(RED) {
|
|||||||
node.c = 0;
|
node.c = 0;
|
||||||
}
|
}
|
||||||
var pos = 0;
|
var pos = 0;
|
||||||
var data = msg.payload;
|
var data = value;
|
||||||
for (var i=0; i<count-1; i++) {
|
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++;
|
msg.parts.index = node.c++;
|
||||||
pos += node.splt;
|
pos += node.splt;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
@ -102,7 +109,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
node.remainder = data.substring(pos);
|
node.remainder = data.substring(pos);
|
||||||
if ((node.stream !== true) || (node.remainder.length === node.splt)) {
|
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++;
|
msg.parts.index = node.c++;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
node.pendingDones.forEach(d => d());
|
node.pendingDones.forEach(d => d());
|
||||||
@ -119,47 +126,48 @@ module.exports = function(RED) {
|
|||||||
if (!node.spltBufferString) {
|
if (!node.spltBufferString) {
|
||||||
node.spltBufferString = node.splt.toString();
|
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
|
msg.parts.ch = node.spltBuffer; // pass the split char to other end for rejoin
|
||||||
} else if (node.spltType === "str") {
|
} 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
|
msg.parts.ch = node.splt; // pass the split char to other end for rejoin
|
||||||
}
|
}
|
||||||
sendArray(node,msg,a,send);
|
sendArray(node,msg,a,send);
|
||||||
done();
|
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";
|
msg.parts.type = "array";
|
||||||
var count = msg.payload.length/node.arraySplt;
|
var count = value.length/node.arraySplt;
|
||||||
if (Math.floor(count) !== count) {
|
if (Math.floor(count) !== count) {
|
||||||
count = Math.ceil(count);
|
count = Math.ceil(count);
|
||||||
}
|
}
|
||||||
msg.parts.count = count;
|
msg.parts.count = count;
|
||||||
var pos = 0;
|
var pos = 0;
|
||||||
var data = msg.payload;
|
var data = value;
|
||||||
msg.parts.len = node.arraySplt;
|
msg.parts.len = node.arraySplt;
|
||||||
for (var i=0; i<count; i++) {
|
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) {
|
if (node.arraySplt === 1) {
|
||||||
msg.payload = msg.payload[0];
|
m = m[0];
|
||||||
}
|
}
|
||||||
|
RED.util.setMessageProperty(msg,node.property,m);
|
||||||
msg.parts.index = i;
|
msg.parts.index = i;
|
||||||
pos += node.arraySplt;
|
pos += node.arraySplt;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) {
|
else if ((typeof value === "object") && !Buffer.isBuffer(value)) {
|
||||||
var j = 0;
|
var j = 0;
|
||||||
var l = Object.keys(msg.payload).length;
|
var l = Object.keys(value).length;
|
||||||
var pay = msg.payload;
|
var pay = value;
|
||||||
msg.parts.type = "object";
|
msg.parts.type = "object";
|
||||||
for (var p in pay) {
|
for (var p in pay) {
|
||||||
if (pay.hasOwnProperty(p)) {
|
if (pay.hasOwnProperty(p)) {
|
||||||
msg.payload = pay[p];
|
RED.util.setMessageProperty(msg,node.property,pay[p]);
|
||||||
if (node.addname !== "") {
|
if (node.addname !== "") {
|
||||||
msg[node.addname] = p;
|
RED.util.setMessageProperty(msg,node.addname,p);
|
||||||
}
|
}
|
||||||
msg.parts.key = p;
|
msg.parts.key = p;
|
||||||
msg.parts.index = j;
|
msg.parts.index = j;
|
||||||
@ -170,9 +178,9 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
else if (Buffer.isBuffer(msg.payload)) {
|
else if (Buffer.isBuffer(value)) {
|
||||||
var len = node.buffer.length + msg.payload.length;
|
var len = node.buffer.length + value.length;
|
||||||
var buff = Buffer.concat([node.buffer, msg.payload], len);
|
var buff = Buffer.concat([node.buffer, value], len);
|
||||||
msg.parts.type = "buffer";
|
msg.parts.type = "buffer";
|
||||||
if (node.spltType === "len") {
|
if (node.spltType === "len") {
|
||||||
var count = buff.length/node.splt;
|
var count = buff.length/node.splt;
|
||||||
@ -186,7 +194,7 @@ module.exports = function(RED) {
|
|||||||
var pos = 0;
|
var pos = 0;
|
||||||
msg.parts.len = node.splt;
|
msg.parts.len = node.splt;
|
||||||
for (var i=0; i<count-1; i++) {
|
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++;
|
msg.parts.index = node.c++;
|
||||||
pos += node.splt;
|
pos += node.splt;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
@ -197,7 +205,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
node.buffer = buff.slice(pos);
|
node.buffer = buff.slice(pos);
|
||||||
if ((node.stream !== true) || (node.buffer.length === node.splt)) {
|
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++;
|
msg.parts.index = node.c++;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
node.pendingDones.forEach(d => d());
|
node.pendingDones.forEach(d => d());
|
||||||
@ -230,7 +238,7 @@ module.exports = function(RED) {
|
|||||||
var i = 0, p = 0;
|
var i = 0, p = 0;
|
||||||
pos = buff.indexOf(node.splt);
|
pos = buff.indexOf(node.splt);
|
||||||
while (pos > -1) {
|
while (pos > -1) {
|
||||||
msg.payload = buff.slice(p,pos);
|
RED.util.setMessageProperty(msg,node.property,buff.slice(p,pos));
|
||||||
msg.parts.index = node.c++;
|
msg.parts.index = node.c++;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
i++;
|
i++;
|
||||||
@ -242,7 +250,7 @@ module.exports = function(RED) {
|
|||||||
node.pendingDones = [];
|
node.pendingDones = [];
|
||||||
}
|
}
|
||||||
if ((node.stream !== true) && (p < buff.length)) {
|
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.index = node.c++;
|
||||||
msg.parts.count = node.c++;
|
msg.parts.count = node.c++;
|
||||||
send(RED.util.cloneMessage(msg));
|
send(RED.util.cloneMessage(msg));
|
||||||
@ -298,7 +306,6 @@ module.exports = function(RED) {
|
|||||||
return exp
|
return exp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) {
|
function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) {
|
||||||
var msgInfo = msgInfos.shift();
|
var msgInfo = msgInfos.shift();
|
||||||
exp.assign("I", msgInfo.msg.parts.index);
|
exp.assign("I", msgInfo.msg.parts.index);
|
||||||
@ -330,6 +337,7 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reduceAndSendGroup(node, group, done) {
|
function reduceAndSendGroup(node, group, done) {
|
||||||
var is_right = node.reduce_right;
|
var is_right = node.reduce_right;
|
||||||
var flag = is_right ? -1 : 1;
|
var flag = is_right ? -1 : 1;
|
||||||
@ -515,13 +523,13 @@ module.exports = function(RED) {
|
|||||||
if (typeof group.joinChar !== 'string') {
|
if (typeof group.joinChar !== 'string') {
|
||||||
groupJoinChar = group.joinChar.toString();
|
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 {
|
else {
|
||||||
if (node.propertyType === 'full') {
|
if (node.propertyType === 'full') {
|
||||||
group.msg = RED.util.cloneMessage(group.msg);
|
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')) {
|
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
|
||||||
group.msg.parts = group.msg.parts.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 (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 (msg.hasOwnProperty("reset")) {
|
||||||
if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) {
|
if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) {
|
||||||
clearTimeout(inflight[partId].timeout);
|
clearTimeout(inflight[partId].timeout);
|
||||||
@ -603,6 +611,15 @@ module.exports = function(RED) {
|
|||||||
return;
|
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 payloadType;
|
||||||
var propertyKey;
|
var propertyKey;
|
||||||
var targetCount;
|
var targetCount;
|
||||||
@ -618,6 +635,7 @@ module.exports = function(RED) {
|
|||||||
propertyKey = msg.parts.key;
|
propertyKey = msg.parts.key;
|
||||||
arrayLen = msg.parts.len;
|
arrayLen = msg.parts.len;
|
||||||
propertyIndex = msg.parts.index;
|
propertyIndex = msg.parts.index;
|
||||||
|
property = RED.util.getMessageProperty(msg,msg.parts.property||"payload");
|
||||||
}
|
}
|
||||||
else if (node.mode === 'reduce') {
|
else if (node.mode === 'reduce') {
|
||||||
return processReduceMessageQueue({msg, send, done});
|
return processReduceMessageQueue({msg, send, done});
|
||||||
@ -719,6 +737,8 @@ module.exports = function(RED) {
|
|||||||
completeSend(partId)
|
completeSend(partId)
|
||||||
}, node.timer)
|
}, node.timer)
|
||||||
}
|
}
|
||||||
|
if (node.mode === "auto") { inflight[partId].prop = msg.parts.property; }
|
||||||
|
else { inflight[partId].prop = node.property; }
|
||||||
}
|
}
|
||||||
inflight[partId].dones.push(done);
|
inflight[partId].dones.push(done);
|
||||||
|
|
||||||
|
@ -849,7 +849,13 @@
|
|||||||
"newline": "Newline",
|
"newline": "Newline",
|
||||||
"usestrings": "parse numerical values",
|
"usestrings": "parse numerical values",
|
||||||
"include_empty_strings": "include empty strings",
|
"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": {
|
"placeholder": {
|
||||||
"columns": "comma-separated column names"
|
"columns": "comma-separated column names"
|
||||||
@ -878,6 +884,7 @@
|
|||||||
"once": "send headers once, until msg.reset"
|
"once": "send headers once, until msg.reset"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
"bad_template": "Malformed columns template.",
|
||||||
"csv_js": "This node only handles CSV strings or js objects.",
|
"csv_js": "This node only handles CSV strings or js objects.",
|
||||||
"obj_csv": "No columns template specified for object -> CSV.",
|
"obj_csv": "No columns template specified for object -> CSV.",
|
||||||
"bad_csv": "Malformed CSV data - output probably corrupt."
|
"bad_csv": "Malformed CSV data - output probably corrupt."
|
||||||
@ -887,12 +894,14 @@
|
|||||||
"label": {
|
"label": {
|
||||||
"select": "Selector",
|
"select": "Selector",
|
||||||
"output": "Output",
|
"output": "Output",
|
||||||
"in": "in"
|
"in": "in",
|
||||||
|
"prefix": "Property name for HTML content"
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
"html": "the html content of the elements",
|
"html": "the html content of the elements",
|
||||||
"text": "only the text content of the elements",
|
"text": "only the text content of the elements",
|
||||||
"attr": "an object of any attributes of the elements"
|
"attr": "an object of any attributes of the elements",
|
||||||
|
"compl": "an object of any attributes of the elements and html contents"
|
||||||
},
|
},
|
||||||
"format": {
|
"format": {
|
||||||
"single": "as a single message containing an array",
|
"single": "as a single message containing an array",
|
||||||
@ -1001,7 +1010,7 @@
|
|||||||
"tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process."
|
"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": "Split",
|
||||||
"intro": "Split <code>msg.payload</code> based on type:",
|
"intro": "Split <code>msg.payload</code> based on type:",
|
||||||
"object": "<b>Object</b>",
|
"object": "<b>Object</b>",
|
||||||
"objectSend": "Send a message for each key/value pair",
|
"objectSend": "Send a message for each key/value pair",
|
||||||
|
@ -36,7 +36,9 @@
|
|||||||
</dl>
|
</dl>
|
||||||
<h3>Details</h3>
|
<h3>Details</h3>
|
||||||
<p>The column template can contain an ordered list of column names. When converting CSV to an object, the column names
|
<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>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
|
<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
|
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 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>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> 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>
|
</script>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<dt class="optional">template <span class="property-type">string</span></dt>
|
<dt class="optional">template <span class="property-type">string</span></dt>
|
||||||
<dd>由<code>msg.payload</code>填充的模板。如果未在编辑面板中配置,则可以将设为msg的属性。</dd>
|
<dd>由<code>msg.payload</code>填充的模板。如果未在编辑面板中配置,则可以将设为msg的属性。</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h3>Outputs</h3>
|
<h3>输出</h3>
|
||||||
<dl class="message-properties">
|
<dl class="message-properties">
|
||||||
<dt>msg <span class="property-type">object</span></dt>
|
<dt>msg <span class="property-type">object</span></dt>
|
||||||
<dd>由来自传入msg的属性来填充已配置的模板后输出的带有属性的msg。</dd>
|
<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>默认情况下使用<i><a href="http://mustache.github.io/mustache.5.html" target="_blank">mustache</a></i>格式。如有需要也可以切换其他格式。</p>
|
||||||
<p>例如:
|
<p>例如:
|
||||||
<pre>Hello {{payload.name}}. Today is {{date}}</pre>
|
<pre>Hello {{payload.name}}. Today is {{date}}</pre>
|
||||||
<p>receives a message containing:
|
<p>接收一条消息,其中包含:
|
||||||
<pre>{
|
<pre>{
|
||||||
date: "Monday",
|
date: "Monday",
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -36,7 +36,7 @@ async function getFlowsFromPath(path) {
|
|||||||
promises.push(getFlowsFromPath(fullPath));
|
promises.push(getFlowsFromPath(fullPath));
|
||||||
} else if (/\.json$/.test(file)){
|
} else if (/\.json$/.test(file)){
|
||||||
validFiles.push(file);
|
validFiles.push(file);
|
||||||
promises.push(Promise.resolve(file.split(".")[0]))
|
promises.push(Promise.resolve(file.replace(/\.json$/, '')))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -485,7 +485,7 @@ class Flow {
|
|||||||
}
|
}
|
||||||
if (!key.startsWith("$parent.")) {
|
if (!key.startsWith("$parent.")) {
|
||||||
if (this._env.hasOwnProperty(key)) {
|
if (this._env.hasOwnProperty(key)) {
|
||||||
return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
|
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
key = key.substring(8);
|
key = key.substring(8);
|
||||||
|
@ -41,7 +41,7 @@ class Group {
|
|||||||
}
|
}
|
||||||
if (!key.startsWith("$parent.")) {
|
if (!key.startsWith("$parent.")) {
|
||||||
if (this._env.hasOwnProperty(key)) {
|
if (this._env.hasOwnProperty(key)) {
|
||||||
return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
|
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
key = key.substring(8);
|
key = key.substring(8);
|
||||||
|
@ -376,7 +376,7 @@ class Subflow extends Flow {
|
|||||||
}
|
}
|
||||||
if (!key.startsWith("$parent.")) {
|
if (!key.startsWith("$parent.")) {
|
||||||
if (this._env.hasOwnProperty(key)) {
|
if (this._env.hasOwnProperty(key)) {
|
||||||
return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
|
return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
key = key.substring(8);
|
key = key.substring(8);
|
||||||
|
@ -77,7 +77,7 @@ var storageModuleInterface = {
|
|||||||
flows: flows,
|
flows: flows,
|
||||||
credentials: creds
|
credentials: creds
|
||||||
};
|
};
|
||||||
result.rev = crypto.createHash('md5').update(JSON.stringify(result.flows)).digest("hex");
|
result.rev = crypto.createHash('sha256').update(JSON.stringify(result.flows)).digest("hex");
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@ -95,7 +95,7 @@ var storageModuleInterface = {
|
|||||||
|
|
||||||
return credentialSavePromise.then(function() {
|
return credentialSavePromise.then(function() {
|
||||||
return storageModule.saveFlows(flows, user).then(function() {
|
return storageModule.saveFlows(flows, user).then(function() {
|
||||||
return crypto.createHash('md5').update(JSON.stringify(config.flows)).digest("hex");
|
return crypto.createHash('sha256').update(JSON.stringify(config.flows)).digest("hex");
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
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') {
|
} else if (type === 're') {
|
||||||
result = new RegExp(value);
|
result = new RegExp(value);
|
||||||
} else if (type === 'date') {
|
} 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') {
|
} else if (type === 'bin') {
|
||||||
var data = JSON.parse(value);
|
var data = JSON.parse(value);
|
||||||
if (Array.isArray(data) || (typeof(data) === "string")) {
|
if (Array.isArray(data) || (typeof(data) === "string")) {
|
||||||
@ -769,12 +777,15 @@ function evaluateJSONataExpression(expr,msg,callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn('Deprecated API warning: Calls to RED.util.evaluateJSONataExpression must include a callback. '+
|
const error = new Error('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 '+
|
throw error
|
||||||
'and check for an update on npm. If none is available, please notify the node author.')
|
|
||||||
log.warn(new Error().stack)
|
|
||||||
}
|
}
|
||||||
return expr.evaluate(context, bindings, callback);
|
|
||||||
|
expr.evaluate(context, bindings).then(result => {
|
||||||
|
callback(null, result)
|
||||||
|
}).catch(err => {
|
||||||
|
callback(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"fs-extra": "11.1.1",
|
"fs-extra": "11.1.1",
|
||||||
"i18next": "21.10.0",
|
"i18next": "21.10.0",
|
||||||
"json-stringify-safe": "5.0.1",
|
"json-stringify-safe": "5.0.1",
|
||||||
"jsonata": "1.8.6",
|
"jsonata": "2.0.4",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"moment": "2.29.4",
|
"moment": "2.29.4",
|
||||||
"moment-timezone": "0.5.43"
|
"moment-timezone": "0.5.43"
|
||||||
|
8
packages/node_modules/node-red/red.js
vendored
8
packages/node_modules/node-red/red.js
vendored
@ -415,9 +415,15 @@ httpsPromise.then(function(startupHttps) {
|
|||||||
if (settings.httpAdminRoot !== false) {
|
if (settings.httpAdminRoot !== false) {
|
||||||
app.use(settings.httpAdminRoot,RED.httpAdmin);
|
app.use(settings.httpAdminRoot,RED.httpAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
|
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) {
|
if (settings.httpNodeRoot !== false) {
|
||||||
app.use(settings.httpNodeRoot,RED.httpNode);
|
app.use(settings.httpNodeRoot,RED.httpNode);
|
||||||
}
|
}
|
||||||
|
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) {
|
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"},
|
var flow = [{id:"sn1", type:"split", wires:[["sn2"]], arraySplt:3, arraySpltType:"len"},
|
||||||
{id:"sn2", type:"helper"}];
|
{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) {
|
it('should split an object into pieces and overwrite their topics', function(done) {
|
||||||
var flow = [{id:"sn1", type:"split", addname:"topic", wires:[["sn2"]]},
|
var flow = [{id:"sn1", type:"split", addname:"topic", wires:[["sn2"]]},
|
||||||
{id:"sn2", type:"helper"}];
|
{id:"sn2", type:"helper"}];
|
||||||
@ -516,6 +562,7 @@ describe('JOIN node', function() {
|
|||||||
n1.receive({payload:{a:1}});
|
n1.receive({payload:{a:1}});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should join things into an array ignoring msg.parts.index in manual mode', function(done) {
|
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"},
|
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",",mode:"custom"},
|
||||||
{id:"n2", type:"helper"}];
|
{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) {
|
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"},
|
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:2, build:"buffer", joinerType:"bin", joiner:"", mode:"custom"},
|
||||||
{id:"n2", type:"helper"}];
|
{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) {
|
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:""},
|
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:6, build:"merged", mode:"custom", propertyType:"full", property:""},
|
||||||
{id:"n2", type:"helper"}];
|
{id:"n2", type:"helper"}];
|
||||||
|
@ -33,16 +33,15 @@ describe("library api", function() {
|
|||||||
should.not.exist(library.getExampleFlowPath('foo','bar'));
|
should.not.exist(library.getExampleFlowPath('foo','bar'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a valid example path', function(done) {
|
it('returns valid example paths', function(done) {
|
||||||
library.init();
|
library.init();
|
||||||
library.addExamplesDir("test-module",path.resolve(__dirname+'/resources/examples')).then(function() {
|
library.addExamplesDir("test-module",path.resolve(__dirname+'/resources/examples')).then(function() {
|
||||||
try {
|
try {
|
||||||
var flows = library.getExampleFlows();
|
var flows = library.getExampleFlows();
|
||||||
flows.should.deepEqual({"test-module":{"f":["one"]}});
|
flows.should.deepEqual({"test-module":{"f":["1.2.3","one"]}});
|
||||||
|
|
||||||
var examplePath = library.getExampleFlowPath('test-module','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');
|
library.removeExamplesDir('test-module');
|
||||||
|
|
||||||
@ -57,6 +56,5 @@ describe("library api", function() {
|
|||||||
done(err);
|
done(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
@ -379,10 +379,17 @@ describe("@node-red/util/util", function() {
|
|||||||
result = util.evaluateNodeProperty('','bool');
|
result = util.evaluateNodeProperty('','bool');
|
||||||
result.should.be.false();
|
result.should.be.false();
|
||||||
});
|
});
|
||||||
it('returns date',function() {
|
it('returns date - default format',function() {
|
||||||
var result = util.evaluateNodeProperty('','date');
|
var result = util.evaluateNodeProperty('','date');
|
||||||
(Date.now() - result).should.be.approximately(0,50);
|
(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 () {
|
it('returns bin', function () {
|
||||||
var result = util.evaluateNodeProperty('[1, 2]','bin');
|
var result = util.evaluateNodeProperty('[1, 2]','bin');
|
||||||
result[0].should.eql(1);
|
result[0].should.eql(1);
|
||||||
@ -441,9 +448,16 @@ describe("@node-red/util/util", function() {
|
|||||||
},{});
|
},{});
|
||||||
result.should.eql("123");
|
result.should.eql("123");
|
||||||
});
|
});
|
||||||
it('returns jsonata result', function () {
|
it('returns jsonata result', function (done) {
|
||||||
var result = util.evaluateNodeProperty('$abs(-1)','jsonata',{},{});
|
util.evaluateNodeProperty('$abs(-1)','jsonata',{},{}, (err, result) => {
|
||||||
result.should.eql(1);
|
try {
|
||||||
|
result.should.eql(1);
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('returns null', function() {
|
it('returns null', function() {
|
||||||
var result = util.evaluateNodeProperty(null,'null');
|
var result = util.evaluateNodeProperty(null,'null');
|
||||||
@ -601,51 +615,105 @@ describe("@node-red/util/util", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('evaluateJSONataExpression', function() {
|
describe('evaluateJSONataExpression', function() {
|
||||||
it('evaluates an expression', function() {
|
it('evaluates an expression', function(done) {
|
||||||
var expr = util.prepareJSONataExpression('payload',{});
|
var expr = util.prepareJSONataExpression('payload',{});
|
||||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||||
result.should.eql("hello");
|
try {
|
||||||
|
result.should.eql("hello");
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('evaluates a legacyMode expression', function() {
|
it('evaluates a legacyMode expression', function() {
|
||||||
var expr = util.prepareJSONataExpression('msg.payload',{});
|
var expr = util.prepareJSONataExpression('msg.payload',{});
|
||||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||||
result.should.eql("hello");
|
try {
|
||||||
|
result.should.eql("hello");
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('accesses flow context from an expression', function() {
|
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 expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
|
||||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||||
result.should.eql("bar");
|
try {
|
||||||
|
result.should.eql("bar");
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('accesses undefined environment variable from an expression', function() {
|
it('accesses undefined environment variable from an expression', function() {
|
||||||
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
||||||
var result = util.evaluateJSONataExpression(expr,{});
|
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||||
result.should.eql('');
|
try {
|
||||||
});
|
result.should.eql("");
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
it('accesses environment variable from an expression', function() {
|
it('accesses environment variable from an expression', function() {
|
||||||
process.env.UTIL_ENV = 'foo';
|
process.env.UTIL_ENV = 'foo';
|
||||||
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
||||||
var result = util.evaluateJSONataExpression(expr,{});
|
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||||
result.should.eql('foo');
|
try {
|
||||||
});
|
result.should.eql("foo");
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
it('accesses moment from an expression', function() {
|
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 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,{});
|
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||||
result.should.eql('2020-07-03');
|
try {
|
||||||
|
result.should.eql("2020-07-03");
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('accesses moment-timezone from an expression', function() {
|
it('accesses moment-timezone from an expression', function() {
|
||||||
var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{});
|
var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{});
|
||||||
var result = util.evaluateJSONataExpression(expr,{});
|
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||||
result.should.eql('2013-11-18T19:55:00+08:00');
|
try {
|
||||||
|
result.should.eql("2013-11-18T19:55:00+08:00");
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('handles non-existant flow context variable', function() {
|
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 expr = util.prepareJSONataExpression('$flowContext("nonExistant")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
|
||||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||||
should.not.exist(result);
|
try {
|
||||||
});
|
should.not.exist(result);
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
it('handles non-existant global context variable', function() {
|
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 expr = util.prepareJSONataExpression('$globalContext("nonExistant")',{context:function() { return {global:{get: function(key) { return {'foo':'bar'}[key]}}}}});
|
||||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||||
should.not.exist(result);
|
try {
|
||||||
|
should.not.exist(result);
|
||||||
|
done()
|
||||||
|
} catch (error) {
|
||||||
|
done(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('handles async flow context access', function(done) {
|
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)}}}}});
|
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,store,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user