mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Compare commits
50 Commits
resyn-dev
...
make-split
Author | SHA1 | Date | |
---|---|---|---|
|
742aa2fa0d | ||
|
ce133c1c04 | ||
|
e4dc1779c3 | ||
|
22b4ab6bb2 | ||
|
2dcff51125 | ||
|
b50e0533eb | ||
|
711545539f | ||
|
a6cbceed28 | ||
|
6802539ccc | ||
|
a5223709ba | ||
|
2291dc6132 | ||
|
b2548c158d | ||
|
5a48d6d4cd | ||
|
7ee2b93b10 | ||
|
cc611a7a02 | ||
|
1a9c34fe40 | ||
|
ff8eb0ec2b | ||
|
f66b48e586 | ||
|
931a2344b4 | ||
|
dd3c75d298 | ||
|
4a4a15de93 | ||
|
a007ab7f2e | ||
|
54e6d60fe5 | ||
|
c2710f4f6f | ||
|
20187b51b1 | ||
|
4be6d57d98 | ||
|
a77f8cc3e9 | ||
|
ea4c0cdbee | ||
|
7197153fd5 | ||
|
b9c1dedab3 | ||
|
918943816f | ||
|
33cf34f7c7 | ||
|
5b5b06cc06 | ||
|
f49f692ffa | ||
|
08c6ea94cb | ||
|
fea1da5542 | ||
|
32e8f4eac6 | ||
|
bfe5a8a986 | ||
|
f2cb5ea44e | ||
|
c7335ed25b | ||
|
5fda57c730 | ||
|
9fd929ac1e | ||
|
eb940d6d57 | ||
|
9091935d77 | ||
|
34e8d2b051 | ||
|
0c2ab13c48 | ||
|
9489953a8f | ||
|
54d4079457 | ||
|
cef3a01042 | ||
|
0c042abcab |
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -12,12 +12,11 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
checks: write # for coverallsapp/github-action to create new checks
|
||||
contents: read # for actions/checkout to fetch code
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16, 18, 20]
|
||||
node-version: [18, 20]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
@@ -29,8 +28,3 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
npm run test
|
||||
# - name: Publish to coveralls.io
|
||||
# if: ${{ matrix.node-version == 16 }}
|
||||
# uses: coverallsapp/github-action@v1.1.2
|
||||
# with:
|
||||
# github-token: ${{ github.token }}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-red",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"description": "Low-code programming for event-driven applications",
|
||||
"homepage": "https://nodered.org",
|
||||
"license": "Apache-2.0",
|
||||
@@ -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",
|
||||
@@ -122,6 +122,6 @@
|
||||
"supertest": "6.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,9 @@ module.exports = {
|
||||
store: req.query['store'],
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
if (req.query['keysOnly'] !== undefined) {
|
||||
opts.keysOnly = true
|
||||
}
|
||||
runtimeAPI.context.getValue(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/editor-api",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@@ -16,8 +16,8 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/util": "3.1.5",
|
||||
"@node-red/editor-client": "3.1.5",
|
||||
"@node-red/util": "4.0.0-dev",
|
||||
"@node-red/editor-client": "4.0.0-dev",
|
||||
"bcryptjs": "2.4.3",
|
||||
"body-parser": "1.20.2",
|
||||
"clone": "2.1.2",
|
||||
|
@@ -925,6 +925,12 @@
|
||||
"jsonata": "expression",
|
||||
"env": "env variable",
|
||||
"cred": "credential"
|
||||
},
|
||||
"date": {
|
||||
"format": {
|
||||
"timestamp": "milliseconds since epoch",
|
||||
"object": "JavaScript Date Object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editableList": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/editor-client",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -54,25 +54,26 @@
|
||||
return icon;
|
||||
}
|
||||
|
||||
var autoComplete = function(options) {
|
||||
function getMatch(value, searchValue) {
|
||||
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
|
||||
const len = idx > -1 ? searchValue.length : 0;
|
||||
return {
|
||||
index: idx,
|
||||
found: idx > -1,
|
||||
pre: value.substring(0,idx),
|
||||
match: value.substring(idx,idx+len),
|
||||
post: value.substring(idx+len),
|
||||
}
|
||||
}
|
||||
function generateSpans(match) {
|
||||
const els = [];
|
||||
if(match.pre) { els.push($('<span/>').text(match.pre)); }
|
||||
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
|
||||
if(match.post) { els.push($('<span/>').text(match.post)); }
|
||||
return els;
|
||||
function getMatch(value, searchValue) {
|
||||
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
|
||||
const len = idx > -1 ? searchValue.length : 0;
|
||||
return {
|
||||
index: idx,
|
||||
found: idx > -1,
|
||||
pre: value.substring(0,idx),
|
||||
match: value.substring(idx,idx+len),
|
||||
post: value.substring(idx+len),
|
||||
}
|
||||
}
|
||||
function generateSpans(match) {
|
||||
const els = [];
|
||||
if(match.pre) { els.push($('<span/>').text(match.pre)); }
|
||||
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
|
||||
if(match.post) { els.push($('<span/>').text(match.post)); }
|
||||
return els;
|
||||
}
|
||||
|
||||
const msgAutoComplete = function(options) {
|
||||
return function(val) {
|
||||
var matches = [];
|
||||
options.forEach(opt => {
|
||||
@@ -102,6 +103,197 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getEnvVars (obj, envVars = {}) {
|
||||
contextKnownKeys.env = contextKnownKeys.env || {}
|
||||
if (contextKnownKeys.env[obj.id]) {
|
||||
return contextKnownKeys.env[obj.id]
|
||||
}
|
||||
let parent
|
||||
if (obj.type === 'tab' || obj.type === 'subflow') {
|
||||
RED.nodes.eachConfig(function (conf) {
|
||||
if (conf.type === "global-config") {
|
||||
parent = conf;
|
||||
}
|
||||
})
|
||||
} else if (obj.g) {
|
||||
parent = RED.nodes.group(obj.g)
|
||||
} else if (obj.z) {
|
||||
parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z)
|
||||
}
|
||||
if (parent) {
|
||||
getEnvVars(parent, envVars)
|
||||
}
|
||||
if (obj.env) {
|
||||
obj.env.forEach(env => {
|
||||
envVars[env.name] = obj
|
||||
})
|
||||
}
|
||||
contextKnownKeys.env[obj.id] = envVars
|
||||
return envVars
|
||||
}
|
||||
|
||||
const envAutoComplete = function (val) {
|
||||
const editStack = RED.editor.getEditStack()
|
||||
if (editStack.length === 0) {
|
||||
done([])
|
||||
return
|
||||
}
|
||||
const editingNode = editStack.pop()
|
||||
if (!editingNode) {
|
||||
return []
|
||||
}
|
||||
const envVarsMap = getEnvVars(editingNode)
|
||||
const envVars = Object.keys(envVarsMap)
|
||||
const matches = []
|
||||
const i = val.lastIndexOf('${')
|
||||
let searchKey = val
|
||||
let isSubkey = false
|
||||
if (i > -1) {
|
||||
if (val.lastIndexOf('}') < i) {
|
||||
searchKey = val.substring(i+2)
|
||||
isSubkey = true
|
||||
}
|
||||
}
|
||||
envVars.forEach(v => {
|
||||
let valMatch = getMatch(v, searchKey);
|
||||
if (valMatch.found) {
|
||||
const optSrc = envVarsMap[v]
|
||||
const element = $('<div>',{style: "display: flex"});
|
||||
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
|
||||
valEl.append(generateSpans(valMatch))
|
||||
valEl.appendTo(element)
|
||||
|
||||
if (optSrc) {
|
||||
const optEl = $('<div>').css({ "font-size": "0.8em" });
|
||||
let label
|
||||
if (optSrc.type === 'global-config') {
|
||||
label = RED._('sidebar.context.global')
|
||||
} else if (optSrc.type === 'group') {
|
||||
label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id)
|
||||
} else {
|
||||
label = RED.utils.getNodeLabel(optSrc) || optSrc.id
|
||||
}
|
||||
|
||||
optEl.append(generateSpans({ match: label }));
|
||||
optEl.appendTo(element);
|
||||
}
|
||||
matches.push({
|
||||
value: isSubkey ? val + v + '}' : v,
|
||||
label: element,
|
||||
i: valMatch.index
|
||||
});
|
||||
}
|
||||
})
|
||||
matches.sort(function(A,B){return A.i-B.i})
|
||||
return matches
|
||||
}
|
||||
|
||||
let contextKnownKeys = {}
|
||||
let contextCache = {}
|
||||
if (RED.events) {
|
||||
RED.events.on("editor:close", function () {
|
||||
contextCache = {}
|
||||
contextKnownKeys = {}
|
||||
});
|
||||
}
|
||||
|
||||
const contextAutoComplete = function() {
|
||||
const that = this
|
||||
const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
|
||||
contextKnownKeys[scope] = contextKnownKeys[scope] || {}
|
||||
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
|
||||
if (searchKey.length > 0) {
|
||||
try {
|
||||
RED.utils.normalisePropertyExpression(searchKey)
|
||||
} catch (err) {
|
||||
// Not a valid context key, so don't try looking up
|
||||
done()
|
||||
return
|
||||
}
|
||||
}
|
||||
const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
|
||||
if (contextCache[url]) {
|
||||
// console.log('CACHED', url)
|
||||
done()
|
||||
} else {
|
||||
// console.log('GET', url)
|
||||
$.getJSON(url, function(data) {
|
||||
// console.log(data)
|
||||
contextCache[url] = true
|
||||
const result = data[store] || {}
|
||||
const keys = result.keys || []
|
||||
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
|
||||
keys.forEach(key => {
|
||||
if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
|
||||
contextKnownKeys[scope][store].add(keyPrefix + key)
|
||||
} else {
|
||||
contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
|
||||
}
|
||||
})
|
||||
done()
|
||||
})
|
||||
}
|
||||
}
|
||||
const getContextKeys = function(key, done) {
|
||||
const keyParts = key.split('.')
|
||||
const partialKey = keyParts.pop()
|
||||
let scope = that.propertyType
|
||||
if (scope === 'flow') {
|
||||
// Get the flow id of the node we're editing
|
||||
const editStack = RED.editor.getEditStack()
|
||||
if (editStack.length === 0) {
|
||||
done([])
|
||||
return
|
||||
}
|
||||
const editingNode = editStack.pop()
|
||||
if (editingNode.z) {
|
||||
scope = `${scope}/${editingNode.z}`
|
||||
} else {
|
||||
done([])
|
||||
return
|
||||
}
|
||||
}
|
||||
const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue
|
||||
const searchKey = keyParts.join('.')
|
||||
|
||||
getContextKeysFromRuntime(scope, store, searchKey, function() {
|
||||
if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) {
|
||||
getContextKeysFromRuntime(scope, store, key, function() {
|
||||
done(contextKnownKeys[scope][store])
|
||||
})
|
||||
}
|
||||
done(contextKnownKeys[scope][store])
|
||||
})
|
||||
}
|
||||
|
||||
return function(val, done) {
|
||||
getContextKeys(val, function (keys) {
|
||||
const matches = []
|
||||
keys.forEach(v => {
|
||||
let optVal = v
|
||||
let valMatch = getMatch(optVal, val);
|
||||
if (!valMatch.found && val.length > 0 && val.endsWith('.')) {
|
||||
// Search key ends in '.' - but doesn't match. Check again
|
||||
// with [" at the end instead so we match bracket notation
|
||||
valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
|
||||
}
|
||||
if (valMatch.found) {
|
||||
const element = $('<div>',{style: "display: flex"});
|
||||
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
|
||||
valEl.append(generateSpans(valMatch))
|
||||
valEl.appendTo(element)
|
||||
matches.push({
|
||||
value: optVal,
|
||||
label: element,
|
||||
});
|
||||
}
|
||||
})
|
||||
matches.sort(function(a, b) { return a.value.localeCompare(b.value) });
|
||||
done(matches);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This is a hand-generated list of completions for the core nodes (based on the node help html).
|
||||
var msgCompletions = [
|
||||
{ value: "payload" },
|
||||
@@ -166,20 +358,22 @@
|
||||
{ value: "_session", source: ["websocket out","tcp out"] },
|
||||
]
|
||||
var allOptions = {
|
||||
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
|
||||
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
|
||||
flow: {value:"flow",label:"flow.",hasValue:true,
|
||||
options:[],
|
||||
validate:RED.utils.validatePropertyExpression,
|
||||
parse: contextParse,
|
||||
export: contextExport,
|
||||
valueLabel: contextLabel
|
||||
valueLabel: contextLabel,
|
||||
autoComplete: contextAutoComplete
|
||||
},
|
||||
global: {value:"global",label:"global.",hasValue:true,
|
||||
options:[],
|
||||
validate:RED.utils.validatePropertyExpression,
|
||||
parse: contextParse,
|
||||
export: contextExport,
|
||||
valueLabel: contextLabel
|
||||
valueLabel: contextLabel,
|
||||
autoComplete: contextAutoComplete
|
||||
},
|
||||
str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
|
||||
num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) {
|
||||
@@ -214,7 +408,25 @@
|
||||
}
|
||||
},
|
||||
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
|
||||
date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
|
||||
date: {
|
||||
value:"date",
|
||||
label:"timestamp",
|
||||
icon:"fa fa-clock-o",
|
||||
options:[
|
||||
{
|
||||
label: 'milliseconds since epoch',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
|
||||
value: 'iso'
|
||||
},
|
||||
{
|
||||
label: 'JavaScript Date Object',
|
||||
value: 'object'
|
||||
}
|
||||
]
|
||||
},
|
||||
jsonata: {
|
||||
value: "jsonata",
|
||||
label: "expression",
|
||||
@@ -251,7 +463,8 @@
|
||||
env: {
|
||||
value: "env",
|
||||
label: "env variable",
|
||||
icon: "red/images/typedInput/env.svg"
|
||||
icon: "red/images/typedInput/env.svg",
|
||||
autoComplete: envAutoComplete
|
||||
},
|
||||
node: {
|
||||
value: "node",
|
||||
@@ -427,6 +640,7 @@
|
||||
}
|
||||
|
||||
var nlsd = false;
|
||||
let contextStoreOptions;
|
||||
|
||||
$.widget( "nodered.typedInput", {
|
||||
_create: function() {
|
||||
@@ -438,7 +652,7 @@
|
||||
}
|
||||
}
|
||||
var contextStores = RED.settings.context.stores;
|
||||
var contextOptions = contextStores.map(function(store) {
|
||||
contextStoreOptions = contextStores.map(function(store) {
|
||||
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
|
||||
}).sort(function(A,B) {
|
||||
if (A.value === RED.settings.context.default) {
|
||||
@@ -449,13 +663,17 @@
|
||||
return A.value.localeCompare(B.value);
|
||||
}
|
||||
})
|
||||
if (contextOptions.length < 2) {
|
||||
if (contextStoreOptions.length < 2) {
|
||||
allOptions.flow.options = [];
|
||||
allOptions.global.options = [];
|
||||
} else {
|
||||
allOptions.flow.options = contextOptions;
|
||||
allOptions.global.options = contextOptions;
|
||||
allOptions.flow.options = contextStoreOptions;
|
||||
allOptions.global.options = contextStoreOptions;
|
||||
}
|
||||
// Translate timestamp options
|
||||
allOptions.date.options.forEach(opt => {
|
||||
opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
|
||||
})
|
||||
}
|
||||
nlsd = true;
|
||||
var that = this;
|
||||
@@ -544,7 +762,7 @@
|
||||
that.element.trigger('paste',evt);
|
||||
});
|
||||
this.input.on('keydown', function(evt) {
|
||||
if (that.typeMap[that.propertyType].autoComplete) {
|
||||
if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
|
||||
return
|
||||
}
|
||||
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
|
||||
@@ -967,6 +1185,9 @@
|
||||
// If previousType is !null, then this is a change of the type, rather than the initialisation
|
||||
var previousType = this.typeMap[this.propertyType];
|
||||
previousValue = this.input.val();
|
||||
if (this.input.hasClass('red-ui-autoComplete')) {
|
||||
this.input.autoComplete("destroy");
|
||||
}
|
||||
|
||||
if (previousType && this.typeChanged) {
|
||||
if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
|
||||
@@ -1013,7 +1234,9 @@
|
||||
this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
|
||||
}
|
||||
if (previousType.autoComplete) {
|
||||
this.input.autoComplete("destroy");
|
||||
if (this.input.hasClass('red-ui-autoComplete')) {
|
||||
this.input.autoComplete("destroy");
|
||||
}
|
||||
}
|
||||
}
|
||||
this.propertyType = type;
|
||||
@@ -1141,6 +1364,16 @@
|
||||
} else {
|
||||
this.optionSelectTrigger.hide();
|
||||
}
|
||||
if (opt.autoComplete) {
|
||||
let searchFunction = opt.autoComplete
|
||||
if (searchFunction.length === 0) {
|
||||
searchFunction = opt.autoComplete.call(this)
|
||||
}
|
||||
this.input.autoComplete({
|
||||
search: searchFunction,
|
||||
minLength: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
this.optionMenu = this._createMenu(opt.options,opt,function(v){
|
||||
if (!opt.multiple) {
|
||||
@@ -1183,8 +1416,12 @@
|
||||
this.valueLabelContainer.hide();
|
||||
this.elementDiv.show();
|
||||
if (opt.autoComplete) {
|
||||
let searchFunction = opt.autoComplete
|
||||
if (searchFunction.length === 0) {
|
||||
searchFunction = opt.autoComplete.call(this)
|
||||
}
|
||||
this.input.autoComplete({
|
||||
search: opt.autoComplete,
|
||||
search: searchFunction,
|
||||
minLength: 0
|
||||
})
|
||||
}
|
||||
|
@@ -2087,6 +2087,7 @@ RED.editor = (function() {
|
||||
}
|
||||
},
|
||||
editBuffer: function(options) { showTypeEditor("_buffer", options) },
|
||||
getEditStack: function () { return [...editStack] },
|
||||
buildEditForm: buildEditForm,
|
||||
validateNode: validateNode,
|
||||
updateNodeProperties: updateNodeProperties,
|
||||
|
@@ -6252,6 +6252,10 @@ RED.view = (function() {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (selection.links) {
|
||||
selectedLinks.clear();
|
||||
selection.links.forEach(selectedLinks.add);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateSelection();
|
||||
|
@@ -5,6 +5,7 @@ module.exports = function(RED) {
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
var debuglength = RED.settings.debugMaxLength || 1000;
|
||||
var statuslength = RED.settings.debugStatusLength || 32;
|
||||
var useColors = RED.settings.debugUseColors || false;
|
||||
util.inspect.styles.boolean = "red";
|
||||
const { hasOwnProperty } = Object.prototype;
|
||||
@@ -164,7 +165,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
|
||||
if (st.length > 32) { st = st.substr(0,32) + "..."; }
|
||||
if (st.length > statuslength) { st = st.substr(0,statuslength) + "..."; }
|
||||
|
||||
var newStatus = {fill:fill, shape:shape, text:st};
|
||||
if (JSON.stringify(newStatus) !== node.oldState) { // only send if we have to
|
||||
|
@@ -117,7 +117,7 @@ module.exports = function(RED) {
|
||||
});
|
||||
return
|
||||
} else if (rule.tot === 'date') {
|
||||
value = Date.now();
|
||||
value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node)
|
||||
} else if (rule.tot === 'jsonata') {
|
||||
RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
|
||||
if (err) {
|
||||
@@ -233,7 +233,9 @@ module.exports = function(RED) {
|
||||
// only replace if they match exactly
|
||||
RED.util.setMessageProperty(msg,property,value);
|
||||
} else {
|
||||
current = current.replace(fromRE,value);
|
||||
// if target is boolean then just replace it
|
||||
if (rule.tot === "bool") { current = value; }
|
||||
else { current = current.replace(fromRE,value); }
|
||||
RED.util.setMessageProperty(msg,property,current);
|
||||
}
|
||||
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
|
||||
|
@@ -411,23 +411,33 @@ module.exports = function(RED) {
|
||||
if (msg._session && msg._session.type == "tcp") {
|
||||
var client = connectionPool[msg._session.id];
|
||||
if (client) {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
client.write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
client.write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
client.write(Buffer.from(""+msg.payload));
|
||||
if (msg?.reset === true) {
|
||||
client.destroy();
|
||||
}
|
||||
else {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
client.write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
client.write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
client.write(Buffer.from(""+msg.payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (var i in connectionPool) {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
connectionPool[i].write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
connectionPool[i].write(Buffer.from(""+msg.payload));
|
||||
if (msg?.reset === true) {
|
||||
connectionPool[i].destroy();
|
||||
}
|
||||
else {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
connectionPool[i].write(msg.payload);
|
||||
} else if (typeof msg.payload === "string" && node.base64) {
|
||||
connectionPool[i].write(Buffer.from(msg.payload,'base64'));
|
||||
} else {
|
||||
connectionPool[i].write(Buffer.from(""+msg.payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,13 +557,33 @@ module.exports = function(RED) {
|
||||
|
||||
this.on("input", function(msg, nodeSend, nodeDone) {
|
||||
var i = 0;
|
||||
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
|
||||
if (msg.payload !== undefined && (!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
|
||||
msg.payload = msg.payload.toString();
|
||||
}
|
||||
|
||||
var host = node.server || msg.host;
|
||||
var port = node.port || msg.port;
|
||||
|
||||
if (node.out === "sit" && msg?.reset) {
|
||||
if (msg.reset === true) { // kill all connections
|
||||
for (var cl in clients) {
|
||||
if (clients[cl].hasOwnProperty("client")) {
|
||||
clients[cl].client.destroy();
|
||||
delete clients[cl];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof(msg.reset) === "string" && msg.reset.includes(":")) { // just kill connection host:port
|
||||
if (clients.hasOwnProperty(msg.reset) && clients[msg.reset].hasOwnProperty("client")) {
|
||||
clients[msg.reset].client.destroy();
|
||||
delete clients[msg.reset];
|
||||
}
|
||||
}
|
||||
const cc = Object.keys(clients).length;
|
||||
node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})});
|
||||
if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; }
|
||||
}
|
||||
|
||||
// Store client information independently
|
||||
// the clients object will have:
|
||||
// clients[id].client, clients[id].msg, clients[id].timeout
|
||||
@@ -621,13 +651,16 @@ module.exports = function(RED) {
|
||||
clients[connection_id].connecting = true;
|
||||
clients[connection_id].client.connect(connOpts, function() {
|
||||
//node.log(RED._("tcpin.errors.client-connected"));
|
||||
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
|
||||
// node.status({fill:"green",shape:"dot",text:"common.status.connected"});
|
||||
node.status({fill:"green",shape:"dot",text:RED._("tcpin.status.connections",{count:Object.keys(clients).length})});
|
||||
if (clients[connection_id] && clients[connection_id].client) {
|
||||
clients[connection_id].connected = true;
|
||||
clients[connection_id].connecting = false;
|
||||
let event;
|
||||
while (event = dequeue(clients[connection_id].msgQueue)) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
if (event.msg.payload !== undefined) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
}
|
||||
event.nodeDone();
|
||||
}
|
||||
if (node.out === "time" && node.splitc < 0) {
|
||||
@@ -823,7 +856,9 @@ module.exports = function(RED) {
|
||||
else if (!clients[connection_id].connecting && clients[connection_id].connected) {
|
||||
if (clients[connection_id] && clients[connection_id].client) {
|
||||
let event = dequeue(clients[connection_id].msgQueue)
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
if (event.msg.payload !== undefined ) {
|
||||
clients[connection_id].client.write(event.msg.payload);
|
||||
}
|
||||
event.nodeDone();
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,20 @@
|
||||
</select>
|
||||
<input style="width:40px;" type="text" id="node-input-sep" pattern=".">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label><i class="fa fa-code"></i> <span data-i18n="csv.label.spec"></span></label>
|
||||
<div style="display: inline-grid;width: 70%;">
|
||||
<select style="width:100%" id="csv-option-spec">
|
||||
<option value="rfc" data-i18n="csv.spec.rfc"></option>
|
||||
<option value="" data-i18n="csv.spec.legacy"></option>
|
||||
</select>
|
||||
<div>
|
||||
<div class="form-tips csv-lecacy-warning" data-i18n="node-red:csv.spec.legacy_warning"
|
||||
style="width: calc(100% - 18px); margin-top: 4px; max-width: unset;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
|
||||
@@ -60,10 +73,10 @@
|
||||
<div class="form-row" style="padding-left:20px;">
|
||||
<label></label>
|
||||
<label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
|
||||
<select style="width:150px;" id="node-input-ret">
|
||||
<select style="width:calc(70% - 108px);" id="node-input-ret">
|
||||
<option value='\r\n' data-i18n="csv.newline.windows"></option>
|
||||
<option value='\n' data-i18n="csv.newline.linux"></option>
|
||||
<option value='\r' data-i18n="csv.newline.mac"></option>
|
||||
<option value='\r\n' data-i18n="csv.newline.windows"></option>
|
||||
</select>
|
||||
</div>
|
||||
</script>
|
||||
@@ -75,6 +88,7 @@
|
||||
color:"#DEBD5C",
|
||||
defaults: {
|
||||
name: {value:""},
|
||||
spec: {value:"rfc"},
|
||||
sep: {
|
||||
value:',', required:true,
|
||||
label:RED._("node-red:csv.label.separator"),
|
||||
@@ -83,7 +97,7 @@
|
||||
hdrin: {value:""},
|
||||
hdrout: {value:"none"},
|
||||
multi: {value:"one",required:true},
|
||||
ret: {value:'\\n'},
|
||||
ret: {value:'\\r\\n'}, // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
|
||||
temp: {value:""},
|
||||
skip: {value:"0"},
|
||||
strings: {value:true},
|
||||
@@ -123,6 +137,27 @@
|
||||
$("#node-input-sep").hide();
|
||||
}
|
||||
});
|
||||
|
||||
$("#csv-option-spec").on("change", function() {
|
||||
if ($("#csv-option-spec").val() == "rfc") {
|
||||
$(".form-tips.csv-lecacy-warning").hide();
|
||||
} else {
|
||||
$(".form-tips.csv-lecacy-warning").show();
|
||||
}
|
||||
});
|
||||
// new nodes will have `spec` set to "rfc" (default), but existing nodes will either not have
|
||||
// a spec value or it will be empty - we need to maintain the legacy behaviour for existing
|
||||
// flows but default to rfc for new nodes
|
||||
let spec = !this.spec ? "" : "rfc"
|
||||
$("#csv-option-spec").val(spec).trigger("change")
|
||||
},
|
||||
oneditsave: function() {
|
||||
const specFormVal = $("#csv-option-spec").val() || '' // empty === legacy
|
||||
const spectNodeVal = this.spec || '' // empty === legacy, null/undefined means in-place node upgrade (keep as is)
|
||||
if (specFormVal !== spectNodeVal) {
|
||||
// only update the flow value if changed (avoid marking the node dirty unnecessarily)
|
||||
this.spec = specFormVal
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@@ -15,322 +15,674 @@
|
||||
**/
|
||||
|
||||
module.exports = function(RED) {
|
||||
const csv = require('./lib/csv')
|
||||
|
||||
"use strict";
|
||||
function CSVNode(n) {
|
||||
RED.nodes.createNode(this,n);
|
||||
this.template = (n.temp || "");
|
||||
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
|
||||
this.quo = '"';
|
||||
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
|
||||
this.winflag = (this.ret === "\r\n");
|
||||
this.lineend = "\n";
|
||||
this.multi = n.multi || "one";
|
||||
this.hdrin = n.hdrin || false;
|
||||
this.hdrout = n.hdrout || "none";
|
||||
this.goodtmpl = true;
|
||||
this.skip = parseInt(n.skip || 0);
|
||||
this.store = [];
|
||||
this.parsestrings = n.strings;
|
||||
this.include_empty_strings = n.include_empty_strings || false;
|
||||
this.include_null_values = n.include_null_values || false;
|
||||
if (this.parsestrings === undefined) { this.parsestrings = true; }
|
||||
if (this.hdrout === false) { this.hdrout = "none"; }
|
||||
if (this.hdrout === true) { this.hdrout = "all"; }
|
||||
var tmpwarn = true;
|
||||
var node = this;
|
||||
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
|
||||
RED.nodes.createNode(this,n)
|
||||
const node = this
|
||||
const RFC4180Mode = n.spec === 'rfc'
|
||||
const legacyMode = !RFC4180Mode
|
||||
|
||||
// pass in an array of column names to be trimmed, de-quoted and retrimmed
|
||||
var clean = function(col,sep) {
|
||||
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
|
||||
col = col.trim().split(re) || [""];
|
||||
col = col.map(x => x.replace(/"/g,'').trim());
|
||||
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
|
||||
else { node.goodtmpl = true; }
|
||||
return col;
|
||||
}
|
||||
var template = clean(node.template,',');
|
||||
var notemplate = template.length === 1 && template[0] === '';
|
||||
node.hdrSent = false;
|
||||
node.status({}) // clear status
|
||||
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
node.hdrSent = false;
|
||||
if (legacyMode) {
|
||||
this.template = (n.temp || "");
|
||||
this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
|
||||
this.quo = '"';
|
||||
this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r");
|
||||
this.winflag = (this.ret === "\r\n");
|
||||
this.lineend = "\n";
|
||||
this.multi = n.multi || "one";
|
||||
this.hdrin = n.hdrin || false;
|
||||
this.hdrout = n.hdrout || "none";
|
||||
this.goodtmpl = true;
|
||||
this.skip = parseInt(n.skip || 0);
|
||||
this.store = [];
|
||||
this.parsestrings = n.strings;
|
||||
this.include_empty_strings = n.include_empty_strings || false;
|
||||
this.include_null_values = n.include_null_values || false;
|
||||
if (this.parsestrings === undefined) { this.parsestrings = true; }
|
||||
if (this.hdrout === false) { this.hdrout = "none"; }
|
||||
if (this.hdrout === true) { this.hdrout = "all"; }
|
||||
var tmpwarn = true;
|
||||
// var node = this;
|
||||
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
|
||||
|
||||
// pass in an array of column names to be trimmed, de-quoted and retrimmed
|
||||
var clean = function(col,sep) {
|
||||
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
|
||||
col = col.trim().split(re) || [""];
|
||||
col = col.map(x => x.replace(/"/g,'').trim());
|
||||
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
|
||||
else { node.goodtmpl = true; }
|
||||
return col;
|
||||
}
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
if (typeof msg.payload == "object") { // convert object to CSV string
|
||||
try {
|
||||
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
||||
template = clean(node.template);
|
||||
}
|
||||
const ou = [];
|
||||
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
|
||||
if (node.hdrout !== "none" && node.hdrSent === false) {
|
||||
if ((template.length === 1) && (template[0] === '')) {
|
||||
if (msg.hasOwnProperty("columns")) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
else {
|
||||
template = Object.keys(msg.payload[0]);
|
||||
}
|
||||
var template = clean(node.template,',');
|
||||
var notemplate = template.length === 1 && template[0] === '';
|
||||
node.hdrSent = false;
|
||||
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
node.hdrSent = false;
|
||||
}
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
if (typeof msg.payload == "object") { // convert object to CSV string
|
||||
try {
|
||||
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
||||
template = clean(node.template);
|
||||
}
|
||||
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
|
||||
if (node.hdrout === "once") { node.hdrSent = true; }
|
||||
}
|
||||
for (var s = 0; s < msg.payload.length; s++) {
|
||||
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
|
||||
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
|
||||
for (var t = 0; t < msg.payload[s].length; t++) {
|
||||
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
|
||||
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
}
|
||||
ou.push(msg.payload[s].join(node.sep));
|
||||
}
|
||||
else {
|
||||
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
const ou = [];
|
||||
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
|
||||
if (node.hdrout !== "none" && node.hdrSent === false) {
|
||||
if ((template.length === 1) && (template[0] === '')) {
|
||||
/* istanbul ignore else */
|
||||
if (tmpwarn === true) { // just warn about missing template once
|
||||
node.warn(RED._("csv.errors.obj_csv"));
|
||||
tmpwarn = false;
|
||||
if (msg.hasOwnProperty("columns")) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
const row = [];
|
||||
for (var p in msg.payload[0]) {
|
||||
else {
|
||||
template = Object.keys(msg.payload[0]);
|
||||
}
|
||||
}
|
||||
ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep));
|
||||
if (node.hdrout === "once") { node.hdrSent = true; }
|
||||
}
|
||||
for (var s = 0; s < msg.payload.length; s++) {
|
||||
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
|
||||
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
|
||||
for (var t = 0; t < msg.payload[s].length; t++) {
|
||||
if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
|
||||
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n"
|
||||
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
|
||||
}
|
||||
}
|
||||
ou.push(msg.payload[s].join(node.sep));
|
||||
}
|
||||
else {
|
||||
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
|
||||
template = clean(msg.columns || "",",");
|
||||
}
|
||||
if ((template.length === 1) && (template[0] === '')) {
|
||||
/* istanbul ignore else */
|
||||
if (msg.payload[s].hasOwnProperty(p)) {
|
||||
if (tmpwarn === true) { // just warn about missing template once
|
||||
node.warn(RED._("csv.errors.obj_csv"));
|
||||
tmpwarn = false;
|
||||
}
|
||||
const row = [];
|
||||
for (var p in msg.payload[0]) {
|
||||
/* istanbul ignore else */
|
||||
if (typeof msg.payload[s][p] !== "object") {
|
||||
// Fix to honour include null values flag
|
||||
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
|
||||
var q = "";
|
||||
if (msg.payload[s][p] !== undefined) {
|
||||
q += msg.payload[s][p];
|
||||
if (msg.payload[s].hasOwnProperty(p)) {
|
||||
/* istanbul ignore else */
|
||||
if (typeof msg.payload[s][p] !== "object") {
|
||||
// Fix to honour include null values flag
|
||||
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
|
||||
var q = "";
|
||||
if (msg.payload[s][p] !== undefined) {
|
||||
q += msg.payload[s][p];
|
||||
}
|
||||
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
q = q.replace(/"/g, '""');
|
||||
row.push(node.quo + q + node.quo);
|
||||
}
|
||||
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
row.push(node.quo + q + node.quo);
|
||||
}
|
||||
else { row.push(q); } // otherwise just add
|
||||
}
|
||||
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
q = q.replace(/"/g, '""');
|
||||
row.push(node.quo + q + node.quo);
|
||||
}
|
||||
else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
row.push(node.quo + q + node.quo);
|
||||
}
|
||||
else { row.push(q); } // otherwise just add
|
||||
}
|
||||
}
|
||||
ou.push(row.join(node.sep)); // add separator
|
||||
}
|
||||
ou.push(row.join(node.sep)); // add separator
|
||||
else {
|
||||
const row = [];
|
||||
for (var t=0; t < template.length; t++) {
|
||||
if (template[t] === '') {
|
||||
row.push('');
|
||||
}
|
||||
else {
|
||||
var tt = template[t];
|
||||
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
|
||||
else { tt = '"'+tt+'"'; }
|
||||
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
|
||||
/* istanbul ignore else */
|
||||
if (p === undefined) { p = ""; }
|
||||
// fix to honour include null values flag
|
||||
//if (p === null && node.include_null_values !== true) { p = "";}
|
||||
p = RED.util.ensureString(p);
|
||||
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
p = p.replace(/"/g, '""');
|
||||
row.push(node.quo + p + node.quo);
|
||||
}
|
||||
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
row.push(node.quo + p + node.quo);
|
||||
}
|
||||
else { row.push(p); } // otherwise just add
|
||||
}
|
||||
}
|
||||
ou.push(row.join(node.sep)); // add separator
|
||||
}
|
||||
}
|
||||
}
|
||||
// join lines, don't forget to add the last new line
|
||||
msg.payload = ou.join(node.ret) + node.ret;
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
|
||||
if (msg.payload !== '') {
|
||||
send(msg);
|
||||
}
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
}
|
||||
else if (typeof msg.payload == "string") { // convert CSV string to object
|
||||
try {
|
||||
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
|
||||
var j = 0; // pointer into array of template items
|
||||
var k = [""]; // array of data for each of the template items
|
||||
var o = {}; // output object to build up
|
||||
var a = []; // output array is needed for multiline option
|
||||
var first = true; // is this the first line
|
||||
var last = false;
|
||||
var line = msg.payload;
|
||||
var linecount = 0;
|
||||
var tmp = "";
|
||||
var has_parts = msg.hasOwnProperty("parts");
|
||||
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
|
||||
if (msg.hasOwnProperty("parts")) {
|
||||
linecount = msg.parts.index;
|
||||
if (msg.parts.index > node.skip) { first = false; }
|
||||
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
|
||||
}
|
||||
|
||||
// For now we are just going to assume that any \r or \n means an end of line...
|
||||
// got to be a weird csv that has singleton \r \n in it for another reason...
|
||||
|
||||
// Now process the whole file/line
|
||||
var nocr = (line.match(/[\r\n]/g)||[]).length;
|
||||
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
|
||||
for (var i = 0; i < line.length; i++) {
|
||||
if (first && (linecount < node.skip)) {
|
||||
if (line[i] === "\n") { linecount += 1; }
|
||||
continue;
|
||||
}
|
||||
if ((node.hdrin === true) && first) { // if the template is in the first line
|
||||
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
|
||||
if (line.length - i === 1) { tmp += line[i]; }
|
||||
template = clean(tmp,node.sep);
|
||||
first = false;
|
||||
}
|
||||
else { tmp += line[i]; }
|
||||
}
|
||||
else {
|
||||
const row = [];
|
||||
for (var t=0; t < template.length; t++) {
|
||||
if (template[t] === '') {
|
||||
row.push('');
|
||||
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
|
||||
f = !f;
|
||||
if (line[i-1] === node.quo) {
|
||||
if (f === false) { k[j] += '\"'; }
|
||||
} // if it's a quotequote then it's actually a quote
|
||||
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
|
||||
}
|
||||
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
|
||||
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
else {
|
||||
var tt = template[t];
|
||||
if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; }
|
||||
else { tt = '"'+tt+'"'; }
|
||||
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
|
||||
/* istanbul ignore else */
|
||||
if (p === undefined) { p = ""; }
|
||||
// fix to honour include null values flag
|
||||
//if (p === null && node.include_null_values !== true) { p = "";}
|
||||
p = RED.util.ensureString(p);
|
||||
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
|
||||
p = p.replace(/"/g, '""');
|
||||
row.push(node.quo + p + node.quo);
|
||||
}
|
||||
else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n"
|
||||
row.push(node.quo + p + node.quo);
|
||||
}
|
||||
else { row.push(p); } // otherwise just add
|
||||
j += 1;
|
||||
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
|
||||
k[j] = line.length - 1 === i ? null : "";
|
||||
}
|
||||
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
|
||||
//console.log(j,k,o,k[j]);
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
|
||||
if (line[i-1] === node.sep) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
j = 0;
|
||||
k = [""];
|
||||
o = {};
|
||||
f = true; // reset in/out flag ready for next line.
|
||||
}
|
||||
else { // just add to the part of the message
|
||||
k[j] += line[i];
|
||||
}
|
||||
ou.push(row.join(node.sep)); // add separator
|
||||
}
|
||||
}
|
||||
}
|
||||
// join lines, don't forget to add the last new line
|
||||
msg.payload = ou.join(node.ret) + node.ret;
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(',');
|
||||
if (msg.payload !== '') { send(msg); }
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
}
|
||||
else if (typeof msg.payload == "string") { // convert CSV string to object
|
||||
try {
|
||||
var f = true; // flag to indicate if inside or outside a pair of quotes true = outside.
|
||||
var j = 0; // pointer into array of template items
|
||||
var k = [""]; // array of data for each of the template items
|
||||
var o = {}; // output object to build up
|
||||
var a = []; // output array is needed for multiline option
|
||||
var first = true; // is this the first line
|
||||
var last = false;
|
||||
var line = msg.payload;
|
||||
var linecount = 0;
|
||||
var tmp = "";
|
||||
var has_parts = msg.hasOwnProperty("parts");
|
||||
var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i;
|
||||
if (msg.hasOwnProperty("parts")) {
|
||||
linecount = msg.parts.index;
|
||||
if (msg.parts.index > node.skip) { first = false; }
|
||||
if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; }
|
||||
}
|
||||
// Finished so finalize and send anything left
|
||||
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
|
||||
// For now we are just going to assume that any \r or \n means an end of line...
|
||||
// got to be a weird csv that has singleton \r \n in it for another reason...
|
||||
|
||||
// Now process the whole file/line
|
||||
var nocr = (line.match(/[\r\n]/g)||[]).length;
|
||||
if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; }
|
||||
for (var i = 0; i < line.length; i++) {
|
||||
if (first && (linecount < node.skip)) {
|
||||
if (line[i] === "\n") { linecount += 1; }
|
||||
continue;
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
if ((node.hdrin === true) && first) { // if the template is in the first line
|
||||
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
|
||||
if (line.length - i === 1) { tmp += line[i]; }
|
||||
template = clean(tmp,node.sep);
|
||||
first = false;
|
||||
}
|
||||
else { tmp += line[i]; }
|
||||
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
else {
|
||||
if (line[i] === node.quo) { // if it's a quote toggle inside or outside
|
||||
f = !f;
|
||||
if (line[i-1] === node.quo) {
|
||||
if (f === false) { k[j] += '\"'; }
|
||||
} // if it's a quotequote then it's actually a quote
|
||||
//if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; }
|
||||
}
|
||||
else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null
|
||||
if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
j += 1;
|
||||
// if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",'
|
||||
k[j] = line.length - 1 === i ? null : "";
|
||||
}
|
||||
else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines
|
||||
//console.log(j,k,o,k[j]);
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
// if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3'
|
||||
if (line[i-1] === node.sep) k[j] = null;
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
j = 0;
|
||||
k = [""];
|
||||
o = {};
|
||||
f = true; // reset in/out flag ready for next line.
|
||||
}
|
||||
else { // just add to the part of the message
|
||||
k[j] += line[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Finished so finalize and send anything left
|
||||
if (f === false) { node.warn(RED._("csv.errors.bad_csv")); }
|
||||
if (!node.goodtmpl) { template[j] = "col"+(j+1); }
|
||||
|
||||
if ( template[j] && (template[j] !== "") ) {
|
||||
if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); }
|
||||
else { if (k[j] !== null) k[j].replace(/\r$/,''); }
|
||||
if (node.include_null_values && k[j] === null) o[template[j]] = k[j];
|
||||
if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j];
|
||||
if (k[j] !== null && k[j] !== "") o[template[j]] = k[j];
|
||||
}
|
||||
|
||||
if (JSON.stringify(o) !== "{}") { // don't send empty objects
|
||||
a.push(o); // add to the array
|
||||
}
|
||||
|
||||
if (node.multi !== "one") {
|
||||
msg.payload = a;
|
||||
if (has_parts && nocr <= 1) {
|
||||
if (JSON.stringify(o) !== "{}") {
|
||||
node.store.push(o);
|
||||
if (node.multi !== "one") {
|
||||
msg.payload = a;
|
||||
if (has_parts && nocr <= 1) {
|
||||
if (JSON.stringify(o) !== "{}") {
|
||||
node.store.push(o);
|
||||
}
|
||||
if (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store;
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
delete msg.parts;
|
||||
send(msg);
|
||||
node.store = [];
|
||||
}
|
||||
}
|
||||
if (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store;
|
||||
else {
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
delete msg.parts;
|
||||
send(msg);
|
||||
node.store = [];
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
else {
|
||||
var len = a.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var newMessage = RED.util.cloneMessage(msg);
|
||||
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
newMessage.payload = a[i];
|
||||
if (!has_parts) {
|
||||
newMessage.parts = {
|
||||
id: msg._msgid,
|
||||
index: i,
|
||||
count: len
|
||||
};
|
||||
var len = a.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var newMessage = RED.util.cloneMessage(msg);
|
||||
newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(',');
|
||||
newMessage.payload = a[i];
|
||||
if (!has_parts) {
|
||||
newMessage.parts = {
|
||||
id: msg._msgid,
|
||||
index: i,
|
||||
count: len
|
||||
};
|
||||
}
|
||||
else {
|
||||
newMessage.parts.index -= node.skip;
|
||||
newMessage.parts.count -= node.skip;
|
||||
if (node.hdrin) { // if we removed the header line then shift the counts by 1
|
||||
newMessage.parts.index -= 1;
|
||||
newMessage.parts.count -= 1;
|
||||
}
|
||||
}
|
||||
if (last) { newMessage.complete = true; }
|
||||
send(newMessage);
|
||||
}
|
||||
else {
|
||||
newMessage.parts.index -= node.skip;
|
||||
newMessage.parts.count -= node.skip;
|
||||
if (node.hdrin) { // if we removed the header line then shift the counts by 1
|
||||
newMessage.parts.index -= 1;
|
||||
newMessage.parts.count -= 1;
|
||||
if (has_parts && last && len === 0) {
|
||||
send({complete:true});
|
||||
}
|
||||
}
|
||||
node.linecount = 0;
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
}
|
||||
else { node.warn(RED._("csv.errors.csv_js")); done(); }
|
||||
}
|
||||
else {
|
||||
if (!msg.hasOwnProperty("reset")) {
|
||||
node.send(msg); // If no payload and not reset - just pass it on.
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(RFC4180Mode) {
|
||||
node.template = (n.temp || "")
|
||||
node.sep = (n.sep || ',').replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
|
||||
node.quo = '"'
|
||||
// default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)")
|
||||
node.ret = (n.ret || "\r\n").replace(/\\n/g, "\n").replace(/\\r/g, "\r")
|
||||
node.multi = n.multi || "one"
|
||||
node.hdrin = n.hdrin || false
|
||||
node.hdrout = n.hdrout || "none"
|
||||
node.goodtmpl = true
|
||||
node.skip = parseInt(n.skip || 0)
|
||||
node.store = []
|
||||
node.parsestrings = n.strings
|
||||
node.include_empty_strings = n.include_empty_strings || false
|
||||
node.include_null_values = n.include_null_values || false
|
||||
if (node.parsestrings === undefined) { node.parsestrings = true }
|
||||
if (node.hdrout === false) { node.hdrout = "none" }
|
||||
if (node.hdrout === true) { node.hdrout = "all" }
|
||||
const dontSendHeaders = node.hdrout === "none"
|
||||
const sendHeadersOnce = node.hdrout === "once"
|
||||
const sendHeadersAlways = node.hdrout === "all"
|
||||
const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways)
|
||||
const quoteables = [node.sep, node.quo, "\n", "\r"]
|
||||
const templateQuoteables = [',', '"', "\n", "\r"]
|
||||
let badTemplateWarnOnce = true
|
||||
|
||||
const columnStringToTemplateArray = function (col, sep) {
|
||||
// NOTE: enforce strict column template parsing in RFC4180 mode
|
||||
const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true })
|
||||
if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false }
|
||||
return parsed.headers.length ? parsed.headers : null
|
||||
}
|
||||
const templateArrayToColumnString = function (template, keepEmptyColumns) {
|
||||
// NOTE: enforce strict column template parsing in RFC4180 mode
|
||||
const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true })
|
||||
return keepEmptyColumns
|
||||
? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables}))
|
||||
: parsed.header // exclues empty columns
|
||||
// TODO: resolve inconsistency between CSV->JSON and JSON->CSV
|
||||
// CSV->JSON: empty columns are excluded
|
||||
// JSON->CSV: empty columns are kept in some cases
|
||||
}
|
||||
function addQuotes(cell, options) {
|
||||
options = options || {}
|
||||
return csv.quoteCell(cell, {
|
||||
quote: options.quote || node.quo || '"',
|
||||
separator: options.separator || node.sep || ',',
|
||||
quoteables: options.quoteables || quoteables
|
||||
})
|
||||
}
|
||||
const hasTemplate = (t) => t?.length > 0 && !(t.length === 1 && t[0] === '')
|
||||
let template
|
||||
try {
|
||||
template = columnStringToTemplateArray(node.template, ',') || ['']
|
||||
} catch (e) {
|
||||
node.warn(RED._("csv.errors.bad_template")) // is warning really necessary now we have status?
|
||||
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
|
||||
return // dont hook up the node
|
||||
}
|
||||
const noTemplate = hasTemplate(template) === false
|
||||
node.hdrSent = false
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
node.status({}) // clear status
|
||||
if (msg.hasOwnProperty("reset")) {
|
||||
node.hdrSent = false
|
||||
}
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
let inputData = msg.payload
|
||||
if (typeof inputData == "object") { // convert object to CSV string
|
||||
try {
|
||||
// first determine the payload kind. Array or objects? Array of primitives? Array of arrays? Just an object?
|
||||
// then, if necessary, convert to an array of objects/arrays
|
||||
let isObject = !Array.isArray(inputData) && typeof inputData === 'object'
|
||||
let isArrayOfObjects = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] === 'object'
|
||||
let isArrayOfArrays = Array.isArray(inputData) && inputData.length > 0 && Array.isArray(inputData[0])
|
||||
let isArrayOfPrimitives = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] !== 'object'
|
||||
|
||||
if (isObject) {
|
||||
inputData = [inputData]
|
||||
isArrayOfObjects = true
|
||||
isObject = false
|
||||
} else if (isArrayOfPrimitives) {
|
||||
inputData = [inputData]
|
||||
isArrayOfArrays = true
|
||||
isArrayOfPrimitives = false
|
||||
}
|
||||
|
||||
const stringBuilder = []
|
||||
if (!(noTemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
|
||||
template = columnStringToTemplateArray(node.template) || ['']
|
||||
}
|
||||
|
||||
// build header line
|
||||
if (sendHeaders && node.hdrSent === false) {
|
||||
if (hasTemplate(template) === false) {
|
||||
if (msg.hasOwnProperty("columns")) {
|
||||
template = columnStringToTemplateArray(msg.columns || "", ",") || ['']
|
||||
}
|
||||
else {
|
||||
template = Object.keys(inputData[0]) || ['']
|
||||
}
|
||||
}
|
||||
if (last) { newMessage.complete = true; }
|
||||
send(newMessage);
|
||||
stringBuilder.push(templateArrayToColumnString(template, true))
|
||||
if (sendHeadersOnce) { node.hdrSent = true }
|
||||
}
|
||||
if (has_parts && last && len === 0) {
|
||||
send({complete:true});
|
||||
|
||||
// build csv lines
|
||||
for (let s = 0; s < inputData.length; s++) {
|
||||
let row = inputData[s]
|
||||
if (isArrayOfArrays) {
|
||||
/*** row is an array of arrays ***/
|
||||
const _hasTemplate = hasTemplate(template)
|
||||
const len = _hasTemplate ? template.length : row.length
|
||||
const result = []
|
||||
for (let t = 0; t < len; t++) {
|
||||
let cell = row[t]
|
||||
if (cell === undefined) { cell = "" }
|
||||
if(_hasTemplate) {
|
||||
const header = template[t]
|
||||
if (header) {
|
||||
result[t] = addQuotes(RED.util.ensureString(cell))
|
||||
}
|
||||
} else {
|
||||
result[t] = addQuotes(RED.util.ensureString(cell))
|
||||
}
|
||||
}
|
||||
stringBuilder.push(result.join(node.sep))
|
||||
} else {
|
||||
/*** row is an object ***/
|
||||
if (hasTemplate(template) === false && (msg.hasOwnProperty("columns"))) {
|
||||
template = columnStringToTemplateArray(msg.columns || "", ",")
|
||||
}
|
||||
if (hasTemplate(template) === false) {
|
||||
/*** row is an object but we still don't have a template ***/
|
||||
if (badTemplateWarnOnce === true) {
|
||||
node.warn(RED._("csv.errors.obj_csv"))
|
||||
badTemplateWarnOnce = false
|
||||
}
|
||||
const rowData = []
|
||||
for (let header in inputData[0]) {
|
||||
if (row.hasOwnProperty(header)) {
|
||||
const cell = row[header]
|
||||
if (typeof cell !== "object") {
|
||||
let cellValue = ""
|
||||
if (cell !== undefined) {
|
||||
cellValue += cell
|
||||
}
|
||||
rowData.push(addQuotes(cellValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
stringBuilder.push(rowData.join(node.sep))
|
||||
} else {
|
||||
/*** row is an object and we have a template ***/
|
||||
const rowData = []
|
||||
for (let t = 0; t < template.length; t++) {
|
||||
if (!template[t]) {
|
||||
rowData.push('')
|
||||
}
|
||||
else {
|
||||
let cellValue = inputData[s][template[t]]
|
||||
if (cellValue === undefined) { cellValue = "" }
|
||||
cellValue = RED.util.ensureString(cellValue)
|
||||
rowData.push(addQuotes(cellValue))
|
||||
}
|
||||
}
|
||||
stringBuilder.push(rowData.join(node.sep)); // add separator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// join lines, don't forget to add the last new line
|
||||
msg.payload = stringBuilder.join(node.ret) + node.ret
|
||||
msg.columns = templateArrayToColumnString(template)
|
||||
if (msg.payload !== '') { send(msg) }
|
||||
done()
|
||||
}
|
||||
catch (e) {
|
||||
done(e)
|
||||
}
|
||||
node.linecount = 0;
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e); }
|
||||
else if (typeof inputData == "string") { // convert CSV string to object
|
||||
try {
|
||||
let firstLine = true; // is this the first line
|
||||
let last = false
|
||||
let linecount = 0
|
||||
const has_parts = msg.hasOwnProperty("parts")
|
||||
|
||||
// determine if this is a multi part message and if so what part we are processing
|
||||
if (msg.hasOwnProperty("parts")) {
|
||||
linecount = msg.parts.index
|
||||
if (msg.parts.index > node.skip) { firstLine = false }
|
||||
if (msg.parts.hasOwnProperty("count") && (msg.parts.index + 1 >= msg.parts.count)) { last = true }
|
||||
}
|
||||
|
||||
// If skip is set, compute the cursor position to start parsing from
|
||||
let _cursor = 0
|
||||
if (node.skip > 0 && linecount < node.skip) {
|
||||
for (; _cursor < inputData.length; _cursor++) {
|
||||
if (firstLine && (linecount < node.skip)) {
|
||||
if (inputData[_cursor] === "\r" || inputData[_cursor] === "\n") {
|
||||
linecount += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if (_cursor >= inputData.length) {
|
||||
return // skip this line
|
||||
}
|
||||
}
|
||||
|
||||
// count the number of line breaks in the string
|
||||
const noofCR = ((_cursor ? inputData.slice(_cursor) : inputData).match(/[\r\n]/g) || []).length
|
||||
|
||||
// if we have `parts` and we are outputting multiple objects and we have more than one line
|
||||
// then we need to set firstLine to true so that we process the header line
|
||||
if (has_parts && node.multi === "mult" && noofCR > 1) {
|
||||
firstLine = true
|
||||
}
|
||||
|
||||
// if we are processing the first line and the node has been set to extract the header line
|
||||
// update the template with the header line
|
||||
if (firstLine && node.hdrin === true) {
|
||||
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
|
||||
const csvOptionsForHeaderRow = {
|
||||
cursor: _cursor,
|
||||
separator: node.sep,
|
||||
quote: node.quo,
|
||||
dataHasHeaderRow: true,
|
||||
headersOnly: true,
|
||||
outputStyle: 'array',
|
||||
strict: true // enforce strict parsing of the header row
|
||||
}
|
||||
try {
|
||||
const csvHeader = csv.parse(inputData, csvOptionsForHeaderRow)
|
||||
template = csvHeader.headers
|
||||
_cursor = csvHeader.cursor
|
||||
} catch (e) {
|
||||
// node.warn(RED._("csv.errors.bad_template")) // add warning?
|
||||
node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// now we process the data lines
|
||||
/** @type {import('./lib/csv/index.js').CSVParseOptions} */
|
||||
const csvOptions = {
|
||||
cursor: _cursor,
|
||||
separator: node.sep,
|
||||
quote: node.quo,
|
||||
dataHasHeaderRow: false,
|
||||
headers: hasTemplate(template) ? template : null,
|
||||
outputStyle: 'object',
|
||||
includeNullValues: node.include_null_values,
|
||||
includeEmptyStrings: node.include_empty_strings,
|
||||
parseNumeric: node.parsestrings,
|
||||
strict: false // relax the strictness of the parser for data rows
|
||||
}
|
||||
const csvParseResult = csv.parse(inputData, csvOptions)
|
||||
const data = csvParseResult.data
|
||||
|
||||
// output results
|
||||
if (node.multi !== "one") {
|
||||
if (has_parts && noofCR <= 1) {
|
||||
if (data.length > 0) {
|
||||
node.store.push(...data)
|
||||
}
|
||||
if (msg.parts.index + 1 === msg.parts.count) {
|
||||
msg.payload = node.store
|
||||
msg.columns = csvParseResult.header
|
||||
// msg._mode = 'RFC4180 mode'
|
||||
delete msg.parts
|
||||
send(msg)
|
||||
node.store = []
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg.columns = csvParseResult.header
|
||||
// msg._mode = 'RFC4180 mode'
|
||||
msg.payload = data
|
||||
send(msg); // finally send the array
|
||||
}
|
||||
}
|
||||
else {
|
||||
const len = data.length
|
||||
for (let row = 0; row < len; row++) {
|
||||
const newMessage = RED.util.cloneMessage(msg)
|
||||
newMessage.columns = csvParseResult.header
|
||||
newMessage.payload = data[row]
|
||||
if (!has_parts) {
|
||||
newMessage.parts = {
|
||||
id: msg._msgid,
|
||||
index: row,
|
||||
count: len
|
||||
}
|
||||
}
|
||||
else {
|
||||
newMessage.parts.index -= node.skip
|
||||
newMessage.parts.count -= node.skip
|
||||
if (node.hdrin) { // if we removed the header line then shift the counts by 1
|
||||
newMessage.parts.index -= 1
|
||||
newMessage.parts.count -= 1
|
||||
}
|
||||
}
|
||||
if (last) { newMessage.complete = true }
|
||||
// newMessage._mode = 'RFC4180 mode'
|
||||
send(newMessage)
|
||||
}
|
||||
if (has_parts && last && len === 0) {
|
||||
// send({complete:true, _mode: 'RFC4180 mode'})
|
||||
send({ complete: true })
|
||||
}
|
||||
}
|
||||
|
||||
node.linecount = 0
|
||||
done()
|
||||
}
|
||||
catch (e) {
|
||||
done(e)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// RFC-vs-legacy mode difference: In RFC mode, we throw catchable errors and provide a status message
|
||||
const err = new Error(RED._("csv.errors.csv_js"))
|
||||
node.status({ fill: "red", shape: "dot", text: err.message })
|
||||
done(err)
|
||||
}
|
||||
}
|
||||
else { node.warn(RED._("csv.errors.csv_js")); done(); }
|
||||
}
|
||||
else {
|
||||
if (!msg.hasOwnProperty("reset")) {
|
||||
node.send(msg); // If no payload and not reset - just pass it on.
|
||||
else {
|
||||
if (!msg.hasOwnProperty("reset")) {
|
||||
node.send(msg); // If no payload and not reset - just pass it on.
|
||||
}
|
||||
done()
|
||||
}
|
||||
done();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType("csv",CSVNode);
|
||||
|
||||
RED.nodes.registerType("csv",CSVNode)
|
||||
}
|
||||
|
324
packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
vendored
Normal file
324
packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
vendored
Normal file
@@ -0,0 +1,324 @@
|
||||
|
||||
/**
|
||||
* @typedef {Object} CSVParseOptions
|
||||
* @property {number} [cursor=0] - an index into the CSV to start parsing from
|
||||
* @property {string} [separator=','] - the separator character
|
||||
* @property {string} [quote='"'] - the quote character
|
||||
* @property {boolean} [headersOnly=false] - only parse the headers and return them
|
||||
* @property {string[]} [headers=[]] - an array of headers to use instead of the first row of the CSV data
|
||||
* @property {boolean} [dataHasHeaderRow=true] - whether the CSV data to parse has a header row
|
||||
* @property {boolean} [outputHeader=true] - whether the output data should include a header row (only applies to array output)
|
||||
* @property {boolean} [parseNumeric=false] - parse numeric values into numbers
|
||||
* @property {boolean} [includeNullValues=false] - include null values in the output
|
||||
* @property {boolean} [includeEmptyStrings=true] - include empty strings in the output
|
||||
* @property {string} [outputStyle='object'] - output an array of arrays or an array of objects
|
||||
* @property {boolean} [strict=false] - throw an error if the CSV is malformed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a CSV string into an array of arrays or an array of objects.
|
||||
*
|
||||
* NOTES:
|
||||
* * Deviations from the RFC4180 spec (for the sake of user fiendliness, system implementations and flexibility), this parser will:
|
||||
* * accept any separator character, not just `,`
|
||||
* * accept any quote character, not just `"`
|
||||
* * parse `\r`, `\n` or `\r\n` as line endings (RRFC4180 2.1 states lines are separated by CRLF)
|
||||
* * Only single character `quote` is supported
|
||||
* * `quote` is `"` by default
|
||||
* * Any cell that contains a `quote` or `separator` will be quoted
|
||||
* * Any `quote` characters inside a cell will be escaped as per RFC 4180 2.6
|
||||
* * Only single character `separator` is supported
|
||||
* * Only `array` and `object` output styles are supported
|
||||
* * `array` output style is an array of arrays [[],[],[]]
|
||||
* * `object` output style is an array of objects [{},{},{}]
|
||||
* * Only `headers` or `dataHasHeaderRow` are supported, not both
|
||||
* @param {string} csvIn - the CSV string to parse
|
||||
* @param {CSVParseOptions} parseOptions - options
|
||||
* @throws {Error}
|
||||
*/
|
||||
function parse(csvIn, parseOptions) {
|
||||
/* Normalise options */
|
||||
parseOptions = parseOptions || {};
|
||||
const separator = parseOptions.separator ?? ',';
|
||||
const quote = parseOptions.quote ?? '"';
|
||||
const headersOnly = parseOptions.headersOnly ?? false;
|
||||
const headers = Array.isArray(parseOptions.headers) ? parseOptions.headers : []
|
||||
const dataHasHeaderRow = parseOptions.dataHasHeaderRow ?? true;
|
||||
const outputHeader = parseOptions.outputHeader ?? true;
|
||||
const parseNumeric = parseOptions.parseNumeric ?? false;
|
||||
const includeNullValues = parseOptions.includeNullValues ?? false;
|
||||
const includeEmptyStrings = parseOptions.includeEmptyStrings ?? true;
|
||||
const outputStyle = ['array', 'object'].includes(parseOptions.outputStyle) ? parseOptions.outputStyle : 'object'; // 'array [[],[],[]]' or 'object [{},{},{}]
|
||||
const strict = parseOptions.strict ?? false
|
||||
|
||||
/* Local variables */
|
||||
const cursorMax = csvIn.length;
|
||||
const ouputArrays = outputStyle === 'array';
|
||||
const headersSupplied = headers.length > 0
|
||||
// The original regex was an "is-a-number" positive logic test. /^ *[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+ *$/i;
|
||||
// Below, is less strict and inverted logic but coupled with +cast it is 13%+ faster than original regex+parsefloat
|
||||
// and has the benefit of understanding hexadecimals, binary and octal numbers.
|
||||
const skipNumberConversion = /^ *(\+|-0\d|0\d)/
|
||||
const cellBuilder = []
|
||||
let rowBuilder = []
|
||||
let cursor = typeof parseOptions.cursor === 'number' ? parseOptions.cursor : 0;
|
||||
let newCell = true, inQuote = false, closed = false, output = [];
|
||||
|
||||
/* inline helper functions */
|
||||
const finaliseCell = () => {
|
||||
let cell = cellBuilder.join('')
|
||||
cellBuilder.length = 0
|
||||
// push the cell:
|
||||
// NOTE: if cell is empty but newCell==true, then this cell had zero chars - push `null`
|
||||
// otherwise push empty string
|
||||
return rowBuilder.push(cell || (newCell ? null : ''))
|
||||
}
|
||||
const finaliseRow = () => {
|
||||
if (cellBuilder.length) {
|
||||
finaliseCell()
|
||||
}
|
||||
if (rowBuilder.length) {
|
||||
output.push(rowBuilder)
|
||||
rowBuilder = []
|
||||
}
|
||||
}
|
||||
|
||||
/* Main parsing loop */
|
||||
while (cursor < cursorMax) {
|
||||
const char = csvIn[cursor]
|
||||
if (inQuote) {
|
||||
if (char === quote && csvIn[cursor + 1] === quote) {
|
||||
cellBuilder.push(quote)
|
||||
cursor += 2;
|
||||
newCell = false;
|
||||
closed = false;
|
||||
} else if (char === quote) {
|
||||
inQuote = false;
|
||||
cursor += 1;
|
||||
newCell = false;
|
||||
closed = true;
|
||||
} else {
|
||||
cellBuilder.push(char)
|
||||
newCell = false;
|
||||
closed = false;
|
||||
cursor++;
|
||||
}
|
||||
} else {
|
||||
if (char === separator) {
|
||||
finaliseCell()
|
||||
cursor += 1;
|
||||
newCell = true;
|
||||
closed = false;
|
||||
} else if (char === quote) {
|
||||
if (newCell) {
|
||||
inQuote = true;
|
||||
cursor += 1;
|
||||
newCell = false;
|
||||
closed = false;
|
||||
}
|
||||
else if (strict) {
|
||||
throw new UnquotedQuoteError(cursor)
|
||||
} else {
|
||||
// not strict, keep 1 quote if the next char is not a cell/record separator
|
||||
cursor++
|
||||
if (csvIn[cursor] && csvIn[cursor] !== '\n' && csvIn[cursor] !== '\r' && csvIn[cursor] !== separator) {
|
||||
cellBuilder.push(char)
|
||||
if (csvIn[cursor] === quote) {
|
||||
cursor++ // skip the next quote
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (char === '\n' || char === '\r') {
|
||||
finaliseRow()
|
||||
if (csvIn[cursor + 1] === '\n') {
|
||||
cursor += 2;
|
||||
} else {
|
||||
cursor++
|
||||
}
|
||||
newCell = true;
|
||||
closed = false;
|
||||
if (headersOnly) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if (closed) {
|
||||
if (strict) {
|
||||
throw new DataAfterCloseError(cursor)
|
||||
} else {
|
||||
cursor--; // move back to grab the previously discarded char
|
||||
closed = false
|
||||
}
|
||||
} else {
|
||||
cellBuilder.push(char)
|
||||
newCell = false;
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (strict && inQuote) {
|
||||
throw new ParseError(`Missing quote, unclosed cell`, cursor)
|
||||
}
|
||||
// finalise the last cell/row
|
||||
finaliseRow()
|
||||
let firstRowIsHeader = false
|
||||
// if no headers supplied, generate them
|
||||
if (output.length >= 1) {
|
||||
if (headersSupplied) {
|
||||
// headers already supplied
|
||||
} else if (dataHasHeaderRow) {
|
||||
// take the first row as the headers
|
||||
headers.push(...output[0])
|
||||
firstRowIsHeader = true
|
||||
} else {
|
||||
// generate headers col1, col2, col3, etc
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
headers.push("col" + (i + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalResult = {
|
||||
/** @type {String[]} headers as an array of string */
|
||||
headers: headers,
|
||||
/** @type {String} headers as a comma-separated string */
|
||||
header: null,
|
||||
/** @type {Any[]} Result Data (may include header row: check `firstRowIsHeader` flag) */
|
||||
data: [],
|
||||
/** @type {Boolean|undefined} flag to indicate if the first row is a header row (only applies when `outputStyle` is 'array') */
|
||||
firstRowIsHeader: undefined,
|
||||
/** @type {'array'|'object'} flag to indicate the output style */
|
||||
outputStyle: outputStyle,
|
||||
/** @type {Number} The current cursor position */
|
||||
cursor: cursor,
|
||||
}
|
||||
|
||||
const quotedHeaders = []
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
if (!headers[i]) {
|
||||
continue
|
||||
}
|
||||
quotedHeaders.push(quoteCell(headers[i], { quote, separator: ',' }))
|
||||
}
|
||||
finalResult.header = quotedHeaders.join(',') // always quote headers and join with comma
|
||||
|
||||
// output is an array of arrays [[],[],[]]
|
||||
if (ouputArrays || headersOnly) {
|
||||
if (!firstRowIsHeader && !headersOnly && outputHeader && headers.length > 0) {
|
||||
if (output.length > 0) {
|
||||
output.unshift(headers)
|
||||
} else {
|
||||
output = [headers]
|
||||
}
|
||||
firstRowIsHeader = true
|
||||
}
|
||||
if (headersOnly) {
|
||||
delete finalResult.firstRowIsHeader
|
||||
return finalResult
|
||||
}
|
||||
finalResult.firstRowIsHeader = firstRowIsHeader
|
||||
finalResult.data = (firstRowIsHeader && !outputHeader) ? output.slice(1) : output
|
||||
return finalResult
|
||||
}
|
||||
|
||||
// output is an array of objects [{},{},{}]
|
||||
const outputObjects = []
|
||||
let i = firstRowIsHeader ? 1 : 0
|
||||
for (; i < output.length; i++) {
|
||||
const rowObject = {}
|
||||
let isEmpty = true
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
if (!headers[j]) {
|
||||
continue
|
||||
}
|
||||
let v = output[i][j] === undefined ? null : output[i][j]
|
||||
if (v === null && !includeNullValues) {
|
||||
continue
|
||||
} else if (v === "" && !includeEmptyStrings) {
|
||||
continue
|
||||
} else if (parseNumeric === true && v && !skipNumberConversion.test(v)) {
|
||||
const vTemp = +v
|
||||
const isNumber = !isNaN(vTemp)
|
||||
if(isNumber) {
|
||||
v = vTemp
|
||||
}
|
||||
}
|
||||
rowObject[headers[j]] = v
|
||||
isEmpty = false
|
||||
}
|
||||
// determine if this row is empty
|
||||
if (!isEmpty) {
|
||||
outputObjects.push(rowObject)
|
||||
}
|
||||
}
|
||||
finalResult.data = outputObjects
|
||||
delete finalResult.firstRowIsHeader
|
||||
return finalResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes a cell in a CSV string if necessary. Addiionally, any double quotes inside the cell will be escaped as per RFC 4180 2.6 (https://datatracker.ietf.org/doc/html/rfc4180#section-2).
|
||||
* @param {string} cell - the string to quote
|
||||
* @param {*} options - options
|
||||
* @param {string} [options.quote='"'] - the quote character
|
||||
* @param {string} [options.separator=','] - the separator character
|
||||
* @param {string[]} [options.quoteables] - an array of characters that, when encountered, will trigger the application of outer quotes
|
||||
* @returns
|
||||
*/
|
||||
function quoteCell(cell, { quote = '"', separator = ",", quoteables } = {
|
||||
quote: '"',
|
||||
separator: ",",
|
||||
quoteables: [quote, separator, '\r', '\n']
|
||||
}) {
|
||||
quoteables = quoteables || [quote, separator, '\r', '\n'];
|
||||
|
||||
let doubleUp = false;
|
||||
if (cell.indexOf(quote) !== -1) { // add double quotes if any quotes
|
||||
doubleUp = true;
|
||||
}
|
||||
const quoteChar = quoteables.some(q => cell.includes(q)) ? quote : '';
|
||||
return quoteChar + (doubleUp ? cell.replace(/"/g, '""') : cell) + quoteChar;
|
||||
}
|
||||
|
||||
// #region Custom Error Classes
|
||||
class ParseError extends Error {
|
||||
/**
|
||||
* @param {string} message - the error message
|
||||
* @param {number} cursor - the cursor index where the error occurred
|
||||
*/
|
||||
constructor(message, cursor) {
|
||||
super(message)
|
||||
this.name = 'ParseError'
|
||||
this.cursor = cursor
|
||||
}
|
||||
}
|
||||
|
||||
class UnquotedQuoteError extends ParseError {
|
||||
/**
|
||||
* @param {number} cursor - the cursor index where the error occurred
|
||||
*/
|
||||
constructor(cursor) {
|
||||
super('Quote found in the middle of an unquoted field', cursor)
|
||||
this.name = 'UnquotedQuoteError'
|
||||
}
|
||||
}
|
||||
|
||||
class DataAfterCloseError extends ParseError {
|
||||
/**
|
||||
* @param {number} cursor - the cursor index where the error occurred
|
||||
*/
|
||||
constructor(cursor) {
|
||||
super('Data found after closing quote', cursor)
|
||||
this.name = 'DataAfterCloseError'
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
exports.parse = parse
|
||||
exports.quoteCell = quoteCell
|
||||
exports.ParseError = ParseError
|
||||
exports.UnquotedQuoteError = UnquotedQuoteError
|
||||
exports.DataAfterCloseError = DataAfterCloseError
|
@@ -15,7 +15,11 @@
|
||||
-->
|
||||
|
||||
<script type="text/html" data-template-name="split">
|
||||
<div class="form-row"><span data-i18n="[html]split.intro"></span></div>
|
||||
<!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label>
|
||||
<input type="text" id="node-input-property" style="width:70%;"/>
|
||||
</div>
|
||||
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label>
|
||||
@@ -39,10 +43,9 @@
|
||||
<label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label>
|
||||
<input type="text" id="node-input-addname" style="width:70%">
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
@@ -57,7 +60,8 @@
|
||||
arraySplt: {value:1},
|
||||
arraySpltType: {value:"len"},
|
||||
stream: {value:false},
|
||||
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })}
|
||||
addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
|
||||
property: {value:"payload",required:true}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
@@ -69,6 +73,10 @@
|
||||
return this.name?"node_label_italic":"";
|
||||
},
|
||||
oneditprepare: function() {
|
||||
if (this.property === undefined) {
|
||||
$("#node-input-property").val("payload");
|
||||
}
|
||||
$("#node-input-property").typedInput({default:'msg',types:['msg']});
|
||||
$("#node-input-splt").typedInput({
|
||||
default: 'str',
|
||||
typeField: $("#node-input-spltType"),
|
||||
|
@@ -19,13 +19,13 @@ module.exports = function(RED) {
|
||||
|
||||
function sendArray(node,msg,array,send) {
|
||||
for (var i = 0; i < array.length-1; i++) {
|
||||
msg.payload = array[i];
|
||||
RED.util.setMessageProperty(msg,node.property,array[i]);
|
||||
msg.parts.index = node.c++;
|
||||
if (node.stream !== true) { msg.parts.count = array.length; }
|
||||
send(RED.util.cloneMessage(msg));
|
||||
}
|
||||
if (node.stream !== true) {
|
||||
msg.payload = array[i];
|
||||
RED.util.setMessageProperty(msg,node.property,array[i]);
|
||||
msg.parts.index = node.c++;
|
||||
msg.parts.count = array.length;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -40,10 +40,12 @@ module.exports = function(RED) {
|
||||
node.stream = n.stream;
|
||||
node.spltType = n.spltType || "str";
|
||||
node.addname = n.addname || "";
|
||||
node.property = n.property||"payload";
|
||||
try {
|
||||
if (node.spltType === "str") {
|
||||
this.splt = (n.splt || "\\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t").replace(/\\e/g,"\e").replace(/\\f/g,"\f").replace(/\\0/g,"\0");
|
||||
} else if (node.spltType === "bin") {
|
||||
}
|
||||
else if (node.spltType === "bin") {
|
||||
var spltArray = JSON.parse(n.splt);
|
||||
if (Array.isArray(spltArray)) {
|
||||
this.splt = Buffer.from(spltArray);
|
||||
@@ -51,7 +53,8 @@ module.exports = function(RED) {
|
||||
throw new Error("not an array");
|
||||
}
|
||||
this.spltBuffer = spltArray;
|
||||
} else if (node.spltType === "len") {
|
||||
}
|
||||
else if (node.spltType === "len") {
|
||||
this.splt = parseInt(n.splt);
|
||||
if (isNaN(this.splt) || this.splt < 1) {
|
||||
throw new Error("invalid split length: "+n.splt);
|
||||
@@ -69,18 +72,22 @@ module.exports = function(RED) {
|
||||
node.buffer = Buffer.from([]);
|
||||
node.pendingDones = [];
|
||||
this.on("input", function(msg, send, done) {
|
||||
if (msg.hasOwnProperty("payload")) {
|
||||
var value = RED.util.getMessageProperty(msg,node.property);
|
||||
if (value !== undefined) {
|
||||
if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack
|
||||
else { msg.parts = {}; }
|
||||
msg.parts.id = RED.util.generateId(); // generate a random id
|
||||
if (node.property !== "payload") {
|
||||
msg.parts.property = node.property;
|
||||
}
|
||||
delete msg._msgid;
|
||||
if (typeof msg.payload === "string") { // Split String into array
|
||||
msg.payload = (node.remainder || "") + msg.payload;
|
||||
if (typeof value === "string") { // Split String into array
|
||||
value = (node.remainder || "") + value;
|
||||
msg.parts.type = "string";
|
||||
if (node.spltType === "len") {
|
||||
msg.parts.ch = "";
|
||||
msg.parts.len = node.splt;
|
||||
var count = msg.payload.length/node.splt;
|
||||
var count = value.length/node.splt;
|
||||
if (Math.floor(count) !== count) {
|
||||
count = Math.ceil(count);
|
||||
}
|
||||
@@ -89,9 +96,9 @@ module.exports = function(RED) {
|
||||
node.c = 0;
|
||||
}
|
||||
var pos = 0;
|
||||
var data = msg.payload;
|
||||
var data = value;
|
||||
for (var i=0; i<count-1; i++) {
|
||||
msg.payload = data.substring(pos,pos+node.splt);
|
||||
RED.util.setMessageProperty(msg,node.property,data.substring(pos,pos+node.splt));
|
||||
msg.parts.index = node.c++;
|
||||
pos += node.splt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -102,7 +109,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
node.remainder = data.substring(pos);
|
||||
if ((node.stream !== true) || (node.remainder.length === node.splt)) {
|
||||
msg.payload = node.remainder;
|
||||
RED.util.setMessageProperty(msg,node.property,node.remainder);
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
node.pendingDones.forEach(d => d());
|
||||
@@ -119,47 +126,48 @@ module.exports = function(RED) {
|
||||
if (!node.spltBufferString) {
|
||||
node.spltBufferString = node.splt.toString();
|
||||
}
|
||||
a = msg.payload.split(node.spltBufferString);
|
||||
a = value.split(node.spltBufferString);
|
||||
msg.parts.ch = node.spltBuffer; // pass the split char to other end for rejoin
|
||||
} else if (node.spltType === "str") {
|
||||
a = msg.payload.split(node.splt);
|
||||
a = value.split(node.splt);
|
||||
msg.parts.ch = node.splt; // pass the split char to other end for rejoin
|
||||
}
|
||||
sendArray(node,msg,a,send);
|
||||
done();
|
||||
}
|
||||
}
|
||||
else if (Array.isArray(msg.payload)) { // then split array into messages
|
||||
else if (Array.isArray(value)) { // then split array into messages
|
||||
msg.parts.type = "array";
|
||||
var count = msg.payload.length/node.arraySplt;
|
||||
var count = value.length/node.arraySplt;
|
||||
if (Math.floor(count) !== count) {
|
||||
count = Math.ceil(count);
|
||||
}
|
||||
msg.parts.count = count;
|
||||
var pos = 0;
|
||||
var data = msg.payload;
|
||||
var data = value;
|
||||
msg.parts.len = node.arraySplt;
|
||||
for (var i=0; i<count; i++) {
|
||||
msg.payload = data.slice(pos,pos+node.arraySplt);
|
||||
var m = data.slice(pos,pos+node.arraySplt);
|
||||
if (node.arraySplt === 1) {
|
||||
msg.payload = msg.payload[0];
|
||||
m = m[0];
|
||||
}
|
||||
RED.util.setMessageProperty(msg,node.property,m);
|
||||
msg.parts.index = i;
|
||||
pos += node.arraySplt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
}
|
||||
done();
|
||||
}
|
||||
else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) {
|
||||
else if ((typeof value === "object") && !Buffer.isBuffer(value)) {
|
||||
var j = 0;
|
||||
var l = Object.keys(msg.payload).length;
|
||||
var pay = msg.payload;
|
||||
var l = Object.keys(value).length;
|
||||
var pay = value;
|
||||
msg.parts.type = "object";
|
||||
for (var p in pay) {
|
||||
if (pay.hasOwnProperty(p)) {
|
||||
msg.payload = pay[p];
|
||||
RED.util.setMessageProperty(msg,node.property,pay[p]);
|
||||
if (node.addname !== "") {
|
||||
msg[node.addname] = p;
|
||||
RED.util.setMessageProperty(msg,node.addname,p);
|
||||
}
|
||||
msg.parts.key = p;
|
||||
msg.parts.index = j;
|
||||
@@ -170,9 +178,9 @@ module.exports = function(RED) {
|
||||
}
|
||||
done();
|
||||
}
|
||||
else if (Buffer.isBuffer(msg.payload)) {
|
||||
var len = node.buffer.length + msg.payload.length;
|
||||
var buff = Buffer.concat([node.buffer, msg.payload], len);
|
||||
else if (Buffer.isBuffer(value)) {
|
||||
var len = node.buffer.length + value.length;
|
||||
var buff = Buffer.concat([node.buffer, value], len);
|
||||
msg.parts.type = "buffer";
|
||||
if (node.spltType === "len") {
|
||||
var count = buff.length/node.splt;
|
||||
@@ -186,7 +194,7 @@ module.exports = function(RED) {
|
||||
var pos = 0;
|
||||
msg.parts.len = node.splt;
|
||||
for (var i=0; i<count-1; i++) {
|
||||
msg.payload = buff.slice(pos,pos+node.splt);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(pos,pos+node.splt));
|
||||
msg.parts.index = node.c++;
|
||||
pos += node.splt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -197,7 +205,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
node.buffer = buff.slice(pos);
|
||||
if ((node.stream !== true) || (node.buffer.length === node.splt)) {
|
||||
msg.payload = node.buffer;
|
||||
RED.util.setMessageProperty(msg,node.property,node.buffer);
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
node.pendingDones.forEach(d => d());
|
||||
@@ -230,7 +238,7 @@ module.exports = function(RED) {
|
||||
var i = 0, p = 0;
|
||||
pos = buff.indexOf(node.splt);
|
||||
while (pos > -1) {
|
||||
msg.payload = buff.slice(p,pos);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(p,pos));
|
||||
msg.parts.index = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
i++;
|
||||
@@ -242,7 +250,7 @@ module.exports = function(RED) {
|
||||
node.pendingDones = [];
|
||||
}
|
||||
if ((node.stream !== true) && (p < buff.length)) {
|
||||
msg.payload = buff.slice(p,buff.length);
|
||||
RED.util.setMessageProperty(msg,node.property,buff.slice(p,buff.length));
|
||||
msg.parts.index = node.c++;
|
||||
msg.parts.count = node.c++;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
@@ -298,7 +306,6 @@ module.exports = function(RED) {
|
||||
return exp
|
||||
}
|
||||
|
||||
|
||||
function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) {
|
||||
var msgInfo = msgInfos.shift();
|
||||
exp.assign("I", msgInfo.msg.parts.index);
|
||||
@@ -515,13 +522,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 +596,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);
|
||||
@@ -618,6 +625,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 +727,8 @@ module.exports = function(RED) {
|
||||
completeSend(partId)
|
||||
}, node.timer)
|
||||
}
|
||||
if (node.mode === "auto") { inflight[partId].prop = msg.parts.property; }
|
||||
else { inflight[partId].prop = node.property; }
|
||||
}
|
||||
inflight[partId].dones.push(done);
|
||||
|
||||
|
@@ -849,7 +849,13 @@
|
||||
"newline": "Newline",
|
||||
"usestrings": "parse numerical values",
|
||||
"include_empty_strings": "include empty strings",
|
||||
"include_null_values": "include null values"
|
||||
"include_null_values": "include null values",
|
||||
"spec": "Parser"
|
||||
},
|
||||
"spec": {
|
||||
"rfc": "RFC4180",
|
||||
"legacy": "Legacy",
|
||||
"legacy_warning": "Legacy mode will be removed in a future release."
|
||||
},
|
||||
"placeholder": {
|
||||
"columns": "comma-separated column names"
|
||||
@@ -878,6 +884,7 @@
|
||||
"once": "send headers once, until msg.reset"
|
||||
},
|
||||
"errors": {
|
||||
"bad_template": "Malformed columns template.",
|
||||
"csv_js": "This node only handles CSV strings or js objects.",
|
||||
"obj_csv": "No columns template specified for object -> CSV.",
|
||||
"bad_csv": "Malformed CSV data - output probably corrupt."
|
||||
@@ -1001,7 +1008,7 @@
|
||||
"tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process."
|
||||
},
|
||||
"split": {
|
||||
"split": "split",
|
||||
"split": "Split",
|
||||
"intro": "Split <code>msg.payload</code> based on type:",
|
||||
"object": "<b>Object</b>",
|
||||
"objectSend": "Send a message for each key/value pair",
|
||||
|
@@ -30,6 +30,8 @@
|
||||
before being sent.</p>
|
||||
<p>If <code>msg._session</code> is not present the payload is
|
||||
sent to <b>all</b> connected clients.</p>
|
||||
<p>In Reply-to mode, setting <code>msg.reset = true</code> will reset the connection
|
||||
specified by _session.id, or all connections if no _session.id is specified.</p>
|
||||
<p><b>Note: </b>On some systems you may need root or administrator access
|
||||
to access ports below 1024.</p>
|
||||
</script>
|
||||
@@ -40,6 +42,8 @@
|
||||
returned characters into a fixed buffer, match a specified character before returning,
|
||||
wait a fixed timeout from first reply and then return, sit and wait for data, or send then close the connection
|
||||
immediately, without waiting for a reply.</p>
|
||||
<p>If in sit and wait mode (remain connected) you can send <code>msg.reset = true</code> or <code>msg.reset = "host:port"</code> to force a break in
|
||||
the connection and an automatic reconnection.</p>
|
||||
<p>The response will be output in <code>msg.payload</code> as a buffer, so you may want to .toString() it.</p>
|
||||
<p>If you leave tcp host or port blank they must be set by using the <code>msg.host</code> and <code>msg.port</code> properties in every message sent to the node.</p>
|
||||
</script>
|
||||
|
@@ -36,7 +36,9 @@
|
||||
</dl>
|
||||
<h3>Details</h3>
|
||||
<p>The column template can contain an ordered list of column names. When converting CSV to an object, the column names
|
||||
will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV.</p>
|
||||
will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV.
|
||||
<p>When the RFC parser is selected, the column template must be compliant with RFC4180.</p>
|
||||
</p>
|
||||
<p>When converting to CSV, the columns template is used to identify which properties to extract from the object and in what order.</p>
|
||||
<p>If the columns template is blank then you can use a simple comma separated list of properties supplied in <code>msg.columns</code> to
|
||||
determine what to extract and in what order. If neither are present then all the object properties are output in the order
|
||||
@@ -49,4 +51,5 @@
|
||||
<p>If outputting multiple messages they will have their <code>parts</code> property set and form a complete message sequence.</p>
|
||||
<p>If the node is set to only send column headers once, then setting <code>msg.reset</code> to any value will cause the node to resend the headers.</p>
|
||||
<p><b>Note:</b> the column template must be comma separated - even if a different separator is chosen for the data.</p>
|
||||
<p><b>Note:</b> in RFC mode, catchable errors will be thrown for malformed CSV headers and invalid input payload data</p>
|
||||
</script>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/nodes",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -264,7 +264,7 @@ async function installModule(moduleDetails) {
|
||||
"module": moduleDetails.module,
|
||||
"version": moduleDetails.version,
|
||||
"dir": installDir,
|
||||
"args": ["--production","--engine-strict"]
|
||||
"args": ["--omit=dev","--engine-strict"]
|
||||
}
|
||||
return hooks.trigger("preInstall", triggerPayload).then((result) => {
|
||||
// preInstall passed
|
||||
|
@@ -215,7 +215,7 @@ async function installModule(module,version,url) {
|
||||
"dir": installDir,
|
||||
"isExisting": isExisting,
|
||||
"isUpgrade": isUpgrade,
|
||||
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production','--engine-strict']
|
||||
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--omit=dev','--engine-strict']
|
||||
}
|
||||
|
||||
return hooks.trigger("preInstall", triggerPayload).then((result) => {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/registry",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/util": "3.1.5",
|
||||
"@node-red/util": "4.0.0-dev",
|
||||
"clone": "2.1.2",
|
||||
"fs-extra": "11.1.1",
|
||||
"semver": "7.5.4",
|
||||
|
@@ -68,6 +68,7 @@ var api = module.exports = {
|
||||
* @param {String} opts.store - the context store
|
||||
* @param {String} opts.key - the context key
|
||||
* @param {Object} opts.req - the request to log (optional)
|
||||
* @param {Boolean} opts.keysOnly - whether to return keys only
|
||||
* @return {Promise} - the node information
|
||||
* @memberof @node-red/runtime_context
|
||||
*/
|
||||
@@ -102,6 +103,15 @@ var api = module.exports = {
|
||||
if (key) {
|
||||
store = store || availableStores.default;
|
||||
ctx.get(key,store,function(err, v) {
|
||||
if (opts.keysOnly) {
|
||||
if (Array.isArray(v)) {
|
||||
resolve({ [store]: { format: `array[${v.length}]`}})
|
||||
} else if (typeof v === 'object') {
|
||||
resolve({ [store]: { keys: Object.keys(v), format: 'Object' } })
|
||||
} else {
|
||||
resolve({ [store]: { keys: [] }})
|
||||
}
|
||||
}
|
||||
var encoded = util.encodeObject({msg:v});
|
||||
if (store !== availableStores.default) {
|
||||
encoded.store = store;
|
||||
@@ -118,32 +128,58 @@ var api = module.exports = {
|
||||
stores = [store];
|
||||
}
|
||||
|
||||
|
||||
var result = {};
|
||||
var c = stores.length;
|
||||
var errorReported = false;
|
||||
stores.forEach(function(store) {
|
||||
exportContextStore(scope,ctx,store,result,function(err) {
|
||||
if (err) {
|
||||
// TODO: proper error reporting
|
||||
if (!errorReported) {
|
||||
errorReported = true;
|
||||
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
|
||||
var err = new Error();
|
||||
err.code = "unexpected_error";
|
||||
err.status = 400;
|
||||
return reject(err);
|
||||
if (opts.keysOnly) {
|
||||
ctx.keys(store,function(err, keys) {
|
||||
if (err) {
|
||||
// TODO: proper error reporting
|
||||
if (!errorReported) {
|
||||
errorReported = true;
|
||||
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
|
||||
var err = new Error();
|
||||
err.code = "unexpected_error";
|
||||
err.status = 400;
|
||||
return reject(err);
|
||||
}
|
||||
return
|
||||
}
|
||||
result[store] = { keys }
|
||||
c--;
|
||||
if (c === 0) {
|
||||
if (!errorReported) {
|
||||
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
exportContextStore(scope,ctx,store,result,function(err) {
|
||||
if (err) {
|
||||
// TODO: proper error reporting
|
||||
if (!errorReported) {
|
||||
errorReported = true;
|
||||
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"}, opts.req);
|
||||
var err = new Error();
|
||||
err.code = "unexpected_error";
|
||||
err.status = 400;
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
c--;
|
||||
if (c === 0) {
|
||||
if (!errorReported) {
|
||||
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
c--;
|
||||
if (c === 0) {
|
||||
if (!errorReported) {
|
||||
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key},opts.req);
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
@@ -154,7 +154,7 @@ function start() {
|
||||
log.info(log._("runtime.version",{component:"Node.js ",version:process.version}));
|
||||
if (settings.UNSUPPORTED_VERSION) {
|
||||
log.error("*****************************************************************");
|
||||
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=8.9.0"})+" *");
|
||||
log.error("* "+log._("runtime.unsupported_version",{component:"Node.js",version:process.version,requires: ">=18"})+" *");
|
||||
log.error("*****************************************************************");
|
||||
events.emit("runtime-event",{id:"runtime-unsupported-version",payload:{type:"error",text:"notification.errors.unsupportedVersion"},retain:true});
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/runtime",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"repository": {
|
||||
@@ -16,8 +16,8 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/registry": "3.1.5",
|
||||
"@node-red/util": "3.1.5",
|
||||
"@node-red/registry": "4.0.0-dev",
|
||||
"@node-red/util": "4.0.0-dev",
|
||||
"async-mutex": "0.4.0",
|
||||
"clone": "2.1.2",
|
||||
"express": "4.18.2",
|
||||
|
23
packages/node_modules/@node-red/util/lib/util.js
vendored
23
packages/node_modules/@node-red/util/lib/util.js
vendored
@@ -636,7 +636,15 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
|
||||
} else if (type === 're') {
|
||||
result = new RegExp(value);
|
||||
} else if (type === 'date') {
|
||||
result = Date.now();
|
||||
if (!value) {
|
||||
result = Date.now();
|
||||
} else if (value === 'object') {
|
||||
result = new Date()
|
||||
} else if (value === 'iso') {
|
||||
result = (new Date()).toISOString()
|
||||
} else {
|
||||
result = moment().format(value)
|
||||
}
|
||||
} else if (type === 'bin') {
|
||||
var data = JSON.parse(value);
|
||||
if (Array.isArray(data) || (typeof(data) === "string")) {
|
||||
@@ -769,12 +777,15 @@ function evaluateJSONataExpression(expr,msg,callback) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.warn('Deprecated API warning: Calls to RED.util.evaluateJSONataExpression must include a callback. '+
|
||||
'This will not be optional in Node-RED 4.0. Please identify the node from the following stack '+
|
||||
'and check for an update on npm. If none is available, please notify the node author.')
|
||||
log.warn(new Error().stack)
|
||||
const error = new Error('Calls to RED.util.evaluateJSONataExpression must include a callback.')
|
||||
throw error
|
||||
}
|
||||
return expr.evaluate(context, bindings, callback);
|
||||
|
||||
expr.evaluate(context, bindings).then(result => {
|
||||
callback(null, result)
|
||||
}).catch(err => {
|
||||
callback(err)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@node-red/util",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -18,7 +18,7 @@
|
||||
"fs-extra": "11.1.1",
|
||||
"i18next": "21.10.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"jsonata": "1.8.6",
|
||||
"jsonata": "2.0.4",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"moment": "2.29.4",
|
||||
"moment-timezone": "0.5.43"
|
||||
|
3
packages/node_modules/node-red/lib/red.js
vendored
3
packages/node_modules/node-red/lib/red.js
vendored
@@ -33,8 +33,7 @@ if (NODE_MAJOR_VERSION >= 16) {
|
||||
|
||||
function checkVersion(userSettings) {
|
||||
var semver = require('semver');
|
||||
if (!semver.satisfies(process.version,">=14.0.0")) {
|
||||
// TODO: in the future, make this a hard error.
|
||||
if (!semver.satisfies(process.version,">=18.0.0")) {
|
||||
// var e = new Error("Unsupported version of Node.js");
|
||||
// e.code = "unsupported_version";
|
||||
// throw e;
|
||||
|
12
packages/node_modules/node-red/package.json
vendored
12
packages/node_modules/node-red/package.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-red",
|
||||
"version": "3.1.5",
|
||||
"version": "4.0.0-dev",
|
||||
"description": "Low-code programming for event-driven applications",
|
||||
"homepage": "https://nodered.org",
|
||||
"license": "Apache-2.0",
|
||||
@@ -31,10 +31,10 @@
|
||||
"flow"
|
||||
],
|
||||
"dependencies": {
|
||||
"@node-red/editor-api": "3.1.5",
|
||||
"@node-red/runtime": "3.1.5",
|
||||
"@node-red/util": "3.1.5",
|
||||
"@node-red/nodes": "3.1.5",
|
||||
"@node-red/editor-api": "4.0.0-dev",
|
||||
"@node-red/runtime": "4.0.0-dev",
|
||||
"@node-red/util": "4.0.0-dev",
|
||||
"@node-red/nodes": "4.0.0-dev",
|
||||
"basic-auth": "2.0.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"express": "4.18.2",
|
||||
@@ -47,6 +47,6 @@
|
||||
"bcrypt": "5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
9
packages/node_modules/node-red/red.js
vendored
9
packages/node_modules/node-red/red.js
vendored
@@ -26,6 +26,13 @@ if (process.argv[2] === 'admin') {
|
||||
return;
|
||||
}
|
||||
|
||||
var semver = require('semver');
|
||||
if (!semver.satisfies(process.version, ">=18.0.0")) {
|
||||
console.log("Unsupported version of Node.js:", process.version);
|
||||
console.log("Node-RED requires Node.js v18 or later");
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var util = require("util");
|
||||
@@ -346,7 +353,7 @@ httpsPromise.then(function(startupHttps) {
|
||||
} catch(err) {
|
||||
if (err.code == "unsupported_version") {
|
||||
console.log("Unsupported version of Node.js:",process.version);
|
||||
console.log("Node-RED requires Node.js v8.9.0 or later");
|
||||
console.log("Node-RED requires Node.js v18 or later");
|
||||
} else {
|
||||
console.log("Failed to start server:");
|
||||
if (err.stack) {
|
||||
|
4
packages/node_modules/node-red/settings.js
vendored
4
packages/node_modules/node-red/settings.js
vendored
@@ -449,6 +449,7 @@ module.exports = {
|
||||
* - ui (for use with Node-RED Dashboard)
|
||||
* - debugUseColors
|
||||
* - debugMaxLength
|
||||
* - debugStatusLength
|
||||
* - execMaxBufferSize
|
||||
* - httpRequestTimeout
|
||||
* - mqttReconnectTime
|
||||
@@ -504,6 +505,9 @@ module.exports = {
|
||||
/** The maximum length, in characters, of any message sent to the debug sidebar tab */
|
||||
debugMaxLength: 1000,
|
||||
|
||||
/** The maximum length, in characters, of status messages under the debug node */
|
||||
//debugStatusLength: 32,
|
||||
|
||||
/** Maximum buffer size for the exec node. Defaults to 10Mb */
|
||||
//execMaxBufferSize: 10000000,
|
||||
|
||||
|
@@ -1718,9 +1718,13 @@ describe('function node', function() {
|
||||
describe("init function", function() {
|
||||
|
||||
it('should delay handling messages until init completes', function(done) {
|
||||
const timeoutMS = 200;
|
||||
// Since helper.load uses process.nextTick timers might occasionally finish
|
||||
// a couple of milliseconds too early, so give some leeway to the check.
|
||||
const timeoutCheckMargin = 5;
|
||||
var flow = [{id:"n1",type:"function",wires:[["n2"]],initialize: `
|
||||
return new Promise((resolve,reject) => {
|
||||
setTimeout(resolve,200)
|
||||
setTimeout(resolve, ${timeoutMS});
|
||||
})`,
|
||||
func:"return msg;"
|
||||
},
|
||||
@@ -1733,9 +1737,10 @@ describe('function node', function() {
|
||||
msg.delta = Date.now() - msg.payload;
|
||||
receivedMsgs.push(msg)
|
||||
if (receivedMsgs.length === 5) {
|
||||
var errors = receivedMsgs.filter(msg => msg.delta < 200)
|
||||
let deltas = receivedMsgs.map(msg => msg.delta);
|
||||
var errors = deltas.filter(delta => delta < (timeoutMS - timeoutCheckMargin))
|
||||
if (errors.length > 0) {
|
||||
done(new Error(`Message received before init completed - was ${msg.delta} expected >300`))
|
||||
done(new Error(`Message received before init completed - delta values ${JSON.stringify(deltas)} expected to be > ${timeoutMS - timeoutCheckMargin}`))
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -66,6 +66,27 @@ describe('SPLIT node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should split an array on a sub-property into multiple messages', function(done) {
|
||||
var flow = [{id:"sn1", type:"split", property:"foo", wires:[["sn2"]]},
|
||||
{id:"sn2", type:"helper"}];
|
||||
helper.load(splitNode, flow, function() {
|
||||
var sn1 = helper.getNode("sn1");
|
||||
var sn2 = helper.getNode("sn2");
|
||||
sn2.on("input", function(msg) {
|
||||
msg.should.have.property("parts");
|
||||
msg.parts.should.have.property("count",4);
|
||||
msg.parts.should.have.property("type","array");
|
||||
msg.parts.should.have.property("index");
|
||||
msg.parts.should.have.property("property","foo");
|
||||
if (msg.parts.index === 0) { msg.foo.should.equal(1); }
|
||||
if (msg.parts.index === 1) { msg.foo.should.equal(2); }
|
||||
if (msg.parts.index === 2) { msg.foo.should.equal(3); }
|
||||
if (msg.parts.index === 3) { msg.foo.should.equal(4); done(); }
|
||||
});
|
||||
sn1.receive({foo:[1,2,3,4]});
|
||||
});
|
||||
});
|
||||
|
||||
it('should split an array into multiple messages of a specified size', function(done) {
|
||||
var flow = [{id:"sn1", type:"split", wires:[["sn2"]], arraySplt:3, arraySpltType:"len"},
|
||||
{id:"sn2", type:"helper"}];
|
||||
@@ -108,6 +129,31 @@ describe('SPLIT node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should split an object sub property into pieces', function(done) {
|
||||
var flow = [{id:"sn1", type:"split", property:"foo.bar",wires:[["sn2"]]},
|
||||
{id:"sn2", type:"helper"}];
|
||||
helper.load(splitNode, flow, function() {
|
||||
var sn1 = helper.getNode("sn1");
|
||||
var sn2 = helper.getNode("sn2");
|
||||
var count = 0;
|
||||
sn2.on("input", function(msg) {
|
||||
msg.should.have.property("foo");
|
||||
msg.foo.should.have.property("bar");
|
||||
msg.should.have.property("parts");
|
||||
msg.parts.should.have.property("type","object");
|
||||
msg.parts.should.have.property("key");
|
||||
msg.parts.should.have.property("count");
|
||||
msg.parts.should.have.property("index");
|
||||
msg.parts.should.have.property("property","foo.bar");
|
||||
msg.topic.should.equal("foo");
|
||||
if (msg.parts.index === 0) { msg.foo.bar.should.equal(1); }
|
||||
if (msg.parts.index === 1) { msg.foo.bar.should.equal("2"); }
|
||||
if (msg.parts.index === 2) { msg.foo.bar.should.equal(true); done(); }
|
||||
});
|
||||
sn1.receive({topic:"foo",foo:{bar:{a:1,b:"2",c:true}}});
|
||||
});
|
||||
});
|
||||
|
||||
it('should split an object into pieces and overwrite their topics', function(done) {
|
||||
var flow = [{id:"sn1", type:"split", addname:"topic", wires:[["sn2"]]},
|
||||
{id:"sn2", type:"helper"}];
|
||||
@@ -516,6 +562,7 @@ describe('JOIN node', function() {
|
||||
n1.receive({payload:{a:1}});
|
||||
});
|
||||
});
|
||||
|
||||
it('should join things into an array ignoring msg.parts.index in manual mode', function(done) {
|
||||
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",",mode:"custom"},
|
||||
{id:"n2", type:"helper"}];
|
||||
@@ -562,6 +609,32 @@ describe('JOIN node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should join things into an array on a sub property in auto mode', function(done) {
|
||||
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",", mode:"auto"},
|
||||
{id:"n2", type:"helper"}];
|
||||
helper.load(joinNode, flow, function() {
|
||||
var n1 = helper.getNode("n1");
|
||||
var n2 = helper.getNode("n2");
|
||||
n2.on("input", function(msg) {
|
||||
try {
|
||||
msg.should.have.property("foo");
|
||||
msg.foo.should.have.property("bar");
|
||||
msg.foo.bar.should.be.an.Array();
|
||||
msg.foo.bar[0].should.equal("A");
|
||||
msg.foo.bar[1].should.equal("B");
|
||||
//msg.payload[2].a.should.equal(1);
|
||||
done();
|
||||
}
|
||||
catch(e) {done(e);}
|
||||
});
|
||||
n1.receive({foo:{bar:"A"}, parts:{id:1, type:"array", len:1, index:0, count:4, property:"foo.bar"}});
|
||||
n1.receive({foo:{bar:"B"}, parts:{id:1, type:"array", len:1, index:1, count:4, property:"foo.bar"}});
|
||||
n1.receive({foo:{bar:"C"}, parts:{id:1, type:"array", len:1, index:2, count:4, property:"foo.bar"}});
|
||||
n1.receive({foo:{bar:"D"}, parts:{id:1, type:"array", len:1, index:3, count:4, property:"foo.bar"}});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should join strings into a buffer after a count', function(done) {
|
||||
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:2, build:"buffer", joinerType:"bin", joiner:"", mode:"custom"},
|
||||
{id:"n2", type:"helper"}];
|
||||
@@ -639,6 +712,35 @@ describe('JOIN node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge sub property objects', function(done) {
|
||||
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:5, property:"foo.bar", build:"merged", mode:"custom"},
|
||||
{id:"n2", type:"helper"}];
|
||||
helper.load(joinNode, flow, function() {
|
||||
var n1 = helper.getNode("n1");
|
||||
var n2 = helper.getNode("n2");
|
||||
n2.on("input", function(msg) {
|
||||
try {
|
||||
msg.should.have.property("foo");
|
||||
msg.foo.should.have.property("bar");
|
||||
msg.foo.bar.should.have.property("a",1);
|
||||
msg.foo.bar.should.have.property("b",2);
|
||||
msg.foo.bar.should.have.property("c",3);
|
||||
msg.foo.bar.should.have.property("d",4);
|
||||
msg.foo.bar.should.have.property("e",5);
|
||||
done();
|
||||
}
|
||||
catch(e) { done(e)}
|
||||
});
|
||||
n1.receive({foo:{bar:{a:9}, topic:"f"}});
|
||||
n1.receive({foo:{bar:{a:1}, topic:"a"}});
|
||||
n1.receive({foo:{bar:{b:9}, topic:"b"}});
|
||||
n1.receive({foo:{bar:{b:2}, topic:"b"}});
|
||||
n1.receive({foo:{bar:{c:3}, topic:"c"}});
|
||||
n1.receive({foo:{bar:{d:4}, topic:"d"}});
|
||||
n1.receive({foo:{bar:{e:5}, topic:"e"}});
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge full msg objects', function(done) {
|
||||
var flow = [{id:"n1", type:"join", wires:[["n2"]], count:6, build:"merged", mode:"custom", propertyType:"full", property:""},
|
||||
{id:"n2", type:"helper"}];
|
||||
|
@@ -379,10 +379,17 @@ describe("@node-red/util/util", function() {
|
||||
result = util.evaluateNodeProperty('','bool');
|
||||
result.should.be.false();
|
||||
});
|
||||
it('returns date',function() {
|
||||
it('returns date - default format',function() {
|
||||
var result = util.evaluateNodeProperty('','date');
|
||||
(Date.now() - result).should.be.approximately(0,50);
|
||||
});
|
||||
|
||||
it('returns date - iso format',function() {
|
||||
var result = util.evaluateNodeProperty('iso','date');
|
||||
// 2023-12-04T16:51:04.429Z
|
||||
/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z$/.test(result).should.be.true()
|
||||
});
|
||||
|
||||
it('returns bin', function () {
|
||||
var result = util.evaluateNodeProperty('[1, 2]','bin');
|
||||
result[0].should.eql(1);
|
||||
@@ -441,9 +448,16 @@ describe("@node-red/util/util", function() {
|
||||
},{});
|
||||
result.should.eql("123");
|
||||
});
|
||||
it('returns jsonata result', function () {
|
||||
var result = util.evaluateNodeProperty('$abs(-1)','jsonata',{},{});
|
||||
result.should.eql(1);
|
||||
it('returns jsonata result', function (done) {
|
||||
util.evaluateNodeProperty('$abs(-1)','jsonata',{},{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql(1);
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
it('returns null', function() {
|
||||
var result = util.evaluateNodeProperty(null,'null');
|
||||
@@ -601,51 +615,105 @@ describe("@node-red/util/util", function() {
|
||||
});
|
||||
});
|
||||
describe('evaluateJSONataExpression', function() {
|
||||
it('evaluates an expression', function() {
|
||||
it('evaluates an expression', function(done) {
|
||||
var expr = util.prepareJSONataExpression('payload',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
result.should.eql("hello");
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("hello");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('evaluates a legacyMode expression', function() {
|
||||
var expr = util.prepareJSONataExpression('msg.payload',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
result.should.eql("hello");
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("hello");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses flow context from an expression', function() {
|
||||
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
result.should.eql("bar");
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("bar");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses undefined environment variable from an expression', function() {
|
||||
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('');
|
||||
});
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses environment variable from an expression', function() {
|
||||
process.env.UTIL_ENV = 'foo';
|
||||
var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('foo');
|
||||
});
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("foo");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses moment from an expression', function() {
|
||||
var expr = util.prepareJSONataExpression('$moment("2020-05-27", "YYYY-MM-DD").add(7, "days").add(1, "months").format("YYYY-MM-DD")',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('2020-07-03');
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("2020-07-03");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('accesses moment-timezone from an expression', function() {
|
||||
var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{});
|
||||
var result = util.evaluateJSONataExpression(expr,{});
|
||||
result.should.eql('2013-11-18T19:55:00+08:00');
|
||||
util.evaluateJSONataExpression(expr,{}, (err, result) => {
|
||||
try {
|
||||
result.should.eql("2013-11-18T19:55:00+08:00");
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('handles non-existant flow context variable', function() {
|
||||
var expr = util.prepareJSONataExpression('$flowContext("nonExistant")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
should.not.exist(result);
|
||||
});
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
should.not.exist(result);
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('handles non-existant global context variable', function() {
|
||||
var expr = util.prepareJSONataExpression('$globalContext("nonExistant")',{context:function() { return {global:{get: function(key) { return {'foo':'bar'}[key]}}}}});
|
||||
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
|
||||
should.not.exist(result);
|
||||
util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
|
||||
try {
|
||||
should.not.exist(result);
|
||||
done()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
});
|
||||
});
|
||||
it('handles async flow context access', function(done) {
|
||||
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,store,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});
|
||||
|
Reference in New Issue
Block a user