Merge branch 'dev' into config-in-subflow

This commit is contained in:
Nick O'Leary 2024-03-13 17:08:23 +00:00 committed by GitHub
commit 3e2508c740
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 3006 additions and 588 deletions

View File

@ -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
Runtime

View File

@ -54,7 +54,7 @@
"is-utf8": "0.2.1",
"js-yaml": "4.1.0",
"json-stringify-safe": "5.0.1",
"jsonata": "1.8.6",
"jsonata": "2.0.4",
"lodash.clonedeep": "^4.5.0",
"media-typer": "1.1.0",
"memorystore": "1.6.7",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -99,7 +99,7 @@ module.exports = {
// settings.instanceId is set asynchronously to the editor-api
// being initiaised. So we defer calculating the cacheBuster hash
// until the first load of the editor
cacheBuster = crypto.createHash('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;

View File

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

View File

@ -926,6 +926,12 @@
"env": "env variable",
"cred": "credential",
"conf-types": "config node"
},
"date": {
"format": {
"timestamp": "milliseconds since epoch",
"object": "JavaScript Date Object"
}
}
},
"editableList": {

View File

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

View File

@ -408,7 +408,25 @@
}
},
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
date: {
value:"date",
label:"timestamp",
icon:"fa fa-clock-o",
options:[
{
label: 'milliseconds since epoch',
value: ''
},
{
label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
value: 'iso'
},
{
label: 'JavaScript Date Object',
value: 'object'
}
]
},
jsonata: {
value: "jsonata",
label: "expression",
@ -709,6 +727,10 @@
allOptions.flow.options = contextStoreOptions;
allOptions.global.options = contextStoreOptions;
}
// Translate timestamp options
allOptions.date.options.forEach(opt => {
opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
})
}
nlsd = true;
var that = this;

View File

@ -483,6 +483,16 @@ RED.utils = (function() {
$('<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') {
e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);
@ -589,6 +599,7 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}
@ -619,6 +630,7 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}
@ -675,6 +687,7 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}
@ -906,7 +919,10 @@ RED.utils = (function() {
* @returns true if valid, String if invalid
*/
function validateTypedProperty(propertyValue, propertyType, opt) {
if (propertyValue && /^\${[^}]+}$/.test(propertyValue)) {
// Allow ${ENV_VAR} value
return true
}
let error
if (propertyType === 'json') {
try {

View File

@ -4156,7 +4156,7 @@ RED.view = (function() {
}
var width = img.width * scaleFactor;
if (width > 20) {
scalefactor *= 20/width;
scaleFactor *= 20/width;
width = 20;
}
var height = img.height * scaleFactor;

View File

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

View File

@ -38,7 +38,7 @@ body {
}
#red-ui-main-container {
position: absolute;
top:40px; left:0; bottom: 0; right:0;
top: var(--red-ui-header-height); left:0; bottom: 0; right:0;
overflow:hidden;
}

View File

@ -259,7 +259,8 @@ $deploy-button-background-disabled-hover: #555;
$header-background: #000;
$header-button-background-active: #121212;
$header-menu-color: #C7C7C7;
$header-accent: #d41313;
$header-menu-color: #eee;
$header-menu-color-disabled: #666;
$header-menu-heading-color: #fff;
$header-menu-sublabel-color: #aeaeae;

View File

@ -23,16 +23,20 @@
top: 0;
left: 0;
width: 100%;
height: 40px;
height: var(--red-ui-header-height);
background: var(--red-ui-header-background);
box-sizing: border-box;
padding: 0px 0px 0px 20px;
color: var(--red-ui-header-menu-color);
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 {
float: left;
margin-top: 5px;
font-size: 30px;
line-height: 30px;
text-decoration: none;
@ -42,7 +46,7 @@
vertical-align: middle;
font-size: 16px !important;
&:not(:first-child) {
margin-left: 5px;
margin-left: 8px;
}
}
img {

View 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;

View File

@ -15,4 +15,5 @@
**/
@import "colors";
@import "sizes";
@import "variables";

View File

@ -15,6 +15,7 @@
**/
@import "colors";
@import "sizes";
@import "variables";
@import "mixins";

View File

@ -16,6 +16,9 @@
--red-ui-shadow: #{$shadow};
// Header Height
--red-ui-header-height: #{$header-height};
// Main body text
--red-ui-primary-text-color: #{$primary-text-color};
// UI control label text
@ -240,6 +243,7 @@
--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-menu-color: #{$header-menu-color};
--red-ui-header-menu-color-disabled: #{$header-menu-color-disabled};

View File

@ -227,34 +227,42 @@
name: {value:""},
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) {
if (!v || v.length === 0) { return true }
const errors = []
for (var i=0;i<v.length;i++) {
if (/^\${[^}]+}$/.test(v[i].v)) {
// Allow ${ENV_VAR} value
continue
}
if (/msg|flow|global/.test(v[i].vt)) {
if (!RED.utils.validatePropertyExpression(v[i].v)) {
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") {
try{ jsonata(v[i].v); }
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") {
try{ JSON.parse(v[i].v); }
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"){
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;
}
},
repeat: {
value:"", validate: function(v, opt) {
if ((v === "") ||
(RED.validators.number(v) &&
(RED.validators.number()(v) &&
(v >= 0) && (v <= 2147483))) {
return true;
}
@ -263,7 +271,7 @@
},
crontab: {value:""},
once: {value:false},
onceDelay: {value:0.1},
onceDelay: {value:0.1, validate: RED.validators.number(true)},
topic: {value:""},
payload: {value:"", validate: RED.validators.typedInput("payloadType", false) },
payloadType: {value:"date"},

View File

@ -512,7 +512,8 @@ RED.debug = (function() {
hideKey: false,
path: path,
sourceId: sourceNode&&sourceNode.id,
rootPath: path
rootPath: path,
nodeSelector: config.messageSourceClick,
});
// Do this in a separate step so the element functions aren't stripped
debugMessage.appendTo(el);

View File

@ -117,7 +117,7 @@ module.exports = function(RED) {
});
return
} else if (rule.tot === 'date') {
value = Date.now();
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
} else if (rule.tot === 'jsonata') {
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
if (err) {

View File

@ -582,6 +582,7 @@ module.exports = function(RED) {
const cc = Object.keys(clients).length;
node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
if (!msg.hasOwnProperty("payload")) { return; }
}
// Store client information independently

View File

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

View File

@ -15,9 +15,18 @@
**/
module.exports = function(RED) {
const csv = require('./lib/csv')
"use strict";
function CSVNode(n) {
RED.nodes.createNode(this,n);
RED.nodes.createNode(this,n)
const node = this
const RFC4180Mode = n.spec === 'rfc'
const legacyMode = !RFC4180Mode
node.status({}) // clear status
if (legacyMode) {
this.template = (n.temp || "");
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
this.quo = '"';
@ -37,7 +46,7 @@ module.exports = function(RED) {
if (this.hdrout === false) { this.hdrout = "none"; }
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
var node = this;
// 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
@ -163,7 +172,9 @@ module.exports = function(RED) {
// 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); }
if (msg.payload !== '') {
send(msg);
}
done();
}
catch(e) { done(e); }
@ -332,5 +343,346 @@ module.exports = function(RED) {
}
});
}
RED.nodes.registerType("csv",CSVNode);
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]) || ['']
}
}
stringBuilder.push(templateArrayToColumnString(template, true))
if (sendHeadersOnce) { node.hdrSent = 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)
}
}
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 {
if (!msg.hasOwnProperty("reset")) {
node.send(msg); // If no payload and not reset - just pass it on.
}
done()
}
})
}
}
RED.nodes.registerType("csv",CSVNode)
}

View File

@ -14,6 +14,7 @@
<option value="html" data-i18n="html.output.html"></option>
<option value="text" data-i18n="html.output.text"></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> -->
</select>
</div>
@ -28,6 +29,10 @@
<label for="node-input-outproperty">&nbsp;</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%">
</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/>
<div class="form-row">
<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 }) },
tag: {value:""},
ret: {value:"html"},
as: {value:"single"}
as: {value:"single"},
chr: { value: "_" }
},
inputs:1,
outputs:1,
@ -59,6 +65,13 @@
oneditprepare: function() {
$("#node-input-property").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>

View File

@ -25,6 +25,7 @@ module.exports = function(RED) {
this.tag = n.tag;
this.ret = n.ret || "html";
this.as = n.as || "single";
this.chr = n.chr || "_";
var node = this;
this.on("input", function(msg,send,done) {
var value = RED.util.getMessageProperty(msg,node.property);
@ -47,6 +48,11 @@ module.exports = function(RED) {
if (node.ret === "attr") {
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(); }
/* istanbul ignore else */
if (pay2) {
@ -69,6 +75,11 @@ module.exports = function(RED) {
var attribs = Object.assign({},this.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() ); }
}
index++;

View File

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

View File

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

View File

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

View File

@ -849,7 +849,13 @@
"newline": "Newline",
"usestrings": "parse numerical values",
"include_empty_strings": "include empty strings",
"include_null_values": "include null values"
"include_null_values": "include null values",
"spec": "Parser"
},
"spec": {
"rfc": "RFC4180",
"legacy": "Legacy",
"legacy_warning": "Legacy mode will be removed in a future release."
},
"placeholder": {
"columns": "comma-separated column names"
@ -878,6 +884,7 @@
"once": "send headers once, until msg.reset"
},
"errors": {
"bad_template": "Malformed columns template.",
"csv_js": "This node only handles CSV strings or js objects.",
"obj_csv": "No columns template specified for object -> CSV.",
"bad_csv": "Malformed CSV data - output probably corrupt."
@ -887,12 +894,14 @@
"label": {
"select": "Selector",
"output": "Output",
"in": "in"
"in": "in",
"prefix": "Property name for HTML content"
},
"output": {
"html": "the html 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": {
"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."
},
"split": {
"split": "split",
"split": "Split",
"intro": "Split <code>msg.payload</code> based on type:",
"object": "<b>Object</b>",
"objectSend": "Send a message for each key/value pair",

View File

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

View File

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

View File

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

View File

@ -485,7 +485,7 @@ class Flow {
}
if (!key.startsWith("$parent.")) {
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 {
key = key.substring(8);

View File

@ -41,7 +41,7 @@ class Group {
}
if (!key.startsWith("$parent.")) {
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 {
key = key.substring(8);

View File

@ -376,7 +376,7 @@ class Subflow extends Flow {
}
if (!key.startsWith("$parent.")) {
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 {
key = key.substring(8);

View File

@ -77,7 +77,7 @@ var storageModuleInterface = {
flows: flows,
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;
})
});
@ -95,7 +95,7 @@ var storageModuleInterface = {
return credentialSavePromise.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");
})
});
},

View File

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

View File

@ -18,7 +18,7 @@
"fs-extra": "11.1.1",
"i18next": "21.10.0",
"json-stringify-safe": "5.0.1",
"jsonata": "1.8.6",
"jsonata": "2.0.4",
"lodash.clonedeep": "^4.5.0",
"moment": "2.29.4",
"moment-timezone": "0.5.43"

View File

@ -415,9 +415,15 @@ httpsPromise.then(function(startupHttps) {
if (settings.httpAdminRoot !== false) {
app.use(settings.httpAdminRoot,RED.httpAdmin);
}
if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass));
if (typeof settings.httpNodeAuth === "function" || Array.isArray(settings.httpNodeAuth)) {
app.use(settings.httpNodeRoot, settings.httpNodeAuth);
} else {
app.use(settings.httpNodeRoot, basicAuthMiddleware(settings.httpNodeAuth.user, settings.httpNodeAuth.pass));
}
}
if (settings.httpNodeRoot !== false) {
app.use(settings.httpNodeRoot,RED.httpNode);
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -33,16 +33,15 @@ describe("library api", function() {
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.addExamplesDir("test-module",path.resolve(__dirname+'/resources/examples')).then(function() {
try {
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');
examplePath.should.eql(path.resolve(__dirname+'/resources/examples/one.json'))
examplePath.should.eql(path.resolve(__dirname+'/resources/examples/one.json'));
library.removeExamplesDir('test-module');
@ -57,6 +56,5 @@ describe("library api", function() {
done(err);
}
});
})
});
});

View File

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