@blamejs/core 0.5.15 → 0.5.17
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/CHANGELOG.md +2 -0
- package/index.js +2 -0
- package/lib/backup/index.js +2 -2
- package/lib/cache.js +5 -9
- package/lib/cli.js +4 -3
- package/lib/cluster.js +4 -4
- package/lib/csv.js +208 -162
- package/lib/db.js +5 -5
- package/lib/external-db.js +4 -3
- package/lib/forms.js +3 -6
- package/lib/http-client.js +2 -2
- package/lib/mail.js +2 -2
- package/lib/middleware/body-parser.js +2 -1
- package/lib/middleware/compression.js +3 -2
- package/lib/middleware/cors.js +1 -1
- package/lib/middleware/rate-limit.js +4 -4
- package/lib/mtls-engine-default.js +3 -2
- package/lib/nonce-store.js +4 -4
- package/lib/otel-export.js +267 -0
- package/lib/parsers/index.js +3 -7
- package/lib/queue.js +6 -7
- package/lib/request-helpers.js +35 -3
- package/lib/restore.js +3 -3
- package/lib/safe-async.js +122 -0
- package/lib/tracing.js +3 -3
- package/lib/webhook.js +3 -3
- package/lib/websocket.js +6 -4
- package/package.json +1 -1
- package/lib/parsers/safe-csv.js +0 -224
package/lib/restore.js
CHANGED
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
54
|
var fs = require("fs");
|
|
55
|
-
var nodeCrypto = require("node:crypto");
|
|
56
55
|
var os = require("os");
|
|
57
56
|
var path = require("path");
|
|
57
|
+
var crypto = require("./crypto");
|
|
58
58
|
var restoreBundle = require("./restore-bundle");
|
|
59
59
|
var restoreRollback = require("./restore-rollback");
|
|
60
60
|
var lazyRequire = require("./lazy-require");
|
|
@@ -128,7 +128,7 @@ function create(opts) {
|
|
|
128
128
|
"inspect: bundle '" + bundleId + "' not in storage");
|
|
129
129
|
}
|
|
130
130
|
var pullDir = path.join(os.tmpdir(),
|
|
131
|
-
"blamejs-restore-inspect-" +
|
|
131
|
+
"blamejs-restore-inspect-" + crypto.generateToken(4));
|
|
132
132
|
try {
|
|
133
133
|
await storage.readBundle(bundleId, pullDir);
|
|
134
134
|
return restoreBundle.inspect({ bundleDir: pullDir });
|
|
@@ -150,7 +150,7 @@ function create(opts) {
|
|
|
150
150
|
"run: bundle '" + bundleId + "' not in storage");
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
var pullId =
|
|
153
|
+
var pullId = crypto.generateToken(4);
|
|
154
154
|
var pullDir = path.join(os.tmpdir(), "blamejs-restore-pull-" + pullId);
|
|
155
155
|
var stagingDir = path.join(os.tmpdir(), "blamejs-restore-staging-" + pullId);
|
|
156
156
|
|
package/lib/safe-async.js
CHANGED
|
@@ -494,6 +494,126 @@ class Once {
|
|
|
494
494
|
hasInvoked() { return this._promise !== null; }
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
// ---- repeating ----
|
|
498
|
+
//
|
|
499
|
+
// Bounded-cadence interval timer with consistent unref + cancel semantics.
|
|
500
|
+
// Replaces the scattered setInterval ceremony where each caller hand-rolled
|
|
501
|
+
// `var t = setInterval(...); t.unref();` and a corresponding clearInterval
|
|
502
|
+
// in shutdown — easy to forget the unref and silently block process exit.
|
|
503
|
+
//
|
|
504
|
+
// var sweep = b.safeAsync.repeating(function () {
|
|
505
|
+
// return cleanup();
|
|
506
|
+
// }, b.constants.TIME.seconds(30), { name: "cache-sweep" });
|
|
507
|
+
// ...
|
|
508
|
+
// sweep.stop();
|
|
509
|
+
//
|
|
510
|
+
// fn may be sync or async. If fn returns a Promise, the next tick fires
|
|
511
|
+
// `intervalMs` after the prior fn() *started* (matching setInterval's
|
|
512
|
+
// fixed-rate semantics, not after-completion). Promise rejections are
|
|
513
|
+
// captured by the optional onError callback; if none provided, they're
|
|
514
|
+
// silently dropped — a repeating timer is by definition fire-and-forget,
|
|
515
|
+
// and an unhandled rejection here would crash the process.
|
|
516
|
+
//
|
|
517
|
+
// opts.unref defaults true: most repeating timers are background sweepers
|
|
518
|
+
// that should NOT keep the process alive. Cluster heartbeat etc. set
|
|
519
|
+
// `unref: false` so the lease keeps the leader from exiting silently.
|
|
520
|
+
|
|
521
|
+
function repeating(fn, intervalMs, opts) {
|
|
522
|
+
if (typeof fn !== "function") {
|
|
523
|
+
throw new SafeAsyncError("repeating: fn must be a function", "async/bad-arg");
|
|
524
|
+
}
|
|
525
|
+
if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
526
|
+
throw new SafeAsyncError("repeating: intervalMs must be a positive finite number, got " + intervalMs,
|
|
527
|
+
"async/bad-arg");
|
|
528
|
+
}
|
|
529
|
+
opts = opts || {};
|
|
530
|
+
var unref = opts.unref !== false; // default true
|
|
531
|
+
var onError = typeof opts.onError === "function" ? opts.onError : null;
|
|
532
|
+
|
|
533
|
+
var stopped = false;
|
|
534
|
+
var timer = setInterval(function () {
|
|
535
|
+
if (stopped) return;
|
|
536
|
+
var result;
|
|
537
|
+
try { result = fn(); }
|
|
538
|
+
catch (e) { if (onError) { try { onError(e); } catch (_e) { /* swallow */ } } return; }
|
|
539
|
+
if (result && typeof result.then === "function") {
|
|
540
|
+
result.then(null, function (e) {
|
|
541
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}, intervalMs);
|
|
545
|
+
if (unref && typeof timer.unref === "function") timer.unref();
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
stop: function () {
|
|
549
|
+
if (stopped) return;
|
|
550
|
+
stopped = true;
|
|
551
|
+
clearInterval(timer);
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---- flushLoop ----
|
|
557
|
+
//
|
|
558
|
+
// Schedule fn(), wait for it to settle (resolve or reject), then schedule
|
|
559
|
+
// the next fn() `intervalMs` later. Differs from `repeating` (fixed-rate,
|
|
560
|
+
// fire-and-forget) — flushLoop is the after-completion pattern most
|
|
561
|
+
// background flushers want: never overlap two flushes, and don't accumulate
|
|
562
|
+
// backlog if a flush is slow.
|
|
563
|
+
//
|
|
564
|
+
// var loop = b.safeAsync.flushLoop(function () {
|
|
565
|
+
// return otelExporter.flush();
|
|
566
|
+
// }, b.constants.TIME.seconds(15), { name: "otel-flush" });
|
|
567
|
+
// ...
|
|
568
|
+
// loop.stop();
|
|
569
|
+
//
|
|
570
|
+
// Always unref'd — a pending flush should never keep the process alive
|
|
571
|
+
// (the operator's b.appShutdown drives the final drain explicitly).
|
|
572
|
+
// onError catches rejections; without one, they're silently dropped.
|
|
573
|
+
|
|
574
|
+
function flushLoop(fn, intervalMs, opts) {
|
|
575
|
+
if (typeof fn !== "function") {
|
|
576
|
+
throw new SafeAsyncError("flushLoop: fn must be a function", "async/bad-arg");
|
|
577
|
+
}
|
|
578
|
+
if (typeof intervalMs !== "number" || !Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
579
|
+
throw new SafeAsyncError("flushLoop: intervalMs must be a positive finite number, got " + intervalMs,
|
|
580
|
+
"async/bad-arg");
|
|
581
|
+
}
|
|
582
|
+
opts = opts || {};
|
|
583
|
+
var onError = typeof opts.onError === "function" ? opts.onError : null;
|
|
584
|
+
|
|
585
|
+
var stopped = false;
|
|
586
|
+
var timer = null;
|
|
587
|
+
|
|
588
|
+
function _schedule() {
|
|
589
|
+
if (stopped) return;
|
|
590
|
+
timer = setTimeout(function () {
|
|
591
|
+
timer = null;
|
|
592
|
+
if (stopped) return;
|
|
593
|
+
var settled;
|
|
594
|
+
try { settled = Promise.resolve(fn()); }
|
|
595
|
+
catch (e) {
|
|
596
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
597
|
+
_schedule();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
settled.then(null, function (e) {
|
|
601
|
+
if (onError) { try { onError(e); } catch (_e) { /* swallow */ } }
|
|
602
|
+
}).then(_schedule);
|
|
603
|
+
}, intervalMs);
|
|
604
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
605
|
+
}
|
|
606
|
+
_schedule();
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
stop: function () {
|
|
610
|
+
if (stopped) return;
|
|
611
|
+
stopped = true;
|
|
612
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
497
617
|
// ---- Re-exports of resilience primitives from lib/retry.js ----
|
|
498
618
|
//
|
|
499
619
|
// withRetry + CircuitBreaker live in lib/retry.js (the canonical home).
|
|
@@ -510,6 +630,8 @@ module.exports = {
|
|
|
510
630
|
withSignal: withSignal,
|
|
511
631
|
withTimeoutSignal: withTimeoutSignal,
|
|
512
632
|
sleep: sleep,
|
|
633
|
+
repeating: repeating,
|
|
634
|
+
flushLoop: flushLoop,
|
|
513
635
|
safeAwait: safeAwait,
|
|
514
636
|
Mutex: Mutex,
|
|
515
637
|
Semaphore: Semaphore,
|
package/lib/tracing.js
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
* async-context, which is fine for the surfaces we instrument.
|
|
82
82
|
*/
|
|
83
83
|
|
|
84
|
-
var
|
|
84
|
+
var crypto = require("./crypto");
|
|
85
85
|
var validateOpts = require("./validate-opts");
|
|
86
86
|
var { defineClass } = require("./framework-error");
|
|
87
87
|
var { resolveRoute, captureResponseStatus } = require("./request-helpers");
|
|
@@ -134,10 +134,10 @@ function _formatTraceparent(traceId, spanId, flags) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
function _newTraceId() {
|
|
137
|
-
return
|
|
137
|
+
return crypto.generateToken(16); // 32 hex chars (W3C trace-context)
|
|
138
138
|
}
|
|
139
139
|
function _newSpanId() {
|
|
140
|
-
return
|
|
140
|
+
return crypto.generateToken(8); // 16 hex chars (W3C trace-context)
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// ---- Pass-through span (used when OTel isn't installed) ----
|
package/lib/webhook.js
CHANGED
|
@@ -225,10 +225,10 @@ function _pqcVerify(publicKeyPem, data, expectedHex) {
|
|
|
225
225
|
// a kid → signature pair. Whitespace tolerated around commas.
|
|
226
226
|
|
|
227
227
|
function _parseSignatureHeader(headerValue) {
|
|
228
|
-
var
|
|
228
|
+
var segs = requestHelpers.parseListHeader(headerValue);
|
|
229
229
|
var t = null, id = null, sigs = {};
|
|
230
|
-
for (var i = 0; i <
|
|
231
|
-
var seg =
|
|
230
|
+
for (var i = 0; i < segs.length; i++) {
|
|
231
|
+
var seg = segs[i];
|
|
232
232
|
var eq = seg.indexOf("=");
|
|
233
233
|
if (eq <= 0) continue; // skip malformed segments rather than failing whole header
|
|
234
234
|
var name = seg.slice(0, eq);
|
package/lib/websocket.js
CHANGED
|
@@ -84,6 +84,8 @@
|
|
|
84
84
|
var nodeCrypto = require("crypto");
|
|
85
85
|
var { EventEmitter } = require("events");
|
|
86
86
|
var C = require("./constants");
|
|
87
|
+
var requestHelpers = require("./request-helpers");
|
|
88
|
+
var safeAsync = require("./safe-async");
|
|
87
89
|
var safeBuffer = require("./safe-buffer");
|
|
88
90
|
var { FrameworkError } = require("./framework-error");
|
|
89
91
|
var { boot } = require("./log");
|
|
@@ -192,7 +194,7 @@ function validateUpgradeRequest(req) {
|
|
|
192
194
|
function negotiateSubprotocol(req, supported) {
|
|
193
195
|
if (!supported || supported.length === 0) return null;
|
|
194
196
|
var raw = (req.headers || {})["sec-websocket-protocol"] || "";
|
|
195
|
-
var offered =
|
|
197
|
+
var offered = requestHelpers.parseListHeader(raw);
|
|
196
198
|
for (var i = 0; i < offered.length; i++) {
|
|
197
199
|
if (supported.indexOf(offered[i]) !== -1) return offered[i];
|
|
198
200
|
}
|
|
@@ -417,8 +419,8 @@ class WebSocketConnection extends EventEmitter {
|
|
|
417
419
|
this._lastPongAt = Date.now();
|
|
418
420
|
|
|
419
421
|
var self = this;
|
|
420
|
-
this._pingTimer =
|
|
421
|
-
|
|
422
|
+
this._pingTimer = safeAsync.repeating(function () { self._heartbeat(pongMs); },
|
|
423
|
+
pingMs, { name: "websocket-ping" });
|
|
422
424
|
|
|
423
425
|
socket.on("data", function (chunk) { self._onData(chunk); });
|
|
424
426
|
socket.on("error", function (err) {
|
|
@@ -446,7 +448,7 @@ class WebSocketConnection extends EventEmitter {
|
|
|
446
448
|
if (this._state === STATE_CLOSED) return;
|
|
447
449
|
this._state = STATE_CLOSED;
|
|
448
450
|
if (error) this.lastError = error;
|
|
449
|
-
if (this._pingTimer) {
|
|
451
|
+
if (this._pingTimer) { this._pingTimer.stop(); this._pingTimer = null; }
|
|
450
452
|
if (this._closeTimer) { clearTimeout(this._closeTimer); this._closeTimer = null; }
|
|
451
453
|
// Surface diagnosable errors via 'error' first — but only if the
|
|
452
454
|
// operator is listening AND this is a real diagnosable case.
|
package/package.json
CHANGED
package/lib/parsers/safe-csv.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Security-focused CSV parser + writer.
|
|
4
|
-
*
|
|
5
|
-
* RFC 4180 compliant parsing + the operator-friendly defaults that the
|
|
6
|
-
* RFC doesn't address:
|
|
7
|
-
*
|
|
8
|
-
* - Size + row-count + field-length limits (DoS prevention)
|
|
9
|
-
* - BOM stripping
|
|
10
|
-
* - Configurable delimiter (',' default; '\t' for TSV; ';' for European)
|
|
11
|
-
* - Configurable quote char
|
|
12
|
-
* - CRLF / LF / CR line endings all accepted
|
|
13
|
-
*
|
|
14
|
-
* SECURITY: writer prevents CSV/Excel formula injection. Excel and other
|
|
15
|
-
* spreadsheet apps execute cells starting with '=', '+', '-', '@', or
|
|
16
|
-
* tab/CR characters. By default the writer prefixes such cells with a
|
|
17
|
-
* single quote so the formula doesn't execute when the file is opened.
|
|
18
|
-
* Toggle with { preventFormulaInjection: false } for true RFC 4180 output.
|
|
19
|
-
*
|
|
20
|
-
* Public API:
|
|
21
|
-
* csv.parse(input, opts?) → array of arrays | array of objects
|
|
22
|
-
* csv.stringify(rows, opts?) → string (RFC 4180 + injection-safe)
|
|
23
|
-
* csv.SafeCsvError → error class
|
|
24
|
-
*
|
|
25
|
-
* Defaults (parse):
|
|
26
|
-
* maxBytes: 16 MiB
|
|
27
|
-
* maxRows: 1,000,000
|
|
28
|
-
* maxFieldBytes: 1 MiB
|
|
29
|
-
* delimiter: ','
|
|
30
|
-
* quote: '"'
|
|
31
|
-
* header: true (first row is column names; rows returned as objects)
|
|
32
|
-
* trim: false (don't trim cell whitespace by default)
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
var C = require("../constants");
|
|
36
|
-
var safeBuffer = require("../safe-buffer");
|
|
37
|
-
var { FrameworkError } = require("../framework-error");
|
|
38
|
-
|
|
39
|
-
class SafeCsvError extends FrameworkError {
|
|
40
|
-
constructor(message, code, position) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = "SafeCsvError";
|
|
43
|
-
this.code = code || "csv/invalid";
|
|
44
|
-
this.position = position || null;
|
|
45
|
-
this.isSafeCsvError = true;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
var DEFAULTS_PARSE = {
|
|
50
|
-
maxBytes: C.BYTES.mib(16),
|
|
51
|
-
maxRows: 1000000,
|
|
52
|
-
maxFieldBytes: C.BYTES.mib(1),
|
|
53
|
-
delimiter: ",",
|
|
54
|
-
quote: '"',
|
|
55
|
-
header: true,
|
|
56
|
-
trim: false,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
var DEFAULTS_STRINGIFY = {
|
|
60
|
-
delimiter: ",",
|
|
61
|
-
quote: '"',
|
|
62
|
-
preventFormulaInjection: true,
|
|
63
|
-
formulaPrefixChars: ["=", "+", "-", "@", "\t", "\r"],
|
|
64
|
-
always_quote: false, // quote every field; default only quotes when needed
|
|
65
|
-
newline: "\r\n", // RFC 4180
|
|
66
|
-
header: null, // explicit array; null → derive from first object's keys
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// ---- parse ----
|
|
70
|
-
|
|
71
|
-
function parse(input, opts) {
|
|
72
|
-
opts = Object.assign({}, DEFAULTS_PARSE, opts || {});
|
|
73
|
-
|
|
74
|
-
input = safeBuffer.normalizeText(input, {
|
|
75
|
-
maxBytes: opts.maxBytes,
|
|
76
|
-
errorClass: SafeCsvError,
|
|
77
|
-
typeCode: "csv/wrong-input-type",
|
|
78
|
-
sizeCode: "csv/too-large",
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
var len = input.length;
|
|
82
|
-
var pos = 0;
|
|
83
|
-
var rows = [];
|
|
84
|
-
var row = [];
|
|
85
|
-
var field = "";
|
|
86
|
-
var inQuote = false;
|
|
87
|
-
|
|
88
|
-
function pushField() {
|
|
89
|
-
if (Buffer.byteLength(field, "utf8") > opts.maxFieldBytes) {
|
|
90
|
-
throw new SafeCsvError("field exceeds maxFieldBytes at row " + (rows.length + 1), "csv/field-too-large");
|
|
91
|
-
}
|
|
92
|
-
row.push(opts.trim ? field.trim() : field);
|
|
93
|
-
field = "";
|
|
94
|
-
}
|
|
95
|
-
function pushRow() {
|
|
96
|
-
pushField();
|
|
97
|
-
rows.push(row);
|
|
98
|
-
if (rows.length > opts.maxRows) {
|
|
99
|
-
throw new SafeCsvError("row count exceeds maxRows", "csv/too-many-rows");
|
|
100
|
-
}
|
|
101
|
-
row = [];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
while (pos < len) {
|
|
105
|
-
var ch = input.charAt(pos);
|
|
106
|
-
if (inQuote) {
|
|
107
|
-
if (ch === opts.quote) {
|
|
108
|
-
// Possible escaped quote (double-quote inside quoted field)
|
|
109
|
-
if (pos + 1 < len && input.charAt(pos + 1) === opts.quote) {
|
|
110
|
-
field += opts.quote;
|
|
111
|
-
pos += 2;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
// End of quoted field
|
|
115
|
-
inQuote = false;
|
|
116
|
-
pos += 1;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
field += ch;
|
|
120
|
-
pos += 1;
|
|
121
|
-
} else {
|
|
122
|
-
if (ch === opts.delimiter) {
|
|
123
|
-
pushField();
|
|
124
|
-
pos += 1;
|
|
125
|
-
} else if (ch === "\r") {
|
|
126
|
-
// CR or CRLF — both end the row
|
|
127
|
-
pushRow();
|
|
128
|
-
pos += 1;
|
|
129
|
-
if (pos < len && input.charAt(pos) === "\n") pos += 1;
|
|
130
|
-
} else if (ch === "\n") {
|
|
131
|
-
pushRow();
|
|
132
|
-
pos += 1;
|
|
133
|
-
} else if (ch === opts.quote && field === "") {
|
|
134
|
-
inQuote = true;
|
|
135
|
-
pos += 1;
|
|
136
|
-
} else {
|
|
137
|
-
field += ch;
|
|
138
|
-
pos += 1;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (inQuote) throw new SafeCsvError("unterminated quoted field", "csv/unterminated-quote");
|
|
143
|
-
// Final row (no trailing newline)
|
|
144
|
-
if (field.length > 0 || row.length > 0) {
|
|
145
|
-
pushRow();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (opts.header) {
|
|
149
|
-
if (rows.length === 0) return [];
|
|
150
|
-
var header = rows[0];
|
|
151
|
-
return rows.slice(1).map(function (r) {
|
|
152
|
-
var obj = {};
|
|
153
|
-
for (var i = 0; i < header.length; i++) obj[header[i]] = r[i] !== undefined ? r[i] : null;
|
|
154
|
-
return obj;
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
return rows;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ---- stringify ----
|
|
161
|
-
|
|
162
|
-
function stringify(rows, opts) {
|
|
163
|
-
opts = Object.assign({}, DEFAULTS_STRINGIFY, opts || {});
|
|
164
|
-
if (!Array.isArray(rows)) {
|
|
165
|
-
throw new SafeCsvError("stringify expects an array of rows", "csv/wrong-input-type");
|
|
166
|
-
}
|
|
167
|
-
if (rows.length === 0) return "";
|
|
168
|
-
|
|
169
|
-
// Determine header + row shape
|
|
170
|
-
var header;
|
|
171
|
-
var isObjectRows = false;
|
|
172
|
-
if (Array.isArray(rows[0])) {
|
|
173
|
-
isObjectRows = false;
|
|
174
|
-
header = opts.header || null;
|
|
175
|
-
} else if (typeof rows[0] === "object" && rows[0] !== null) {
|
|
176
|
-
isObjectRows = true;
|
|
177
|
-
header = opts.header || Object.keys(rows[0]);
|
|
178
|
-
} else {
|
|
179
|
-
throw new SafeCsvError("rows must be arrays or objects", "csv/wrong-input-type");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function escapeCell(value) {
|
|
183
|
-
var s = value == null ? "" : String(value);
|
|
184
|
-
if (opts.preventFormulaInjection && s.length > 0) {
|
|
185
|
-
var first = s.charAt(0);
|
|
186
|
-
if (opts.formulaPrefixChars.indexOf(first) !== -1) {
|
|
187
|
-
s = "'" + s; // Excel-safe prefix
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
var needsQuote = opts.always_quote ||
|
|
191
|
-
s.indexOf(opts.delimiter) !== -1 ||
|
|
192
|
-
s.indexOf(opts.quote) !== -1 ||
|
|
193
|
-
s.indexOf("\n") !== -1 ||
|
|
194
|
-
s.indexOf("\r") !== -1;
|
|
195
|
-
if (needsQuote) {
|
|
196
|
-
s = opts.quote + s.split(opts.quote).join(opts.quote + opts.quote) + opts.quote;
|
|
197
|
-
}
|
|
198
|
-
return s;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
var out = [];
|
|
202
|
-
if (header) {
|
|
203
|
-
out.push(header.map(escapeCell).join(opts.delimiter));
|
|
204
|
-
}
|
|
205
|
-
for (var i = 0; i < rows.length; i++) {
|
|
206
|
-
var r = rows[i];
|
|
207
|
-
var cells;
|
|
208
|
-
if (isObjectRows) {
|
|
209
|
-
cells = header.map(function (k) { return escapeCell(r[k]); });
|
|
210
|
-
} else {
|
|
211
|
-
cells = r.map(escapeCell);
|
|
212
|
-
}
|
|
213
|
-
out.push(cells.join(opts.delimiter));
|
|
214
|
-
}
|
|
215
|
-
return out.join(opts.newline);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
module.exports = {
|
|
219
|
-
parse: parse,
|
|
220
|
-
stringify: stringify,
|
|
221
|
-
SafeCsvError: SafeCsvError,
|
|
222
|
-
DEFAULTS_PARSE: DEFAULTS_PARSE,
|
|
223
|
-
DEFAULTS_STRINGIFY: DEFAULTS_STRINGIFY,
|
|
224
|
-
};
|