node-red-contrib-prib-functions 0.23.3 → 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 +15 -0
- 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/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 +19 -8
- 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 +561 -5
- package/test/data/flow.json +571 -2
- package/test/data/package-lock.json +1 -1
- package/test/data/shares/.config.nodes.json +74 -52
- package/test/data/shares/.config.nodes.json.backup +589 -0
- package/test/data/shares/.config.runtime.json +2 -1
- package/test/data/shares/.config.runtime.json.backup +2 -1
- package/test/data/shares/.config.users.json +3 -2
- package/test/data/shares/.config.users.json.backup +5 -1
- 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/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
|
+
})();
|