@blamejs/core 0.4.11 → 0.4.13
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/lib/db-query.js +31 -0
- package/lib/db.js +63 -0
- package/lib/log.js +73 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- **0.4.12** (2026-04-30) — b.log: multi-sink output with per-sink level filtering
|
|
12
|
+
- **0.4.11** (2026-04-30) — b.cache: bytes-cap eviction, sliding TTL, tag invalidation
|
|
11
13
|
- **0.4.10** (2026-04-30) — bodyParser multipart: fileFilter + per-field maxBytes/mimeTypes
|
|
12
14
|
- **0.4.9** (2026-04-30) — Origin-Agent-Cluster + DNS-Prefetch-Control headers; b.auth.lockout primitive
|
|
13
15
|
- **0.4.8** (2026-04-30) — wiki SEO surface: per-page OG / Twitter / JSON-LD + sitemap.xml + robots.txt
|
package/lib/db-query.js
CHANGED
|
@@ -184,6 +184,37 @@ class Query {
|
|
|
184
184
|
return out;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// Streaming counterpart to all(). Each row is auto-unsealed against
|
|
188
|
+
// the bound table's sealedFields registration before it lands in the
|
|
189
|
+
// operator's pipeline. For large result sets (audit exports, backup
|
|
190
|
+
// table dumps) this avoids materializing the full rowset in memory.
|
|
191
|
+
stream() {
|
|
192
|
+
var sql = "SELECT " + this._projection() + ' FROM "' + this._table + '"' +
|
|
193
|
+
this._whereClause() + this._orderLimitOffset();
|
|
194
|
+
var stmt = this._db.prepare(sql);
|
|
195
|
+
var table = this._table;
|
|
196
|
+
var iter;
|
|
197
|
+
var Readable = require("node:stream").Readable;
|
|
198
|
+
try { iter = stmt.iterate.apply(stmt, this._whereParams); }
|
|
199
|
+
catch (e) {
|
|
200
|
+
var r = new Readable({ objectMode: true, read: function () {} });
|
|
201
|
+
setImmediate(function () { r.destroy(e); });
|
|
202
|
+
return r;
|
|
203
|
+
}
|
|
204
|
+
return new Readable({
|
|
205
|
+
objectMode: true,
|
|
206
|
+
read: function () {
|
|
207
|
+
try {
|
|
208
|
+
var step = iter.next();
|
|
209
|
+
if (step.done) { this.push(null); return; }
|
|
210
|
+
this.push(cryptoField.unsealRow(table, step.value));
|
|
211
|
+
} catch (e) {
|
|
212
|
+
this.destroy(e);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
187
218
|
count() {
|
|
188
219
|
var sql = 'SELECT COUNT(*) AS n FROM "' + this._table + '"' + this._whereClause();
|
|
189
220
|
var stmt = this._db.prepare(sql);
|
package/lib/db.js
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
*
|
|
33
33
|
* db.from(tableName) → Query (chainable)
|
|
34
34
|
* db.prepare(sql) → SQLite Statement (raw escape hatch)
|
|
35
|
+
* db.stream(sql, ...params, opts?) → Readable (object-mode rows;
|
|
36
|
+
* opts.table enables auto-unseal)
|
|
35
37
|
* db.runSql(sql) → raw SQL execution (DDL, BEGIN/COMMIT)
|
|
36
38
|
* db.transaction(function (db) {…}) → wraps in BEGIN/COMMIT/ROLLBACK
|
|
37
39
|
* db.hashFor(table, field, value) → derived-hash lookup helper
|
|
@@ -746,6 +748,66 @@ function prepare(sql) {
|
|
|
746
748
|
return database.prepare(sql);
|
|
747
749
|
}
|
|
748
750
|
|
|
751
|
+
// stream — Readable in object mode that yields rows as node:sqlite's
|
|
752
|
+
// iterate() produces them. Unlike all(), the engine doesn't materialize
|
|
753
|
+
// the result set in memory before the first row arrives, so audit
|
|
754
|
+
// exports / backup table dumps / large reports can process millions of
|
|
755
|
+
// rows without OOM pressure.
|
|
756
|
+
//
|
|
757
|
+
// Optional opts.table enables auto-unseal of sealed columns via the
|
|
758
|
+
// table's registered cryptoField schema. Raw / aggregate queries omit
|
|
759
|
+
// it. Mid-iteration prepare()-bound errors propagate as 'error' events.
|
|
760
|
+
function stream(sql) {
|
|
761
|
+
_requireInit();
|
|
762
|
+
var opts = null;
|
|
763
|
+
var params;
|
|
764
|
+
// Last arg may be a plain {table?} options object; everything else
|
|
765
|
+
// is a SQL parameter binding. node:sqlite accepts numbers, strings,
|
|
766
|
+
// bigints, Buffers, and null — plain objects can only be opts.
|
|
767
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
768
|
+
if (args.length > 0) {
|
|
769
|
+
var last = args[args.length - 1];
|
|
770
|
+
var isOptsShape = last !== null && typeof last === "object" &&
|
|
771
|
+
!Buffer.isBuffer(last) && !Array.isArray(last) &&
|
|
772
|
+
typeof last.length !== "number"; // exclude TypedArray-shapes
|
|
773
|
+
if (isOptsShape) {
|
|
774
|
+
opts = last;
|
|
775
|
+
params = args.slice(0, -1);
|
|
776
|
+
} else {
|
|
777
|
+
params = args;
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
params = [];
|
|
781
|
+
}
|
|
782
|
+
var table = opts && typeof opts.table === "string" ? opts.table : null;
|
|
783
|
+
var unseal = table ? cryptoField : null;
|
|
784
|
+
|
|
785
|
+
var Readable = require("node:stream").Readable;
|
|
786
|
+
var stmt;
|
|
787
|
+
var iter;
|
|
788
|
+
try {
|
|
789
|
+
stmt = database.prepare(sql);
|
|
790
|
+
iter = stmt.iterate.apply(stmt, params);
|
|
791
|
+
} catch (e) {
|
|
792
|
+
var r = new Readable({ objectMode: true, read: function () {} });
|
|
793
|
+
setImmediate(function () { r.destroy(e); });
|
|
794
|
+
return r;
|
|
795
|
+
}
|
|
796
|
+
return new Readable({
|
|
797
|
+
objectMode: true,
|
|
798
|
+
read: function () {
|
|
799
|
+
try {
|
|
800
|
+
var step = iter.next();
|
|
801
|
+
if (step.done) { this.push(null); return; }
|
|
802
|
+
var row = step.value;
|
|
803
|
+
this.push(unseal ? unseal.unsealRow(table, row) : row);
|
|
804
|
+
} catch (e) {
|
|
805
|
+
this.destroy(e);
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
749
811
|
function execRaw(sql) {
|
|
750
812
|
_requireInit();
|
|
751
813
|
return runSql(database, sql);
|
|
@@ -946,6 +1008,7 @@ module.exports = {
|
|
|
946
1008
|
init: init,
|
|
947
1009
|
from: from,
|
|
948
1010
|
prepare: prepare,
|
|
1011
|
+
stream: stream,
|
|
949
1012
|
runSql: execRaw,
|
|
950
1013
|
// SQLite multi-statement helper alias matching the node:sqlite
|
|
951
1014
|
// module's shape. Operator migration / seeder files that received
|
package/lib/log.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Each line is one JSON object on a single line, terminated with `\n`.
|
|
11
11
|
* Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4).
|
|
12
|
-
* info
|
|
12
|
+
* Default routing: debug / info / warn → stdout; error / fatal → stderr.
|
|
13
|
+
* Multi-sink config (`sinks: [...]`) takes full control of routing.
|
|
13
14
|
*
|
|
14
15
|
* var log = b.log.create({
|
|
15
16
|
* level: "info", // env LOG_LEVEL > opts.level > "info"
|
|
@@ -17,6 +18,19 @@
|
|
|
17
18
|
* redact: true, // run extras through lib/redact
|
|
18
19
|
* });
|
|
19
20
|
*
|
|
21
|
+
* // Multi-sink: each sink gets every line at-or-above its own level.
|
|
22
|
+
* // Default (no `sinks` opt) splits info-and-below to stdout and
|
|
23
|
+
* // warn-and-up to stderr — same as before.
|
|
24
|
+
* var log = b.log.create({
|
|
25
|
+
* level: "debug",
|
|
26
|
+
* sinks: [
|
|
27
|
+
* { stream: process.stdout, level: "info" },
|
|
28
|
+
* { stream: fs.createWriteStream("./logs/debug.log"), level: "debug" },
|
|
29
|
+
* { stream: fs.createWriteStream("./logs/errors.log"), level: "error" },
|
|
30
|
+
* ],
|
|
31
|
+
* });
|
|
32
|
+
* // sinks: [...] is mutually exclusive with destination/errorDestination.
|
|
33
|
+
*
|
|
20
34
|
* log.info("user logged in", { userId: "u-1" });
|
|
21
35
|
* log.error("payment failed", { orderId, err: e.message });
|
|
22
36
|
*
|
|
@@ -116,10 +130,56 @@ function _mergeExtras(into, extras, redactExtras) {
|
|
|
116
130
|
return clobberAttempt;
|
|
117
131
|
}
|
|
118
132
|
|
|
133
|
+
function _resolveSinks(opts) {
|
|
134
|
+
// Three input shapes — pick exactly one:
|
|
135
|
+
// (a) opts.sinks: [{ stream, level }, ...]
|
|
136
|
+
// (b) opts.destination + opts.errorDestination (legacy two-sink split)
|
|
137
|
+
// (c) neither — defaults to stdout for info-and-below, stderr for warn-and-up
|
|
138
|
+
if (Array.isArray(opts.sinks)) {
|
|
139
|
+
if (opts.destination !== undefined || opts.errorDestination !== undefined) {
|
|
140
|
+
throw new LogError("log/conflicting-sinks",
|
|
141
|
+
"log.create: pass either { sinks: [...] } OR { destination, errorDestination }, not both");
|
|
142
|
+
}
|
|
143
|
+
if (opts.sinks.length === 0) {
|
|
144
|
+
throw new LogError("log/no-sinks",
|
|
145
|
+
"log.create: sinks: [] would silently drop every line — pass at least one sink");
|
|
146
|
+
}
|
|
147
|
+
return opts.sinks.map(function (s, i) {
|
|
148
|
+
if (!s || typeof s !== "object") {
|
|
149
|
+
throw new LogError("log/bad-sink", "sinks[" + i + "]: expected object with { stream, level? }");
|
|
150
|
+
}
|
|
151
|
+
var allowed = ["stream", "level"];
|
|
152
|
+
var keys = Object.keys(s);
|
|
153
|
+
for (var j = 0; j < keys.length; j++) {
|
|
154
|
+
if (allowed.indexOf(keys[j]) === -1) {
|
|
155
|
+
throw new LogError("log/bad-sink",
|
|
156
|
+
"sinks[" + i + "]: unknown key '" + keys[j] + "' (allowed: " + allowed.join(", ") + ")");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
var stream = _normalizeDestination(s.stream, null);
|
|
160
|
+
if (!stream) {
|
|
161
|
+
throw new LogError("log/bad-sink", "sinks[" + i + "]: stream is required");
|
|
162
|
+
}
|
|
163
|
+
// Per-sink level: missing → no filter beyond the global; present → must be valid.
|
|
164
|
+
var minLevel = (s.level === undefined) ? null : _normalizeLevel(s.level);
|
|
165
|
+
return { stream: stream, minLevel: minLevel };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Legacy / default — synthesize the two-sink split.
|
|
169
|
+
var stdoutDest = _normalizeDestination(opts.destination, process.stdout);
|
|
170
|
+
var stderrDest = _normalizeDestination(opts.errorDestination, process.stderr);
|
|
171
|
+
return [
|
|
172
|
+
// Order matters for emit fan-out: stdout sink catches debug-info-warn;
|
|
173
|
+
// stderr catches error-and-up. Existing behavior — same boundary.
|
|
174
|
+
{ stream: stdoutDest, minLevel: null, _maxLevelExclusive: LEVELS.error },
|
|
175
|
+
{ stream: stderrDest, minLevel: LEVELS.error },
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
119
179
|
function create(opts) {
|
|
120
180
|
opts = opts || {};
|
|
121
181
|
validateOpts(opts, [
|
|
122
|
-
"level", "destination", "errorDestination",
|
|
182
|
+
"level", "destination", "errorDestination", "sinks",
|
|
123
183
|
"format", "redact", "base", "clock",
|
|
124
184
|
], "b.log");
|
|
125
185
|
|
|
@@ -134,8 +194,7 @@ function create(opts) {
|
|
|
134
194
|
level = LEVELS.info;
|
|
135
195
|
}
|
|
136
196
|
|
|
137
|
-
var
|
|
138
|
-
var stderrDest = _normalizeDestination(opts.errorDestination, process.stderr);
|
|
197
|
+
var sinks = _resolveSinks(opts);
|
|
139
198
|
|
|
140
199
|
var format = opts.format || "json"; // reserved for future formats
|
|
141
200
|
if (format !== "json") {
|
|
@@ -197,9 +256,16 @@ function create(opts) {
|
|
|
197
256
|
}) + "\n";
|
|
198
257
|
}
|
|
199
258
|
|
|
200
|
-
var
|
|
201
|
-
|
|
202
|
-
|
|
259
|
+
var lvlNum = LEVELS[levelName];
|
|
260
|
+
for (var s = 0; s < sinks.length; s++) {
|
|
261
|
+
var sink = sinks[s];
|
|
262
|
+
if (sink.minLevel !== null && lvlNum < sink.minLevel) continue;
|
|
263
|
+
// Legacy default-sinks split uses an exclusive upper bound so the
|
|
264
|
+
// stdout sink catches only info-and-below (warn+ goes to stderr).
|
|
265
|
+
if (sink._maxLevelExclusive !== undefined && lvlNum >= sink._maxLevelExclusive) continue;
|
|
266
|
+
try { sink.stream.write(line); }
|
|
267
|
+
catch (_e) { /* sink write best-effort — never throw out of a log call */ }
|
|
268
|
+
}
|
|
203
269
|
}
|
|
204
270
|
|
|
205
271
|
function _makeInstance(boundChain) {
|