node-red-contrib-prib-functions 0.23.2 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +36 -0
- package/README.md +153 -140
- package/columnar/columnar.html +258 -0
- package/columnar/columnar.js +1055 -0
- package/columnar/icons/columnar.svg +38 -0
- package/fileSystem/filesystem.html +299 -0
- package/fileSystem/filesystem.js +170 -0
- package/gitlab/gitlab.html +191 -0
- package/gitlab/gitlab.js +248 -0
- package/gitlab/icons/gitlab.svg +17 -0
- package/lib/AlphaBeta.js +32 -0
- package/lib/GraphDB.js +40 -9
- package/lib/MinMax.js +17 -0
- package/lib/Tree.js +64 -0
- package/lib/objectExtensions.js +28 -5
- package/lib/timeDimension.js +36 -0
- package/lib/typedInput.js +18 -2
- package/logisticRegression/icons/logisticregression.svg +22 -0
- package/logisticRegression/logisticRegression.html +136 -0
- package/logisticRegression/logisticRegression.js +83 -0
- package/package.json +21 -9
- package/test/02-graphdb.js +46 -0
- package/test/columnar.js +509 -0
- package/test/data/.config.nodes.json +114 -70
- package/test/data/.config.nodes.json.backup +104 -71
- package/test/data/.config.runtime.json +2 -1
- package/test/data/.config.runtime.json.backup +2 -1
- package/test/data/.config.users.json +3 -2
- package/test/data/.config.users.json.backup +3 -2
- package/test/data/.flow.json.backup +1545 -369
- package/test/data/flow.json +1457 -270
- package/test/data/package-lock.json +11 -11
- package/test/data/shares/.config.nodes.json +611 -0
- package/test/data/shares/.config.nodes.json.backup +589 -0
- package/test/data/shares/.config.runtime.json +5 -0
- package/test/data/shares/.config.runtime.json.backup +4 -0
- package/test/data/shares/.config.users.json +33 -0
- package/test/data/shares/.config.users.json.backup +33 -0
- package/test/data/shares/.flow.json.backup +230 -0
- package/test/data/shares/.flow_cred.json.backup +3 -0
- package/test/data/shares/flow.json +267 -0
- package/test/data/shares/flow_cred.json +3 -0
- package/test/data/shares/package.json +6 -0
- package/test/data/shares/settings.js +544 -0
- package/test/dataAnalysisExtensions.js +93 -93
- package/test/logisticRegression.js +379 -0
- package/test/transform.js +11 -11
- package/test/transformConfluence.js +4 -2
- package/test/transformNumPy.js +3 -1
- package/test/transformXLSX.js +4 -2
- package/test/transformXML.js +4 -2
- package/test-runner.js +400 -0
- package/test.parq +0 -0
- package/test_select.js +37 -0
- package/testing/test.js +8 -7
- package/transform/transform.html +23 -2
- package/transform/transform.js +239 -283
- package/transform/xlsx2.js +74 -0
package/test-runner.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Simple unit test for logistic regression node
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const LogisticRegression = require('logisticegression');
|
|
6
|
+
|
|
7
|
+
// counters used by both runners
|
|
8
|
+
let testsPassed = 0;
|
|
9
|
+
let testsFailed = 0;
|
|
10
|
+
|
|
11
|
+
// If the CLI includes the word 'transform' proceed to run the
|
|
12
|
+
// transformation node tests rather than the logistic regression
|
|
13
|
+
// suite. This avoids pulling mocha back in while still exercising
|
|
14
|
+
// the existing mocha-based files under test/transform*.js.
|
|
15
|
+
if (process.argv.includes('transform')) {
|
|
16
|
+
runTransformTests();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// If the CLI includes 'dataanalysis' run dataAnalysis tests
|
|
21
|
+
if (process.argv.includes('dataanalysis')) {
|
|
22
|
+
runDataAnalysisTests();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If the CLI includes 'columnar' run the columnar-format tests
|
|
27
|
+
if (process.argv.includes('columnar')) {
|
|
28
|
+
runColumnarTests();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function test(description, fn) {
|
|
34
|
+
try {
|
|
35
|
+
fn();
|
|
36
|
+
console.log(`✓ PASS: ${description}`);
|
|
37
|
+
testsPassed++;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`✗ FAIL: ${description}`);
|
|
40
|
+
console.error(` Error: ${err.message}`);
|
|
41
|
+
testsFailed++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// transform test emulation
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function callMaybeAsync(fn) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
try {
|
|
51
|
+
if (fn.length >= 1) {
|
|
52
|
+
fn(err => (err ? reject(err) : resolve()));
|
|
53
|
+
} else {
|
|
54
|
+
Promise.resolve(fn()).then(resolve, reject);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
reject(err);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runTransformTests() {
|
|
63
|
+
const path = require('path');
|
|
64
|
+
const fs = require('fs');
|
|
65
|
+
|
|
66
|
+
let beforeEachFns = [];
|
|
67
|
+
let afterEachFns = [];
|
|
68
|
+
let tests = [];
|
|
69
|
+
let currentDescribe = '';
|
|
70
|
+
|
|
71
|
+
global.describe = function(desc, fn) {
|
|
72
|
+
currentDescribe = desc;
|
|
73
|
+
fn();
|
|
74
|
+
};
|
|
75
|
+
global.beforeEach = function(fn) {
|
|
76
|
+
beforeEachFns.push(fn);
|
|
77
|
+
};
|
|
78
|
+
global.afterEach = function(fn) {
|
|
79
|
+
afterEachFns.push(fn);
|
|
80
|
+
};
|
|
81
|
+
global.it = function(desc, fn) {
|
|
82
|
+
const full = currentDescribe ? `${currentDescribe} ${desc}` : desc;
|
|
83
|
+
tests.push({desc: full, fn});
|
|
84
|
+
return { timeout: () => {} };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
console.log("Transform Node Tests\n" + "=".repeat(60));
|
|
88
|
+
|
|
89
|
+
const dir = path.join(__dirname, 'test');
|
|
90
|
+
const files = fs.readdirSync(dir).filter(f => /^transform.*\.js$/.test(f));
|
|
91
|
+
files.forEach(f => require(path.join(dir, f)));
|
|
92
|
+
|
|
93
|
+
for (const t of tests) {
|
|
94
|
+
for (const bf of beforeEachFns) await callMaybeAsync(bf);
|
|
95
|
+
try {
|
|
96
|
+
await callMaybeAsync(t.fn);
|
|
97
|
+
console.log(`✓ PASS: ${t.desc}`);
|
|
98
|
+
testsPassed++;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`✗ FAIL: ${t.desc}`);
|
|
101
|
+
console.error(` Error: ${err.message}`);
|
|
102
|
+
testsFailed++;
|
|
103
|
+
}
|
|
104
|
+
for (const af of afterEachFns) {
|
|
105
|
+
try {
|
|
106
|
+
await callMaybeAsync(af);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// ignore server-not-running errors which happen when
|
|
109
|
+
// cleanup double-closes the Node-RED test helper server.
|
|
110
|
+
if (err && err.code === 'ERR_SERVER_NOT_RUNNING') {
|
|
111
|
+
// no-op
|
|
112
|
+
} else {
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('\n' + '='.repeat(60));
|
|
120
|
+
console.log(`Tests passed: ${testsPassed}`);
|
|
121
|
+
console.log(`Tests failed: ${testsFailed}`);
|
|
122
|
+
console.log('='.repeat(60));
|
|
123
|
+
process.exit(testsFailed > 0 ? 1 : 0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// dataanalysis test emulation
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
async function runDataAnalysisTests() {
|
|
130
|
+
const path = require('path');
|
|
131
|
+
const fs = require('fs');
|
|
132
|
+
|
|
133
|
+
let beforeEachFns = [];
|
|
134
|
+
let afterEachFns = [];
|
|
135
|
+
let tests = [];
|
|
136
|
+
let currentDescribe = '';
|
|
137
|
+
|
|
138
|
+
global.describe = function(desc, fn) {
|
|
139
|
+
currentDescribe = desc;
|
|
140
|
+
fn();
|
|
141
|
+
};
|
|
142
|
+
global.beforeEach = function(fn) {
|
|
143
|
+
beforeEachFns.push(fn);
|
|
144
|
+
};
|
|
145
|
+
global.afterEach = function(fn) {
|
|
146
|
+
afterEachFns.push(fn);
|
|
147
|
+
};
|
|
148
|
+
global.it = function(desc, fn) {
|
|
149
|
+
const full = currentDescribe ? `${currentDescribe} ${desc}` : desc;
|
|
150
|
+
tests.push({desc: full, fn});
|
|
151
|
+
return { timeout: () => {} };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
console.log("Data Analysis Extension Tests\n" + "=".repeat(60));
|
|
155
|
+
|
|
156
|
+
const dir = path.join(__dirname, 'test');
|
|
157
|
+
const files = fs.readdirSync(dir).filter(f => /^dataAnalysisE.*\.js$/.test(f));
|
|
158
|
+
files.forEach(f => require(path.join(dir, f)));
|
|
159
|
+
|
|
160
|
+
for (const t of tests) {
|
|
161
|
+
for (const bf of beforeEachFns) await callMaybeAsync(bf);
|
|
162
|
+
try {
|
|
163
|
+
await callMaybeAsync(t.fn);
|
|
164
|
+
console.log(`✓ PASS: ${t.desc}`);
|
|
165
|
+
testsPassed++;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error(`✗ FAIL: ${t.desc}`);
|
|
168
|
+
console.error(` Error: ${err.message}`);
|
|
169
|
+
testsFailed++;
|
|
170
|
+
}
|
|
171
|
+
for (const af of afterEachFns) await callMaybeAsync(af);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('\n' + '='.repeat(60));
|
|
175
|
+
console.log(`Tests passed: ${testsPassed}`);
|
|
176
|
+
console.log(`Tests failed: ${testsFailed}`);
|
|
177
|
+
console.log('='.repeat(60));
|
|
178
|
+
process.exit(testsFailed > 0 ? 1 : 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// columnar test emulation
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
async function runColumnarTests() {
|
|
185
|
+
const path = require('path');
|
|
186
|
+
const fs = require('fs');
|
|
187
|
+
|
|
188
|
+
let beforeEachFns = [];
|
|
189
|
+
let afterEachFns = [];
|
|
190
|
+
let tests = [];
|
|
191
|
+
let currentDescribe = '';
|
|
192
|
+
|
|
193
|
+
global.describe = function(desc, fn) {
|
|
194
|
+
currentDescribe = desc;
|
|
195
|
+
fn();
|
|
196
|
+
};
|
|
197
|
+
global.beforeEach = function(fn) {
|
|
198
|
+
beforeEachFns.push(fn);
|
|
199
|
+
};
|
|
200
|
+
global.afterEach = function(fn) {
|
|
201
|
+
afterEachFns.push(fn);
|
|
202
|
+
};
|
|
203
|
+
global.it = function(desc, fn) {
|
|
204
|
+
const full = currentDescribe ? `${currentDescribe} ${desc}` : desc;
|
|
205
|
+
tests.push({desc: full, fn});
|
|
206
|
+
return { timeout: () => {} };
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
console.log("Columnar File Tests\n" + "=".repeat(60));
|
|
210
|
+
|
|
211
|
+
const dir = path.join(__dirname, 'test');
|
|
212
|
+
const files = fs.readdirSync(dir).filter(f => /^columnar.*\.js$/.test(f));
|
|
213
|
+
files.forEach(f => require(path.join(dir, f)));
|
|
214
|
+
|
|
215
|
+
for (const t of tests) {
|
|
216
|
+
for (const bf of beforeEachFns) await callMaybeAsync(bf);
|
|
217
|
+
try {
|
|
218
|
+
await callMaybeAsync(t.fn);
|
|
219
|
+
console.log(`✓ PASS: ${t.desc}`);
|
|
220
|
+
testsPassed++;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error(`✗ FAIL: ${t.desc}`);
|
|
223
|
+
console.error(` Error: ${err.message}`);
|
|
224
|
+
testsFailed++;
|
|
225
|
+
}
|
|
226
|
+
for (const af of afterEachFns) await callMaybeAsync(af);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log('\n' + '='.repeat(60));
|
|
230
|
+
console.log(`Tests passed: ${testsPassed}`);
|
|
231
|
+
console.log(`Tests failed: ${testsFailed}`);
|
|
232
|
+
console.log('='.repeat(60));
|
|
233
|
+
process.exit(testsFailed > 0 ? 1 : 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log("Logistic Regression Unit Tests\n" + "=".repeat(60));
|
|
237
|
+
|
|
238
|
+
// Test 1: Model instantiation
|
|
239
|
+
test("Should create a LogisticRegression model instance", () => {
|
|
240
|
+
const model = new LogisticRegression({
|
|
241
|
+
learningRate: 0.1,
|
|
242
|
+
iterations: 100
|
|
243
|
+
});
|
|
244
|
+
assert(model instanceof LogisticRegression);
|
|
245
|
+
assert.strictEqual(model.learningRate, 0.1);
|
|
246
|
+
assert.strictEqual(model.iterations, 100);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Test 2: Model fit
|
|
250
|
+
test("Should fit a model with training data", () => {
|
|
251
|
+
const model = new LogisticRegression({
|
|
252
|
+
learningRate: 0.1,
|
|
253
|
+
iterations: 100
|
|
254
|
+
});
|
|
255
|
+
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
|
|
256
|
+
const y = [0, 1, 1, 1];
|
|
257
|
+
|
|
258
|
+
const result = model.fit(X, y);
|
|
259
|
+
assert(result instanceof LogisticRegression);
|
|
260
|
+
assert(model.weights !== null);
|
|
261
|
+
assert.strictEqual(model.weights.length, 3); // 2 features + 1 intercept
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Test 3: Model predict
|
|
265
|
+
test("Should predict class labels", () => {
|
|
266
|
+
const model = new LogisticRegression({
|
|
267
|
+
learningRate: 0.1,
|
|
268
|
+
iterations: 100
|
|
269
|
+
});
|
|
270
|
+
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
|
|
271
|
+
const y = [0, 1, 1, 1];
|
|
272
|
+
|
|
273
|
+
model.fit(X, y);
|
|
274
|
+
const predictions = model.predict([[0, 0], [1, 1], [0, 1]]);
|
|
275
|
+
|
|
276
|
+
assert(Array.isArray(predictions));
|
|
277
|
+
assert.strictEqual(predictions.length, 3);
|
|
278
|
+
predictions.forEach(pred => {
|
|
279
|
+
assert([0, 1].includes(pred));
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Test 4: Model predictProba
|
|
284
|
+
test("Should predict probabilities", () => {
|
|
285
|
+
const model = new LogisticRegression({
|
|
286
|
+
learningRate: 0.1,
|
|
287
|
+
iterations: 100
|
|
288
|
+
});
|
|
289
|
+
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
|
|
290
|
+
const y = [0, 1, 1, 1];
|
|
291
|
+
|
|
292
|
+
model.fit(X, y);
|
|
293
|
+
const probs = model.predictProba([[0, 0], [1, 1], [0.5, 0.5]]);
|
|
294
|
+
|
|
295
|
+
assert(Array.isArray(probs));
|
|
296
|
+
assert.strictEqual(probs.length, 3);
|
|
297
|
+
probs.forEach(prob => {
|
|
298
|
+
assert(typeof prob === 'number');
|
|
299
|
+
assert(prob >= 0 && prob <= 1);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Test 5: Model decision function
|
|
304
|
+
test("Should compute decision function (logits)", () => {
|
|
305
|
+
const model = new LogisticRegression({
|
|
306
|
+
learningRate: 0.1,
|
|
307
|
+
iterations: 100
|
|
308
|
+
});
|
|
309
|
+
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
|
|
310
|
+
const y = [0, 1, 1, 1];
|
|
311
|
+
|
|
312
|
+
model.fit(X, y);
|
|
313
|
+
const decisions = model.decisionFunction([[0, 0], [1, 1]]);
|
|
314
|
+
|
|
315
|
+
assert(Array.isArray(decisions));
|
|
316
|
+
assert.strictEqual(decisions.length, 2);
|
|
317
|
+
decisions.forEach(d => {
|
|
318
|
+
assert(typeof d === 'number');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Test 6: Fit without data
|
|
323
|
+
test("Should throw error when fitting without data", () => {
|
|
324
|
+
const model = new LogisticRegression();
|
|
325
|
+
try {
|
|
326
|
+
model.fit([], []);
|
|
327
|
+
throw new Error("Should have thrown an error");
|
|
328
|
+
} catch (err) {
|
|
329
|
+
assert(err.message.includes("non-empty"));
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Test 7: Mismatched dimensions
|
|
334
|
+
test("Should throw error when X and y have different lengths", () => {
|
|
335
|
+
const model = new LogisticRegression();
|
|
336
|
+
try {
|
|
337
|
+
model.fit([[0, 0], [1, 1]], [0, 1, 1]);
|
|
338
|
+
throw new Error("Should have thrown an error");
|
|
339
|
+
} catch (err) {
|
|
340
|
+
assert(err.message.includes("same number"));
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Test 8: Predict before fit
|
|
345
|
+
test("Should throw error when predicting before fitting", () => {
|
|
346
|
+
const model = new LogisticRegression();
|
|
347
|
+
try {
|
|
348
|
+
model.predict([[0, 0]]);
|
|
349
|
+
throw new Error("Should have thrown an error");
|
|
350
|
+
} catch (err) {
|
|
351
|
+
assert(err.message.includes("not fitted"));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Test 9: L2 regularization
|
|
356
|
+
test("Should support L2 regularization", () => {
|
|
357
|
+
const model = new LogisticRegression({
|
|
358
|
+
learningRate: 0.1,
|
|
359
|
+
iterations: 100,
|
|
360
|
+
l2: 0.01
|
|
361
|
+
});
|
|
362
|
+
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
|
|
363
|
+
const y = [0, 1, 1, 1];
|
|
364
|
+
|
|
365
|
+
model.fit(X, y);
|
|
366
|
+
const predictions = model.predict([[0.5, 0.5]]);
|
|
367
|
+
|
|
368
|
+
assert(Array.isArray(predictions));
|
|
369
|
+
assert.strictEqual(predictions.length, 1);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Test 10: Custom threshold
|
|
373
|
+
test("Should use custom threshold for prediction", () => {
|
|
374
|
+
const model = new LogisticRegression({
|
|
375
|
+
learningRate: 0.1,
|
|
376
|
+
iterations: 100
|
|
377
|
+
});
|
|
378
|
+
const X = [[0, 0], [0, 1], [1, 0], [1, 1]];
|
|
379
|
+
const y = [0, 1, 1, 1];
|
|
380
|
+
|
|
381
|
+
model.fit(X, y);
|
|
382
|
+
const predictions = model.predict([[0.5, 0.5]], 0.7);
|
|
383
|
+
|
|
384
|
+
assert(Array.isArray(predictions));
|
|
385
|
+
assert([0, 1].includes(predictions[0]));
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Summary
|
|
389
|
+
console.log("\n" + "=".repeat(60));
|
|
390
|
+
console.log(`Tests passed: ${testsPassed}`);
|
|
391
|
+
console.log(`Tests failed: ${testsFailed}`);
|
|
392
|
+
console.log("=".repeat(60));
|
|
393
|
+
|
|
394
|
+
if (testsFailed > 0) {
|
|
395
|
+
console.log("\nSome tests failed!");
|
|
396
|
+
process.exit(1);
|
|
397
|
+
} else {
|
|
398
|
+
console.log("\nAll tests passed! ✓");
|
|
399
|
+
process.exit(0);
|
|
400
|
+
}
|
package/test.parq
ADDED
|
Binary file
|
package/test_select.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { SimpleColumnarStore } = require('./columnar/columnar.js');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
(async () => {
|
|
7
|
+
const tmp = path.join(os.tmpdir(), 'test-select-issue.columnar');
|
|
8
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
9
|
+
|
|
10
|
+
const records = [
|
|
11
|
+
{ id: 1, name: 'Alice', age: 25 },
|
|
12
|
+
{ id: 2, name: 'Bob', age: 30 },
|
|
13
|
+
{ id: 3, name: 'Charlie', age: 35 },
|
|
14
|
+
{ id: 4, name: 'Diana', age: 28 },
|
|
15
|
+
{ id: 5, name: 'Eve', age: 32 },
|
|
16
|
+
{ id: 6, name: 'Frank', age: 29 },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
await SimpleColumnarStore.writeRecords(records, tmp);
|
|
20
|
+
|
|
21
|
+
// Test the exact SQL from Node-RED
|
|
22
|
+
const sql = 'SELECT "a1" as test,:msg.payload as name,COUNT(*) AS cnt1 FROM ? a where name= :msg.payload';
|
|
23
|
+
console.log('Testing SQL:', sql);
|
|
24
|
+
|
|
25
|
+
const result = await SimpleColumnarStore.sqlQuery(
|
|
26
|
+
tmp,
|
|
27
|
+
sql,
|
|
28
|
+
null,
|
|
29
|
+
{ msg: { payload: 'Alice' }, flow: {}, global: {} }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
console.log('Result:', JSON.stringify(result, null, 2));
|
|
33
|
+
console.log('Number of columns in first record:', Object.keys(result[0]).length);
|
|
34
|
+
console.log('Column names:', Object.keys(result[0]));
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
37
|
+
})();
|
package/testing/test.js
CHANGED
|
@@ -74,7 +74,7 @@ function equalObjects(obj1,obj2,errorFactor,callEquals=()=>true,callNotEquals=()
|
|
|
74
74
|
if( keys1.length !== keys2.length ) return callNotEquals("different number of properties")
|
|
75
75
|
for(const key1 of keys1){
|
|
76
76
|
try{
|
|
77
|
-
if( !equalObjects(obj1[key1],obj2[key1],errorFactor) ) return callNotEquals()
|
|
77
|
+
if( !equalObjects(obj1[key1],obj2[key1],errorFactor,undefined,err=>{throw Error(err)}) ) return callNotEquals()
|
|
78
78
|
} catch(ex){
|
|
79
79
|
return callNotEquals(ex.message)
|
|
80
80
|
}
|
|
@@ -123,17 +123,18 @@ module.exports = function(RED) {
|
|
|
123
123
|
node.on("input",function(msg) {
|
|
124
124
|
if(msg._test) {
|
|
125
125
|
try{
|
|
126
|
+
const testData=node.getData(msg,node)
|
|
126
127
|
if(msg._test.id!==node.id) return setError(msg,node,"Sent by another test "+msg._test.id);
|
|
127
|
-
|
|
128
128
|
if(node.isJSONata)
|
|
129
|
-
return RED.util.evaluateJSONataExpression(node.resultExpression,
|
|
130
|
-
if(err) testedFailed(node,msg,err)
|
|
131
|
-
|
|
129
|
+
return RED.util.evaluateJSONataExpression(node.resultExpression,testData,(err,data)=>{
|
|
130
|
+
if(err) return testedFailed(node,msg,err)
|
|
131
|
+
if(data==null) testedFailed(node,msg,"no data, expecting boolean")
|
|
132
|
+
return data==true ? testedOK(node,msg) : testedFailed(node,msg,"jsonata not true")
|
|
132
133
|
})
|
|
133
134
|
|
|
134
|
-
node.equalObjects(
|
|
135
|
+
node.equalObjects(testData,msg._test.result,node.errorFactor,
|
|
135
136
|
()=>testedOK(node,msg),
|
|
136
|
-
|
|
137
|
+
err=>testedFailed(node,msg,"equal object test failure "+err)
|
|
137
138
|
);
|
|
138
139
|
|
|
139
140
|
} catch(ex){
|
package/transform/transform.html
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
sourceProperty:{value:"msg.payload"},
|
|
28
28
|
targetProperty:{value:"msg.payload"},
|
|
29
29
|
topicProperty:{value:"msg.topic"},
|
|
30
|
+
JSONataSource:{value:null,validate:v=>v==null||RED.validators.typedInput("JSONataSourceType")},
|
|
31
|
+
JSONataSourceType:{value:"jsonata"},
|
|
32
|
+
JSONataTarget:{value:null,validate:v=>v==null||RED.validators.typedInput("JSONataTargetType")},
|
|
33
|
+
JSONataTargetType:{value:"jsonata"},
|
|
30
34
|
index: {value:0},
|
|
31
35
|
maxMessages: {value:1000},
|
|
32
36
|
maxDate: {value:null},
|
|
@@ -104,6 +108,7 @@
|
|
|
104
108
|
$(".form-row-http-in-skip").hide();
|
|
105
109
|
$(".form-row-http-in-schema").hide();
|
|
106
110
|
$(".form-row-http-in-compressionType").hide();
|
|
111
|
+
$(".form-row-http-in-JSONataSource").hide();
|
|
107
112
|
if(!['CSV','CSVWithHeader'].includes(actionSource)) {
|
|
108
113
|
$(".form-row-http-in-csv").hide();
|
|
109
114
|
}
|
|
@@ -151,6 +156,7 @@
|
|
|
151
156
|
options["Array"]="Array";
|
|
152
157
|
break;
|
|
153
158
|
case 'JSON':
|
|
159
|
+
$(".form-row-http-in-JSONataSource").show();
|
|
154
160
|
options["Array"]="Array";
|
|
155
161
|
options["AVRO"]="AVRO";
|
|
156
162
|
options["Compressed"]="Compressed";
|
|
@@ -277,7 +283,7 @@
|
|
|
277
283
|
$(".form-row-http-in-radix").hide();
|
|
278
284
|
$(".form-row-http-in-schema").hide();
|
|
279
285
|
$(".form-row-http-in-string").hide();
|
|
280
|
-
|
|
286
|
+
$(".form-row-http-in-JSONataTarget").hide();
|
|
281
287
|
switch (actionTarget) {
|
|
282
288
|
case 'AVRO':
|
|
283
289
|
case 'Confluence':
|
|
@@ -317,8 +323,14 @@
|
|
|
317
323
|
$(".form-row-http-in-max"+actionSource).show();
|
|
318
324
|
$(".form-row-http-in-min"+actionSource).show();
|
|
319
325
|
break;
|
|
326
|
+
case 'JSON':
|
|
327
|
+
$(".form-row-http-in-max"+actionSource).show();
|
|
328
|
+
$(".form-row-http-in-JSONataTarget").show();
|
|
329
|
+
break;
|
|
320
330
|
}
|
|
321
331
|
}).change();
|
|
332
|
+
setInput.apply(this,["JSONataSource",["jsonata"]]);
|
|
333
|
+
setInput.apply(this,["JSONataTarget",["jsonata"]]);
|
|
322
334
|
setInput.apply(this,["deleteSource",["bool"]]);
|
|
323
335
|
setInput.apply(this,["schema"]);
|
|
324
336
|
setInput.apply(this,["string",["str","msg","flow","global","env","node"]]);
|
|
@@ -357,11 +369,21 @@
|
|
|
357
369
|
<label for="node-input-actionSource"><i class="fa fa-list-ul"></i> Source Type </label>
|
|
358
370
|
<input type="text" id="node-input-actionSource">
|
|
359
371
|
</div>
|
|
372
|
+
<div class="form-row form-row-http-in-JSONataSource hide">
|
|
373
|
+
<label for="node-input-JSONataSource"><i class="fa fa-bookmark"></i> <span data-i18n="common.label.string"> JSONata Source</span></label>
|
|
374
|
+
<input type="text" id="node-input-JSONataSource" style="width:70%">
|
|
375
|
+
<input type="hidden" id="node-input-JSONataSourceType">
|
|
376
|
+
</div>
|
|
360
377
|
<div class="form-row">
|
|
361
378
|
<label for="node-input-actionTarget"><i class="fa fa-list-ul"></i> Target Type </label>
|
|
362
379
|
<select id="node-input-actionTarget" placeholder="actionTarget">
|
|
363
380
|
</select>
|
|
364
381
|
</div>
|
|
382
|
+
<div class="form-row form-row-http-in-JSONataTarget hide">
|
|
383
|
+
<label for="node-input-JSONataTarget"><i class="fa fa-bookmark"></i> <span data-i18n="common.label.string"> JSONata Target</span></label>
|
|
384
|
+
<input type="text" id="node-input-JSONataTarget" style="width:70%">
|
|
385
|
+
<input type="hidden" id="node-input-JSONataTargetType">
|
|
386
|
+
</div>
|
|
365
387
|
<div class="form-row form-row-http-in-delimiter hide">
|
|
366
388
|
<label for="node-input-delimiter"><i class="icon-bookmark"></i> Delimiter</label>
|
|
367
389
|
<input type="text" id="node-input-delimiter" placeholder="delimiter" size="1" minlength="1">
|
|
@@ -426,7 +448,6 @@
|
|
|
426
448
|
<input type="text" id="node-input-string" style="width:70%">
|
|
427
449
|
<input type="hidden" id="node-input-stringType">
|
|
428
450
|
</div>
|
|
429
|
-
|
|
430
451
|
</script>
|
|
431
452
|
|
|
432
453
|
<script type="text/x-red" data-help-name="transform">
|