@blamejs/core 0.14.27 → 0.15.1
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 +6 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +158 -77
- package/lib/atomic-file.js +29 -1
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +228 -100
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +22 -0
- package/lib/consent.js +82 -29
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +387 -91
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +517 -256
- package/lib/db-schema.js +209 -44
- package/lib/db.js +202 -95
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/framework-error.js +11 -0
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +596 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +14 -0
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +293 -154
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +31 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/outbox.js +136 -82
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/retention.js +82 -39
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +116 -72
- package/lib/sql.js +3885 -0
- package/lib/static.js +45 -7
- package/lib/subject.js +89 -49
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +16 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/db-query.js
CHANGED
|
@@ -17,6 +17,20 @@
|
|
|
17
17
|
* updateOne(changes), updateMany(changes),
|
|
18
18
|
* deleteOne(), deleteMany().
|
|
19
19
|
*
|
|
20
|
+
* SQL construction composes b.sql (lib/sql.js): every terminal builds a
|
|
21
|
+
* b.sql verb builder ({ dialect: "sqlite" }, the local node:sqlite
|
|
22
|
+
* backend), replays the recorded structured WHERE conditions onto it, and
|
|
23
|
+
* calls .toSql() for the { sql, params } pair — which db-query then
|
|
24
|
+
* prepares + runs on the local sqlite handle. b.sql owns identifier
|
|
25
|
+
* quoting (through b.safeSql), value binding (every value a `?`
|
|
26
|
+
* placeholder), IN-list expansion, LIKE auto-escape, and the output
|
|
27
|
+
* validator (_assertEmittable). db-query keeps everything b.sql cannot
|
|
28
|
+
* know about: the residency write-gate, sealed-row seal/unseal, _id
|
|
29
|
+
* auto-generation, per-row-key materialization, the column-membership
|
|
30
|
+
* gate, sealed-field → derived-hash translation, and the JSONB/JSON-path
|
|
31
|
+
* value guard — all applied at condition-record / row-build time, before
|
|
32
|
+
* the structured shape reaches b.sql.
|
|
33
|
+
*
|
|
20
34
|
* Sealed-field semantics:
|
|
21
35
|
* - On insert/update, sealed columns are vault.seal()'d and their derived
|
|
22
36
|
* hashes computed automatically.
|
|
@@ -33,6 +47,7 @@ var { generateToken } = require("./crypto");
|
|
|
33
47
|
var safeJson = require("./safe-json");
|
|
34
48
|
var safeJsonPath = require("./safe-jsonpath");
|
|
35
49
|
var safeSql = require("./safe-sql");
|
|
50
|
+
var sql = require("./sql");
|
|
36
51
|
var audit = require("./audit");
|
|
37
52
|
var lazyRequire = require("./lazy-require");
|
|
38
53
|
var { DbQueryError } = require("./framework-error");
|
|
@@ -195,8 +210,15 @@ class Query {
|
|
|
195
210
|
this._schema = schema;
|
|
196
211
|
this._table = table;
|
|
197
212
|
this._qualifiedKey = schema ? schema + "." + table : table;
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
// Recorded WHERE chain — an ordered list of leaves. Each leaf is
|
|
214
|
+
// { joiner, apply(predicate) } where apply() replays the leaf onto a
|
|
215
|
+
// b.sql Predicate (or builder) using its where-family methods. The
|
|
216
|
+
// sealed-field translation, JSONB value guard, and column-membership
|
|
217
|
+
// gate run at record time (in _addCondition / whereRaw / search /
|
|
218
|
+
// whereGroup / orWhere), so the recorded shape is already safe; the
|
|
219
|
+
// terminal just replays it through b.sql, which owns quoting +
|
|
220
|
+
// binding + the output validator.
|
|
221
|
+
this._conditions = [];
|
|
200
222
|
this._select = null;
|
|
201
223
|
this._orderBy = null;
|
|
202
224
|
this._limit = null;
|
|
@@ -215,6 +237,16 @@ class Query {
|
|
|
215
237
|
: (Array.isArray(opts.declaredColumns) ? new Set(opts.declaredColumns) : null);
|
|
216
238
|
this._columnGateMode = opts.columnGateMode || "reject";
|
|
217
239
|
this._allowedColumns = null;
|
|
240
|
+
// PRIMARY KEY column for the dialect-aware single-row write idiom on
|
|
241
|
+
// non-sqlite handles (sqlite uses the implicit rowid). db.from() tables
|
|
242
|
+
// key on `_id`; a table with a different PK declares it here. Validated
|
|
243
|
+
// as an identifier so it can splice into SQL as a quoted column.
|
|
244
|
+
if (opts.primaryKey !== undefined && opts.primaryKey !== null) {
|
|
245
|
+
safeSql.validateIdentifier(opts.primaryKey, { allowReserved: true });
|
|
246
|
+
this._primaryKey = opts.primaryKey;
|
|
247
|
+
} else {
|
|
248
|
+
this._primaryKey = null;
|
|
249
|
+
}
|
|
218
250
|
}
|
|
219
251
|
|
|
220
252
|
// Restrict the operator-allowable columns to an explicit subset
|
|
@@ -259,11 +291,55 @@ class Query {
|
|
|
259
291
|
". Use .allowedColumns([...]) or db.init({ columnGate: 'off' }) to bypass.");
|
|
260
292
|
}
|
|
261
293
|
|
|
262
|
-
//
|
|
263
|
-
|
|
294
|
+
// Resolve the SQL dialect for the handle this Query runs against.
|
|
295
|
+
// db.from() drives the framework's local node:sqlite handle (dialect
|
|
296
|
+
// "sqlite", the default). An operator who constructs `new Query(handle,
|
|
297
|
+
// table)` over their OWN Postgres / MySQL handle declares the dialect on
|
|
298
|
+
// the handle via `handle.dialect` ("postgres" | "mysql"), so b.sql emits
|
|
299
|
+
// the matching identifier quoting + single-row-write idiom. An unknown /
|
|
300
|
+
// absent value falls back to "sqlite" — the historical default — so every
|
|
301
|
+
// existing caller is byte-identical.
|
|
302
|
+
_dialect() {
|
|
303
|
+
var d = this._db && this._db.dialect;
|
|
304
|
+
if (d === "postgres" || d === "mysql" || d === "sqlite") return d;
|
|
305
|
+
return "sqlite";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// The b.sql opts for every terminal's verb builder. The dialect is
|
|
309
|
+
// resolved from the handle (sqlite by default; the operator's external
|
|
310
|
+
// handle can declare postgres / mysql). quoteName forces b.sql to QUOTE
|
|
311
|
+
// the resolved table name: db-query does NO clusterStorage prefix rewrite,
|
|
312
|
+
// so it never needs the bare-unquoted form — and quoting preserves
|
|
313
|
+
// db-query's reserved-word / case-sensitive table-name support (`"name"`
|
|
314
|
+
// is the safe identifier form). The schema qualifier (when present) makes
|
|
315
|
+
// b.sql emit the quoted `"schema"."table"` form. db-query owns the column
|
|
316
|
+
// gate (sealed-field rewrite happens before b.sql sees a column), so the
|
|
317
|
+
// builder's own gate stays off.
|
|
318
|
+
_sqlOpts() {
|
|
264
319
|
return this._schema
|
|
265
|
-
?
|
|
266
|
-
:
|
|
320
|
+
? { dialect: this._dialect(), schema: this._schema, quoteName: true }
|
|
321
|
+
: { dialect: this._dialect(), quoteName: true };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Whether any WHERE condition has been recorded — drives the
|
|
325
|
+
// unconditional-update / -delete / -increment refusals.
|
|
326
|
+
_hasConditions() {
|
|
327
|
+
return this._conditions.length > 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Replay the recorded WHERE chain onto a b.sql verb builder. The whole
|
|
331
|
+
// chain is wrapped in one b.sql whereGroup so the leaves' AND/OR
|
|
332
|
+
// joiners compose at a single precedence level (and a no-condition
|
|
333
|
+
// chain leaves the builder's where untouched). Returns the builder.
|
|
334
|
+
_applyConditions(builder) {
|
|
335
|
+
if (this._conditions.length === 0) return builder;
|
|
336
|
+
var conds = this._conditions;
|
|
337
|
+
builder.whereGroup(function (pred) {
|
|
338
|
+
for (var i = 0; i < conds.length; i++) {
|
|
339
|
+
conds[i].apply(pred);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return builder;
|
|
267
343
|
}
|
|
268
344
|
|
|
269
345
|
// ---- Chainable filters ----
|
|
@@ -283,7 +359,20 @@ class Query {
|
|
|
283
359
|
return this._addCondition(fieldOrObj, op, value);
|
|
284
360
|
}
|
|
285
361
|
|
|
286
|
-
|
|
362
|
+
// whereIn(field, values) — AND an `IN (...)` membership predicate. Facade
|
|
363
|
+
// over where(field, "IN", values) symmetric with b.sql's whereIn, so a
|
|
364
|
+
// caller can match a column against a value list (e.g. the dual-read
|
|
365
|
+
// derived-hash candidate set) without spelling the "IN" operator.
|
|
366
|
+
whereIn(field, values) {
|
|
367
|
+
return this.where(field, "IN", values);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Resolve a (field, op, value) predicate through the framework gates
|
|
371
|
+
// (JSONB value guard, sealed-field → derived-hash rewrite, column
|
|
372
|
+
// membership) and return the post-rewrite { field, op, value } that
|
|
373
|
+
// b.sql will emit. Shared by _addCondition and the WhereBuilder so the
|
|
374
|
+
// gates run identically whether the leaf is top-level or grouped.
|
|
375
|
+
_resolvePredicate(field, op, value) {
|
|
287
376
|
if (!ALLOWED_OPS.has(op)) {
|
|
288
377
|
throw new Error("invalid where operator: " + op);
|
|
289
378
|
}
|
|
@@ -341,45 +430,67 @@ class Query {
|
|
|
341
430
|
);
|
|
342
431
|
}
|
|
343
432
|
field = lookup.field;
|
|
344
|
-
|
|
433
|
+
if (op === "=" && lookup.legacyValue != null && lookup.legacyValue !== lookup.value) {
|
|
434
|
+
// Dual-read across the v0.15.0 keyed-MAC default flip: a row written
|
|
435
|
+
// before the flip carries the legacy salted-sha3 digest, so an
|
|
436
|
+
// equality lookup on a sealed field must match BOTH the active
|
|
437
|
+
// keyed-MAC digest and the legacy one — otherwise the flip silently
|
|
438
|
+
// drops every un-migrated row from the result. b.sql expands the
|
|
439
|
+
// IN-list to (?, ?) and binds each digest.
|
|
440
|
+
op = "IN";
|
|
441
|
+
value = [lookup.value, lookup.legacyValue];
|
|
442
|
+
} else {
|
|
443
|
+
value = lookup.value;
|
|
444
|
+
}
|
|
345
445
|
}
|
|
346
|
-
|
|
446
|
+
_validateField(field);
|
|
347
447
|
// Gate the post-sealed-rewrite physical column (derived-hash
|
|
348
448
|
// columns are declared physical columns, so the rewrite target
|
|
349
449
|
// passes membership).
|
|
350
450
|
this._assertColumnMember(field, "where");
|
|
351
451
|
if (op === "IN") {
|
|
352
|
-
// node:sqlite ? does not support array-binding.
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
452
|
+
// node:sqlite ? does not support array-binding; b.sql expands the
|
|
453
|
+
// IN-list to (?, ?, ?) and binds each element. Validate the shape
|
|
454
|
+
// here so the failure is db-query's clear message, not a builder
|
|
455
|
+
// error deeper in the stack.
|
|
356
456
|
if (!Array.isArray(value) || value.length === 0) {
|
|
357
457
|
throw new Error("where IN requires a non-empty array of values");
|
|
358
458
|
}
|
|
359
|
-
var placeholders = value.map(function () { return "?"; }).join(", ");
|
|
360
|
-
this._where.push('"' + field + '" IN (' + placeholders + ")");
|
|
361
|
-
for (var i = 0; i < value.length; i += 1) this._whereParams.push(value[i]);
|
|
362
|
-
return this;
|
|
363
459
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return this;
|
|
460
|
+
return { field: field, op: op, value: value };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Apply a resolved predicate onto a b.sql Predicate using the given
|
|
464
|
+
// joiner ("AND" via where* / "OR" via orWhere*). LIKE auto-escape,
|
|
465
|
+
// IN-list expansion, IS NULL, and JSONB emission are all owned by
|
|
466
|
+
// b.sql's _cmp from here.
|
|
467
|
+
_emitPredicate(pred, joiner, field, op, value) {
|
|
468
|
+
if (op === "IN") {
|
|
469
|
+
if (joiner === "OR") pred.orWhereIn(field, value);
|
|
470
|
+
else pred.whereIn(field, value);
|
|
471
|
+
return;
|
|
377
472
|
}
|
|
378
|
-
|
|
379
|
-
|
|
473
|
+
if (joiner === "OR") pred.orWhereOp(field, op, value);
|
|
474
|
+
else pred.whereOp(field, op, value);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_addCondition(field, op, value) {
|
|
478
|
+
var resolved = this._resolvePredicate(field, op, value);
|
|
479
|
+
var self = this;
|
|
480
|
+
this._pushLeaf("AND", function (pred) {
|
|
481
|
+
self._emitPredicate(pred, "AND", resolved.field, resolved.op, resolved.value);
|
|
482
|
+
});
|
|
380
483
|
return this;
|
|
381
484
|
}
|
|
382
485
|
|
|
486
|
+
// Append a WHERE leaf. `apply(pred)` replays it onto a b.sql Predicate
|
|
487
|
+
// (AND-joined at the chain level — the leaf's own apply decides AND vs
|
|
488
|
+
// OR internally). orWhere() rewrites the last leaf rather than
|
|
489
|
+
// appending, to preserve `(prev OR new)` grouping precedence.
|
|
490
|
+
_pushLeaf(joiner, apply) {
|
|
491
|
+
this._conditions.push({ joiner: joiner, apply: apply });
|
|
492
|
+
}
|
|
493
|
+
|
|
383
494
|
_isSealedField(field) {
|
|
384
495
|
var sealed = cryptoField.getSealedFields(this._cryptoFieldKey());
|
|
385
496
|
return sealed.indexOf(field) !== -1;
|
|
@@ -400,31 +511,35 @@ class Query {
|
|
|
400
511
|
|
|
401
512
|
// whereRaw — append a parenthesized raw SQL fragment with positional
|
|
402
513
|
// placeholders and the parameter values that fill them. Composes with
|
|
403
|
-
// .where() (AND-joined
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
514
|
+
// .where() (AND-joined). The fragment must NOT contain operator-
|
|
515
|
+
// supplied SQL — it's caller-controlled text used to build expressions
|
|
516
|
+
// the chainable .where() can't express (compound OR, row-value
|
|
517
|
+
// comparison for cursor pagination, etc.). b.sql's whereRaw guards the
|
|
518
|
+
// fragment (b.guardSql + embedded-literal + placeholder-count); the
|
|
519
|
+
// count + literal validation that db-query historically did inline now
|
|
520
|
+
// lives in that one choke-point.
|
|
521
|
+
whereRaw(sql_, params, opts) {
|
|
522
|
+
if (typeof sql_ !== "string" || sql_.length === 0) {
|
|
410
523
|
throw new Error("whereRaw: sql must be a non-empty string");
|
|
411
524
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
|
|
525
|
+
var p = Array.isArray(params) ? params.slice() : (params == null ? [] : [params]);
|
|
526
|
+
// Fail-fast at the chain-build boundary (matching the pre-b.sql
|
|
527
|
+
// contract — the operator catches a bad fragment at the whereRaw call,
|
|
528
|
+
// not deep inside a terminal). The embedded-literal + placeholder-count
|
|
529
|
+
// refusals keep db-query's stable SafeSqlError `sql/raw-literal` /
|
|
530
|
+
// explicit count-mismatch contract; b.sql's whereRaw (applied at the
|
|
531
|
+
// terminal) is the additional emission-time guard (b.guardSql, stacked-
|
|
532
|
+
// statement, encoding). allowLiterals opts the operator out of the
|
|
533
|
+
// literal refusal for a static, operator-controlled literal.
|
|
534
|
+
if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql_, "whereRaw");
|
|
535
|
+
var holders = safeSql.countPlaceholders(sql_);
|
|
422
536
|
if (holders !== p.length) {
|
|
423
537
|
throw new Error("whereRaw: " + holders + " placeholder(s) in sql but " +
|
|
424
538
|
p.length + " param(s) supplied");
|
|
425
539
|
}
|
|
426
|
-
this.
|
|
427
|
-
|
|
540
|
+
this._pushLeaf("AND", function (pred) {
|
|
541
|
+
pred.whereRaw(sql_, p, opts);
|
|
542
|
+
});
|
|
428
543
|
return this;
|
|
429
544
|
}
|
|
430
545
|
|
|
@@ -476,49 +591,46 @@ class Query {
|
|
|
476
591
|
return this;
|
|
477
592
|
}
|
|
478
593
|
|
|
479
|
-
// ---- Build SELECT components ----
|
|
480
|
-
|
|
481
|
-
_whereClause() {
|
|
482
|
-
return this._where.length === 0 ? "" : " WHERE " + this._where.join(" AND ");
|
|
483
|
-
}
|
|
594
|
+
// ---- Build SELECT components on a b.sql builder ----
|
|
484
595
|
|
|
485
|
-
|
|
486
|
-
|
|
596
|
+
// Apply the recorded projection / order / limit / offset onto a b.sql
|
|
597
|
+
// SELECT builder. Projection columns + orderBy fields already passed
|
|
598
|
+
// _validateField + the column gate at record time.
|
|
599
|
+
_applySelectClauses(qb) {
|
|
600
|
+
if (this._select) qb.columns(this._select);
|
|
487
601
|
if (this._orderBy) {
|
|
488
602
|
var entries = Array.isArray(this._orderBy) ? this._orderBy : [this._orderBy];
|
|
489
|
-
var fragments = [];
|
|
490
603
|
for (var i = 0; i < entries.length; i++) {
|
|
491
|
-
|
|
604
|
+
qb.orderBy(entries[i].field, entries[i].direction === "DESC" ? "desc" : "asc");
|
|
492
605
|
}
|
|
493
|
-
s += " ORDER BY " + fragments.join(", ");
|
|
494
606
|
}
|
|
495
|
-
if (this._limit !== null)
|
|
496
|
-
if (this._offset !== null)
|
|
497
|
-
return
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
_projection() {
|
|
501
|
-
if (!this._select) return "*";
|
|
502
|
-
return this._select.map(function (c) { return '"' + c + '"'; }).join(", ");
|
|
607
|
+
if (this._limit !== null) qb.limit(this._limit);
|
|
608
|
+
if (this._offset !== null) qb.offset(this._offset);
|
|
609
|
+
return qb;
|
|
503
610
|
}
|
|
504
611
|
|
|
505
612
|
// ---- Terminal methods (sync) ----
|
|
506
613
|
|
|
507
614
|
first() {
|
|
508
|
-
var
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
615
|
+
var qb = sql.select(this._table, this._sqlOpts());
|
|
616
|
+
this._applyConditions(qb);
|
|
617
|
+
this._applySelectClauses(qb);
|
|
618
|
+
qb.limit(1);
|
|
619
|
+
var built = qb.toSql();
|
|
620
|
+
var stmt = this._db.prepare(built.sql);
|
|
621
|
+
var row = stmt.get.apply(stmt, built.params);
|
|
512
622
|
// 4th arg (dbHandle) lets unsealRow fetch + unwrap the row-scoped
|
|
513
623
|
// K_row for vault.row: cells (declarePerRowKey tables).
|
|
514
624
|
return row ? cryptoField.unsealRow(this._cryptoFieldKey(), row, undefined, this._db) : null;
|
|
515
625
|
}
|
|
516
626
|
|
|
517
627
|
all() {
|
|
518
|
-
var
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
var
|
|
628
|
+
var qb = sql.select(this._table, this._sqlOpts());
|
|
629
|
+
this._applyConditions(qb);
|
|
630
|
+
this._applySelectClauses(qb);
|
|
631
|
+
var built = qb.toSql();
|
|
632
|
+
var stmt = this._db.prepare(built.sql);
|
|
633
|
+
var rows = stmt.all.apply(stmt, built.params);
|
|
522
634
|
var out = new Array(rows.length);
|
|
523
635
|
var key = this._cryptoFieldKey();
|
|
524
636
|
var dbHandle = this._db;
|
|
@@ -535,8 +647,10 @@ class Query {
|
|
|
535
647
|
// StreamLimit ceiling enforced from the module-level db
|
|
536
648
|
// config; per-call opts.streamLimit overrides for one-off bumps.
|
|
537
649
|
stream(opts) {
|
|
538
|
-
var
|
|
539
|
-
|
|
650
|
+
var qb = sql.select(this._table, this._sqlOpts());
|
|
651
|
+
this._applyConditions(qb);
|
|
652
|
+
this._applySelectClauses(qb);
|
|
653
|
+
var built = qb.toSql();
|
|
540
654
|
var perCallLimit;
|
|
541
655
|
// db.js exports getStreamLimit so this module reads the live
|
|
542
656
|
// ceiling without bouncing through the lib's circular load.
|
|
@@ -550,11 +664,11 @@ class Query {
|
|
|
550
664
|
}
|
|
551
665
|
perCallLimit = opts.streamLimit;
|
|
552
666
|
}
|
|
553
|
-
var stmt = this._db.prepare(sql);
|
|
667
|
+
var stmt = this._db.prepare(built.sql);
|
|
554
668
|
var key = this._cryptoFieldKey();
|
|
555
669
|
var dbHandle = this._db;
|
|
556
670
|
var iter;
|
|
557
|
-
try { iter = stmt.iterate.apply(stmt,
|
|
671
|
+
try { iter = stmt.iterate.apply(stmt, built.params); }
|
|
558
672
|
catch (e) {
|
|
559
673
|
var r = new Readable({ objectMode: true, read: function () {} });
|
|
560
674
|
setImmediate(function () { r.destroy(e); });
|
|
@@ -583,9 +697,11 @@ class Query {
|
|
|
583
697
|
}
|
|
584
698
|
|
|
585
699
|
count() {
|
|
586
|
-
var
|
|
587
|
-
|
|
588
|
-
var
|
|
700
|
+
var qb = sql.select(this._table, this._sqlOpts()).count("*", "n");
|
|
701
|
+
this._applyConditions(qb);
|
|
702
|
+
var built = qb.toSql();
|
|
703
|
+
var stmt = this._db.prepare(built.sql);
|
|
704
|
+
var row = stmt.get.apply(stmt, built.params);
|
|
589
705
|
return row ? row.n : 0;
|
|
590
706
|
}
|
|
591
707
|
|
|
@@ -605,7 +721,7 @@ class Query {
|
|
|
605
721
|
// (vault.row: cells). rowId MUST be withId._id — the same value
|
|
606
722
|
// b.subject.eraseHard / b.retention destroy on, so a later shred
|
|
607
723
|
// makes these cells undecryptable. Materialize stores the random
|
|
608
|
-
// row-secret AAD-sealed in
|
|
724
|
+
// row-secret AAD-sealed in the per-row-key store.
|
|
609
725
|
var sealOpts;
|
|
610
726
|
var cfKey = this._cryptoFieldKey();
|
|
611
727
|
if (cryptoField.hasPerRowKey(cfKey)) {
|
|
@@ -613,13 +729,9 @@ class Query {
|
|
|
613
729
|
sealOpts = { kRow: kRow, rowId: withId._id };
|
|
614
730
|
}
|
|
615
731
|
var sealed = cryptoField.sealRow(cfKey, withId, sealOpts);
|
|
616
|
-
var
|
|
617
|
-
var
|
|
618
|
-
|
|
619
|
-
var values = cols.map(function (c) { return sealed[c]; });
|
|
620
|
-
var sql = "INSERT INTO " + this._quotedTable() + " (" + quotedCols + ") VALUES (" + placeholders + ")";
|
|
621
|
-
var insertStmt = this._db.prepare(sql);
|
|
622
|
-
insertStmt.run.apply(insertStmt, values);
|
|
732
|
+
var built = sql.insert(this._table, this._sqlOpts()).values(sealed).toSql();
|
|
733
|
+
var insertStmt = this._db.prepare(built.sql);
|
|
734
|
+
insertStmt.run.apply(insertStmt, built.params);
|
|
623
735
|
// Return the original row with _id filled in (plaintext, never sealed)
|
|
624
736
|
return Object.assign({}, withId);
|
|
625
737
|
}
|
|
@@ -646,7 +758,7 @@ class Query {
|
|
|
646
758
|
if (!changes || typeof changes !== "object") {
|
|
647
759
|
throw new Error("update requires a changes object");
|
|
648
760
|
}
|
|
649
|
-
if (this.
|
|
761
|
+
if (!this._hasConditions()) {
|
|
650
762
|
throw new Error("refusing unconditional update — call where(...) first");
|
|
651
763
|
}
|
|
652
764
|
// Residency gates on the plaintext change set — an UPDATE that
|
|
@@ -670,27 +782,106 @@ class Query {
|
|
|
670
782
|
setKeys.forEach(_validateField);
|
|
671
783
|
var selfUpd = this;
|
|
672
784
|
setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
|
|
673
|
-
var setClause = setKeys.map(function (k) { return '"' + k + '" = ?'; }).join(", ");
|
|
674
|
-
var setValues = setKeys.map(function (k) { return sealed[k]; });
|
|
675
785
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
var
|
|
786
|
+
// No engine ships a portable UPDATE ... LIMIT, so a single-row update
|
|
787
|
+
// resolves exactly one row then writes it. The shape is dialect-aware
|
|
788
|
+
// (sqlite rowid sub-select / postgres PK sub-select / mysql
|
|
789
|
+
// resolve-then-write — _buildSingleRowWrite). A null result means the
|
|
790
|
+
// WHERE matched no row, so there is nothing to update (0 changes).
|
|
791
|
+
var built;
|
|
682
792
|
if (single) {
|
|
683
|
-
|
|
684
|
-
|
|
793
|
+
built = this._buildSingleRowWrite(sealed);
|
|
794
|
+
if (built === null) return 0;
|
|
685
795
|
} else {
|
|
686
|
-
|
|
796
|
+
var qb = sql.update(this._table, this._sqlOpts()).set(sealed);
|
|
797
|
+
this._applyConditions(qb);
|
|
798
|
+
built = qb.toSql();
|
|
687
799
|
}
|
|
688
|
-
var
|
|
689
|
-
var
|
|
690
|
-
var info = updStmt.run.apply(updStmt, allParams);
|
|
800
|
+
var updStmt = this._db.prepare(built.sql);
|
|
801
|
+
var info = updStmt.run.apply(updStmt, built.params);
|
|
691
802
|
return info.changes;
|
|
692
803
|
}
|
|
693
804
|
|
|
805
|
+
// The single-row-write row locator, by dialect. No engine ships
|
|
806
|
+
// UPDATE ... LIMIT portably (node:sqlite is built without
|
|
807
|
+
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT), so the single-row idiom is a
|
|
808
|
+
// sub-SELECT that resolves exactly one row then matches it:
|
|
809
|
+
//
|
|
810
|
+
// sqlite — the implicit `rowid` system column (every non-WITHOUT-
|
|
811
|
+
// ROWID table has one); `WHERE "rowid" = (SELECT "rowid"
|
|
812
|
+
// FROM t WHERE ... LIMIT 1)`.
|
|
813
|
+
// postgres — the table's PRIMARY KEY (`_id`, the db.from() convention).
|
|
814
|
+
// Postgres accepts LIMIT in a scalar subquery, so the same
|
|
815
|
+
// `= (SELECT "_id" ... LIMIT 1)` shape works — and using the
|
|
816
|
+
// real, UNIQUE `_id` column keeps b.sql's quote-by-
|
|
817
|
+
// construction intact (ctid is an unquotable system column
|
|
818
|
+
// that would force a raw-identifier escape and is unstable
|
|
819
|
+
// across VACUUM).
|
|
820
|
+
// mysql — also the PRIMARY KEY, but MySQL refuses LIMIT in a
|
|
821
|
+
// subquery that directly references the same table in an
|
|
822
|
+
// `IN`/`=` predicate; wrapping the inner SELECT in a derived
|
|
823
|
+
// table (`... IN (SELECT "_id" FROM (SELECT "_id" ... LIMIT
|
|
824
|
+
// 1) AS _s)`) is the standard work-around.
|
|
825
|
+
//
|
|
826
|
+
// The inner SELECT is composed through b.sql (same table + conditions)
|
|
827
|
+
// and spliced via whereSub — passing the inner BUILDER (not concatenated
|
|
828
|
+
// SQL) so b.sql concatenates the sub-query's sql + params itself and the
|
|
829
|
+
// final statement still runs through b.sql's output validator.
|
|
830
|
+
_rowLocatorColumn(dialect) {
|
|
831
|
+
return dialect === "sqlite" ? "rowid" : this._pkColumn();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// The PRIMARY KEY column for single-row writes on non-sqlite dialects.
|
|
835
|
+
// db.from() tables key on `_id` (auto-generated when absent on insert);
|
|
836
|
+
// an operator running a table with a different PK overrides it via the
|
|
837
|
+
// `primaryKey` construction opt.
|
|
838
|
+
_pkColumn() {
|
|
839
|
+
return this._primaryKey || "_id";
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
_buildSingleRowWrite(sealed) {
|
|
843
|
+
if (this._dialect() === "mysql") {
|
|
844
|
+
// MySQL forbids referencing the UPDATE/DELETE target table in a
|
|
845
|
+
// subquery (error 1093), so the single-statement sub-SELECT idiom
|
|
846
|
+
// the other dialects use is unavailable. Resolve the one row's PK in
|
|
847
|
+
// a prior SELECT, then write `WHERE pk = ?` with the resolved value
|
|
848
|
+
// bound — every value still binds, the identifier still quotes by
|
|
849
|
+
// construction, and the write is a single validated statement with no
|
|
850
|
+
// self-referential subquery. Returns null when no row matched.
|
|
851
|
+
var pkVal = this._resolveSinglePk();
|
|
852
|
+
if (pkVal === null) return null;
|
|
853
|
+
return sql.update(this._table, this._sqlOpts())
|
|
854
|
+
.set(sealed)
|
|
855
|
+
.where(this._pkColumn(), pkVal)
|
|
856
|
+
.toSql();
|
|
857
|
+
}
|
|
858
|
+
var col = this._rowLocatorColumn(this._dialect());
|
|
859
|
+
var inner = sql.select(this._table, this._sqlOpts()).columns([col]);
|
|
860
|
+
this._applyConditions(inner);
|
|
861
|
+
inner.limit(1);
|
|
862
|
+
return sql.update(this._table, this._sqlOpts())
|
|
863
|
+
.set(sealed)
|
|
864
|
+
.whereSub(col, "=", inner)
|
|
865
|
+
.toSql();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Resolve the PK of exactly one row matching the recorded WHERE (LIMIT
|
|
869
|
+
// 1). Used by the MySQL single-row write path, where a self-referential
|
|
870
|
+
// subquery is rejected by the engine. The SELECT is a clean, fully-bound
|
|
871
|
+
// b.sql statement; returns the PK value, or null when nothing matched.
|
|
872
|
+
_resolveSinglePk() {
|
|
873
|
+
var pk = this._pkColumn();
|
|
874
|
+
var pick = sql.select(this._table, this._sqlOpts()).columns([pk]);
|
|
875
|
+
this._applyConditions(pick);
|
|
876
|
+
pick.limit(1);
|
|
877
|
+
var built = pick.toSql();
|
|
878
|
+
var stmt = this._db.prepare(built.sql);
|
|
879
|
+
var row = stmt.get.apply(stmt, built.params);
|
|
880
|
+
if (!row) return null;
|
|
881
|
+
var v = row[pk];
|
|
882
|
+
return (v === undefined || v === null) ? null : v;
|
|
883
|
+
}
|
|
884
|
+
|
|
694
885
|
// Per-row-key UPDATE. Sealed columns on a declarePerRowKey table are
|
|
695
886
|
// K_row cells (vault.row:), so each affected row must be re-sealed
|
|
696
887
|
// under its OWN K_row — a single set-based UPDATE can't carry per-row
|
|
@@ -699,11 +890,12 @@ class Query {
|
|
|
699
890
|
// under it (derived hashes computed from plaintext as usual), and
|
|
700
891
|
// UPDATE that single row by _id. `single` stops after the first row.
|
|
701
892
|
_updatePerRowKey(cfKey, changes, single) {
|
|
702
|
-
var
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
var
|
|
893
|
+
var idSelect = sql.select(this._table, this._sqlOpts()).columns(["_id"]);
|
|
894
|
+
this._applyConditions(idSelect);
|
|
895
|
+
if (single) idSelect.limit(1);
|
|
896
|
+
var idBuilt = idSelect.toSql();
|
|
897
|
+
var idStmt = this._db.prepare(idBuilt.sql);
|
|
898
|
+
var idRows = idStmt.all.apply(idStmt, idBuilt.params);
|
|
707
899
|
var changed = 0;
|
|
708
900
|
for (var r = 0; r < idRows.length; r++) {
|
|
709
901
|
var rowId = idRows[r]._id;
|
|
@@ -717,10 +909,10 @@ class Query {
|
|
|
717
909
|
setKeys.forEach(_validateField);
|
|
718
910
|
var selfUpd = this;
|
|
719
911
|
setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
|
|
720
|
-
var
|
|
721
|
-
|
|
722
|
-
var updStmt = this._db.prepare(
|
|
723
|
-
var info = updStmt.run.apply(updStmt,
|
|
912
|
+
var built = sql.update(this._table, this._sqlOpts())
|
|
913
|
+
.set(sealed).where("_id", rowId).toSql();
|
|
914
|
+
var updStmt = this._db.prepare(built.sql);
|
|
915
|
+
var info = updStmt.run.apply(updStmt, built.params);
|
|
724
916
|
changed += (info && info.changes) || 0;
|
|
725
917
|
}
|
|
726
918
|
return changed;
|
|
@@ -737,9 +929,9 @@ class Query {
|
|
|
737
929
|
// Atomic counter increment.
|
|
738
930
|
//
|
|
739
931
|
// `from(table).where(filter).increment("col", 1)` emits
|
|
740
|
-
// `UPDATE table SET col = col + ? WHERE ...` so concurrent
|
|
741
|
-
// can't collide on a fetch/mutate/store sequence (which would
|
|
742
|
-
// increments under racing transactions). Pass a negative delta to
|
|
932
|
+
// `UPDATE table SET col = COALESCE(col, 0) + ? WHERE ...` so concurrent
|
|
933
|
+
// writers can't collide on a fetch/mutate/store sequence (which would
|
|
934
|
+
// lose increments under racing transactions). Pass a negative delta to
|
|
743
935
|
// decrement.
|
|
744
936
|
//
|
|
745
937
|
// Returns the number of rows changed (matches updateMany shape).
|
|
@@ -753,19 +945,22 @@ class Query {
|
|
|
753
945
|
if (typeof delta !== "number" || !Number.isFinite(delta) || !Number.isInteger(delta)) {
|
|
754
946
|
throw new Error("increment(column, delta): delta must be a finite integer (default 1)");
|
|
755
947
|
}
|
|
756
|
-
if (this.
|
|
948
|
+
if (!this._hasConditions()) {
|
|
757
949
|
throw new Error("refusing unconditional increment — call where(...) first");
|
|
758
950
|
}
|
|
759
|
-
var whereSql = this._where.join(" AND ");
|
|
760
|
-
var qt = this._quotedTable();
|
|
761
|
-
var qc = '"' + column + '"';
|
|
762
951
|
// Use COALESCE so a NULL counter starts at 0 instead of producing
|
|
763
952
|
// NULL + delta = NULL silently (which would silently drop the
|
|
764
|
-
// operation under SQLite's NULL-arithmetic rules).
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
var
|
|
953
|
+
// operation under SQLite's NULL-arithmetic rules). The quoted column
|
|
954
|
+
// expression is built by b.safeSql under the active dialect so the
|
|
955
|
+
// increment RHS references the same quoted identifier b.sql's set
|
|
956
|
+
// target uses (double-quote on sqlite/postgres, backtick on mysql).
|
|
957
|
+
var qc = safeSql.quoteIdentifier(column, this._dialect(), { allowReserved: true });
|
|
958
|
+
var qb = sql.update(this._table, this._sqlOpts())
|
|
959
|
+
.setRaw(column, "COALESCE(" + qc + ", 0) + ?", [delta]);
|
|
960
|
+
this._applyConditions(qb);
|
|
961
|
+
var built = qb.toSql();
|
|
962
|
+
var stmt = this._db.prepare(built.sql);
|
|
963
|
+
var info = stmt.run.apply(stmt, built.params);
|
|
769
964
|
return info.changes;
|
|
770
965
|
}
|
|
771
966
|
|
|
@@ -785,10 +980,10 @@ class Query {
|
|
|
785
980
|
}
|
|
786
981
|
var sub = new WhereBuilder(this);
|
|
787
982
|
closure(sub);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
983
|
+
if (sub._parts.length === 0) return this;
|
|
984
|
+
this._pushLeaf("AND", function (pred) {
|
|
985
|
+
pred.whereGroup(function (g) { sub.replay(g); });
|
|
986
|
+
});
|
|
792
987
|
return this;
|
|
793
988
|
}
|
|
794
989
|
|
|
@@ -796,36 +991,61 @@ class Query {
|
|
|
796
991
|
// `.where(a).orWhere(b)` produces `WHERE (a) OR (b)` rather than
|
|
797
992
|
// `WHERE (a) AND (b)`. Accepts the same arg shapes as `.where`:
|
|
798
993
|
// object-literal map, `(field, value)`, `(field, op, value)`, or a
|
|
799
|
-
// `(qb) => ...` closure.
|
|
994
|
+
// `(qb) => ...` closure. Replays as `(prevLeaf OR newLeaf)` so the
|
|
995
|
+
// grouping precedence matches the pre-b.sql `( prev OR ( new ) )` form.
|
|
800
996
|
orWhere(fieldOrObjOrFn, op, value) {
|
|
801
|
-
if (this.
|
|
997
|
+
if (this._conditions.length === 0) {
|
|
802
998
|
throw new Error("orWhere(...): no prior where(...) — start the chain with where(...)");
|
|
803
999
|
}
|
|
1000
|
+
var argc = arguments.length;
|
|
1001
|
+
var prevLeaf = this._conditions.pop();
|
|
1002
|
+
var orApply;
|
|
804
1003
|
if (typeof fieldOrObjOrFn === "function") {
|
|
805
1004
|
var sub = new WhereBuilder(this);
|
|
806
1005
|
fieldOrObjOrFn(sub);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1006
|
+
if (sub._parts.length === 0) {
|
|
1007
|
+
// Empty OR closure — restore the prior leaf untouched.
|
|
1008
|
+
this._conditions.push(prevLeaf);
|
|
1009
|
+
return this;
|
|
1010
|
+
}
|
|
1011
|
+
orApply = function (pred) {
|
|
1012
|
+
pred.orWhereGroup(function (g) { sub.replay(g); });
|
|
1013
|
+
};
|
|
1014
|
+
} else if (fieldOrObjOrFn !== null && typeof fieldOrObjOrFn === "object" &&
|
|
1015
|
+
!Array.isArray(fieldOrObjOrFn)) {
|
|
1016
|
+
// Object map — all equalities OR'd as one group leaf.
|
|
1017
|
+
var self = this;
|
|
1018
|
+
var resolvedList = Object.keys(fieldOrObjOrFn).map(function (k) {
|
|
1019
|
+
return self._resolvePredicate(k, "=", fieldOrObjOrFn[k]);
|
|
1020
|
+
});
|
|
1021
|
+
orApply = function (pred) {
|
|
1022
|
+
pred.orWhereGroup(function (g) {
|
|
1023
|
+
for (var i = 0; i < resolvedList.length; i++) {
|
|
1024
|
+
self._emitPredicate(g, "AND", resolvedList[i].field, resolvedList[i].op,
|
|
1025
|
+
resolvedList[i].value);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
821
1029
|
} else {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1030
|
+
// 2-arg orWhere(field, value) is the equality shorthand; 3-arg
|
|
1031
|
+
// orWhere(field, op, value) carries an explicit operator. Mirror
|
|
1032
|
+
// .where()'s arguments.length discrimination so a 2-arg value of
|
|
1033
|
+
// (e.g.) the number 5 is never mistaken for an operator.
|
|
1034
|
+
var resolved = (argc === 2)
|
|
1035
|
+
? this._resolvePredicate(fieldOrObjOrFn, "=", op)
|
|
1036
|
+
: this._resolvePredicate(fieldOrObjOrFn, op, value);
|
|
1037
|
+
var selfP = this;
|
|
1038
|
+
orApply = function (pred) {
|
|
1039
|
+
selfP._emitPredicate(pred, "OR", resolved.field, resolved.op, resolved.value);
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
// Re-push a single leaf that emits ( prevLeaf OR newLeaf ).
|
|
1043
|
+
this._pushLeaf("AND", function (pred) {
|
|
1044
|
+
pred.whereGroup(function (g) {
|
|
1045
|
+
prevLeaf.apply(g);
|
|
1046
|
+
orApply(g);
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
829
1049
|
return this;
|
|
830
1050
|
}
|
|
831
1051
|
|
|
@@ -851,22 +1071,24 @@ class Query {
|
|
|
851
1071
|
}
|
|
852
1072
|
if (term.length === 0) return this;
|
|
853
1073
|
var match = (opts && opts.match) || "substring";
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1074
|
+
if (match !== "exact" && match !== "prefix" && match !== "substring") {
|
|
1075
|
+
throw new Error("search: opts.match must be 'substring' | 'prefix' | 'exact'");
|
|
1076
|
+
}
|
|
1077
|
+
// b.sql's whereLike owns the wildcard handling end-to-end: it escapes
|
|
1078
|
+
// the user's `%` / `_` metacharacters with `~`, adds the LIVE wrapping
|
|
1079
|
+
// wildcard per mode, and emits `"field" LIKE ? ESCAPE '~'` (a
|
|
1080
|
+
// builder-emitted ESCAPE clause, so no raw-fragment guard refusal). An
|
|
1081
|
+
// OR group across every search field; the first leaf leads, the rest
|
|
1082
|
+
// OR-join.
|
|
1083
|
+
var fieldList = fields.slice();
|
|
1084
|
+
this._pushLeaf("AND", function (pred) {
|
|
1085
|
+
pred.whereGroup(function (g) {
|
|
1086
|
+
for (var i = 0; i < fieldList.length; i++) {
|
|
1087
|
+
if (i === 0) g.whereLike(fieldList[i], term, match);
|
|
1088
|
+
else g.orWhereLike(fieldList[i], term, match);
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
870
1092
|
return this;
|
|
871
1093
|
}
|
|
872
1094
|
|
|
@@ -907,20 +1129,41 @@ class Query {
|
|
|
907
1129
|
}
|
|
908
1130
|
|
|
909
1131
|
_delete(single) {
|
|
910
|
-
if (this.
|
|
1132
|
+
if (!this._hasConditions()) {
|
|
911
1133
|
throw new Error("refusing unconditional delete — call where(...) first");
|
|
912
1134
|
}
|
|
913
|
-
var
|
|
914
|
-
var sql;
|
|
915
|
-
var qt = this._quotedTable();
|
|
1135
|
+
var built;
|
|
916
1136
|
if (single) {
|
|
917
|
-
|
|
918
|
-
|
|
1137
|
+
// No engine ships a portable DELETE ... LIMIT, so single-row delete
|
|
1138
|
+
// mirrors the single-row update idiom: sqlite splices a rowid
|
|
1139
|
+
// sub-select, postgres a PK sub-select (both via b.sql whereSub, the
|
|
1140
|
+
// inner builder object — b.sql concatenates the sub-query's sql +
|
|
1141
|
+
// params, no hand-rolled string), and mysql resolves the one PK in a
|
|
1142
|
+
// prior SELECT then deletes `WHERE pk = ?` (the engine forbids a
|
|
1143
|
+
// subquery referencing the DELETE target table). A null PK means the
|
|
1144
|
+
// WHERE matched nothing — 0 rows deleted.
|
|
1145
|
+
if (this._dialect() === "mysql") {
|
|
1146
|
+
var pkVal = this._resolveSinglePk();
|
|
1147
|
+
if (pkVal === null) return 0;
|
|
1148
|
+
built = sql.delete(this._table, this._sqlOpts())
|
|
1149
|
+
.where(this._pkColumn(), pkVal)
|
|
1150
|
+
.toSql();
|
|
1151
|
+
} else {
|
|
1152
|
+
var col = this._rowLocatorColumn(this._dialect());
|
|
1153
|
+
var inner = sql.select(this._table, this._sqlOpts()).columns([col]);
|
|
1154
|
+
this._applyConditions(inner);
|
|
1155
|
+
inner.limit(1);
|
|
1156
|
+
built = sql.delete(this._table, this._sqlOpts())
|
|
1157
|
+
.whereSub(col, "=", inner)
|
|
1158
|
+
.toSql();
|
|
1159
|
+
}
|
|
919
1160
|
} else {
|
|
920
|
-
|
|
1161
|
+
var dqb = sql.delete(this._table, this._sqlOpts());
|
|
1162
|
+
this._applyConditions(dqb);
|
|
1163
|
+
built = dqb.toSql();
|
|
921
1164
|
}
|
|
922
|
-
var delStmt = this._db.prepare(sql);
|
|
923
|
-
var info = delStmt.run.apply(delStmt,
|
|
1165
|
+
var delStmt = this._db.prepare(built.sql);
|
|
1166
|
+
var info = delStmt.run.apply(delStmt, built.params);
|
|
924
1167
|
return info.changes;
|
|
925
1168
|
}
|
|
926
1169
|
}
|
|
@@ -934,11 +1177,13 @@ class Query {
|
|
|
934
1177
|
// `.orGte` / `.orLt` / `.orLte` / `.orIn` / `.orLike` ORs an
|
|
935
1178
|
// expression. `.raw(sql, params)` AND's an arbitrary fragment.
|
|
936
1179
|
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
1180
|
+
// Each part is recorded structurally ({ joiner, kind, ... }) and replayed
|
|
1181
|
+
// onto a b.sql Predicate via replay(pred) — b.sql owns the quoting +
|
|
1182
|
+
// binding + LIKE escape + IN-list expansion. The owning Query runs the
|
|
1183
|
+
// column-membership gate as each part is recorded.
|
|
939
1184
|
class WhereBuilder {
|
|
940
1185
|
constructor(gate) {
|
|
941
|
-
this._parts = []; // [{ joiner: "
|
|
1186
|
+
this._parts = []; // [{ joiner, kind: "cmp"|"raw", ... }]
|
|
942
1187
|
// The owning Query, so grouped/OR sub-expressions enforce the
|
|
943
1188
|
// same column-membership gate as the top-level chain.
|
|
944
1189
|
this._gate = gate || null;
|
|
@@ -949,19 +1194,17 @@ class WhereBuilder {
|
|
|
949
1194
|
}
|
|
950
1195
|
_validateField(field);
|
|
951
1196
|
if (this._gate) this._gate._assertColumnMember(field, "whereGroup");
|
|
952
|
-
var qf = '"' + field + '"';
|
|
953
1197
|
if (op === "IN" || op === "NOT IN") {
|
|
954
1198
|
if (!Array.isArray(value) || value.length === 0) {
|
|
955
1199
|
throw new Error("WhereBuilder: " + op + " requires a non-empty array of values");
|
|
956
1200
|
}
|
|
957
|
-
|
|
958
|
-
this._parts.push({ joiner: joiner, sql: qf + " " + op + " (" + placeholders + ")", params: value.slice() });
|
|
1201
|
+
this._parts.push({ joiner: joiner, kind: "cmp", field: field, op: op, value: value.slice() });
|
|
959
1202
|
return this;
|
|
960
1203
|
}
|
|
961
|
-
if (!ALLOWED_OPS.has(op)) {
|
|
1204
|
+
if (!ALLOWED_OPS.has(op) && op !== "NOT IN") {
|
|
962
1205
|
throw new Error("WhereBuilder: invalid operator '" + op + "'");
|
|
963
1206
|
}
|
|
964
|
-
this._parts.push({ joiner: joiner,
|
|
1207
|
+
this._parts.push({ joiner: joiner, kind: "cmp", field: field, op: op, value: value });
|
|
965
1208
|
return this;
|
|
966
1209
|
}
|
|
967
1210
|
eq(f, v) { return this._push("AND", f, "=", v); }
|
|
@@ -980,56 +1223,67 @@ class WhereBuilder {
|
|
|
980
1223
|
orLte(f, v) { return this._push("OR", f, "<=", v); }
|
|
981
1224
|
orIn(f, vs) { return this._push("OR", f, "IN", vs); }
|
|
982
1225
|
orLike(f, v) { return this._push("OR", f, "LIKE", v); }
|
|
983
|
-
raw(
|
|
984
|
-
if (typeof
|
|
1226
|
+
raw(sql_, params, opts) {
|
|
1227
|
+
if (typeof sql_ !== "string" || sql_.length === 0) {
|
|
985
1228
|
throw new Error("WhereBuilder.raw: sql must be a non-empty string");
|
|
986
1229
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1230
|
+
var p = Array.isArray(params) ? params.slice() : (params == null ? [] : [params]);
|
|
1231
|
+
// Same fail-fast literal + placeholder-count contract as Query.whereRaw
|
|
1232
|
+
// (stable SafeSqlError code); b.sql re-guards at the terminal.
|
|
1233
|
+
if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql_, "WhereBuilder.raw");
|
|
1234
|
+
if (safeSql.countPlaceholders(sql_) !== p.length) {
|
|
990
1235
|
throw new Error("WhereBuilder.raw: placeholder count mismatch");
|
|
991
1236
|
}
|
|
992
|
-
this._parts.push({ joiner: "AND",
|
|
1237
|
+
this._parts.push({ joiner: "AND", kind: "raw", sql: sql_, params: p, opts: opts });
|
|
993
1238
|
return this;
|
|
994
1239
|
}
|
|
1240
|
+
// Replay the recorded parts onto a b.sql Predicate. The first part
|
|
1241
|
+
// leads the group (its joiner is the group's first leaf); each later
|
|
1242
|
+
// part AND/OR-joins per its recorded joiner. b.sql performs identifier
|
|
1243
|
+
// quoting, value binding, and IN-list expansion.
|
|
1244
|
+
replay(pred) {
|
|
1245
|
+
for (var i = 0; i < this._parts.length; i++) {
|
|
1246
|
+
_replayPart(pred, this._parts[i], this._parts[i].joiner === "OR" && i > 0);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
995
1249
|
build() {
|
|
1250
|
+
// Back-compat shim for any external reader that called build() to get
|
|
1251
|
+
// a { sql, params } pair. Replay onto a transient b.sql SELECT's
|
|
1252
|
+
// predicate and extract. Returns { sql: "", params: [] } when empty.
|
|
996
1253
|
if (this._parts.length === 0) return { sql: "", params: [] };
|
|
997
|
-
var
|
|
998
|
-
var
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
return { sql: sql, params: params };
|
|
1254
|
+
var self = this;
|
|
1255
|
+
var built = sql.select("t", { dialect: "sqlite" })
|
|
1256
|
+
.whereGroup(function (g) { self.replay(g); })
|
|
1257
|
+
.toSql();
|
|
1258
|
+
// Strip the "SELECT * FROM t WHERE (" prefix + trailing ")".
|
|
1259
|
+
var m = /WHERE \((.*)\)$/.exec(built.sql);
|
|
1260
|
+
return { sql: m ? m[1] : "", params: built.params };
|
|
1006
1261
|
}
|
|
1007
1262
|
}
|
|
1008
1263
|
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
// literal. A whereRaw / WhereBuilder.raw fragment is meant to be a
|
|
1015
|
-
// STATIC template whose every value is bound through a `?` placeholder;
|
|
1016
|
-
// an embedded `'...'` literal is the signature of operator input
|
|
1017
|
-
// concatenated into the query (CWE-89 / CWE-564 — concat into a
|
|
1018
|
-
// query builder). Double-quoted identifiers (`"col"`), line comments,
|
|
1264
|
+
// Refuse a raw SQL fragment that embeds a single-quoted string literal.
|
|
1265
|
+
// A whereRaw / WhereBuilder.raw fragment is a STATIC template whose every
|
|
1266
|
+
// value binds through a `?` placeholder; an embedded `'...'` literal is
|
|
1267
|
+
// the signature of operator input concatenated into the query builder
|
|
1268
|
+
// (CWE-89 / CWE-564). Double-quoted identifiers (`"col"`), line comments,
|
|
1019
1269
|
// and block comments are skipped. Operators with a deliberate static
|
|
1020
|
-
// literal pass `{ allowLiterals: true }`.
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1270
|
+
// literal pass `{ allowLiterals: true }`. db-query runs this eagerly at
|
|
1271
|
+
// the chain-build boundary so the operator-facing `sql/raw-literal`
|
|
1272
|
+
// SafeSqlError contract is stable; b.sql's whereRaw re-guards the same
|
|
1273
|
+
// fragment at the terminal (b.guardSql + the emission-time validator).
|
|
1274
|
+
// Single linear pass, no backtracking regex; shares the scan shape with
|
|
1275
|
+
// b.safeSql.countPlaceholders.
|
|
1276
|
+
function _assertRawNoStringLiteral(rawSql, where) {
|
|
1023
1277
|
var i = 0;
|
|
1024
|
-
var len =
|
|
1278
|
+
var len = rawSql.length;
|
|
1025
1279
|
while (i < len) {
|
|
1026
|
-
var ch =
|
|
1027
|
-
var next = i + 1 < len ?
|
|
1280
|
+
var ch = rawSql.charAt(i);
|
|
1281
|
+
var next = i + 1 < len ? rawSql.charAt(i + 1) : "";
|
|
1028
1282
|
if (ch === '"') {
|
|
1029
1283
|
i += 1;
|
|
1030
1284
|
while (i < len) {
|
|
1031
|
-
if (
|
|
1032
|
-
if (
|
|
1285
|
+
if (rawSql.charAt(i) === '"') {
|
|
1286
|
+
if (rawSql.charAt(i + 1) === '"') { i += 2; continue; }
|
|
1033
1287
|
i += 1; break;
|
|
1034
1288
|
}
|
|
1035
1289
|
i += 1;
|
|
@@ -1037,12 +1291,12 @@ function _assertRawNoStringLiteral(sql, where) {
|
|
|
1037
1291
|
continue;
|
|
1038
1292
|
}
|
|
1039
1293
|
if (ch === "-" && next === "-") {
|
|
1040
|
-
while (i < len &&
|
|
1294
|
+
while (i < len && rawSql.charAt(i) !== "\n") i += 1;
|
|
1041
1295
|
continue;
|
|
1042
1296
|
}
|
|
1043
1297
|
if (ch === "/" && next === "*") {
|
|
1044
1298
|
i += 2;
|
|
1045
|
-
while (i < len && !(
|
|
1299
|
+
while (i < len && !(rawSql.charAt(i) === "*" && rawSql.charAt(i + 1) === "/")) i += 1;
|
|
1046
1300
|
i += 2;
|
|
1047
1301
|
continue;
|
|
1048
1302
|
}
|
|
@@ -1057,40 +1311,47 @@ function _assertRawNoStringLiteral(sql, where) {
|
|
|
1057
1311
|
}
|
|
1058
1312
|
}
|
|
1059
1313
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1314
|
+
// Apply one recorded WhereBuilder part onto a b.sql Predicate. `or`
|
|
1315
|
+
// selects the OR-joining method (after the first leaf in a group); the
|
|
1316
|
+
// first leaf ignores its joiner (it leads the group). NOT IN and LIKE
|
|
1317
|
+
// are the two ops with a behavior the bare structured Predicate does not
|
|
1318
|
+
// expose 1:1: NOT IN has no orWhere* form, and the WhereBuilder LIKE is a
|
|
1319
|
+
// caller-controlled-wildcard LIKE (the value binds verbatim — no
|
|
1320
|
+
// auto-escape, matching the pre-b.sql WhereBuilder semantics, distinct
|
|
1321
|
+
// from .search() which escapes). Both compose through the guarded raw /
|
|
1322
|
+
// group surface without weakening anything.
|
|
1323
|
+
function _replayPart(pred, part, or) {
|
|
1324
|
+
if (part.kind === "raw") {
|
|
1325
|
+
if (or) pred.orWhereRaw(part.sql, part.params, part.opts);
|
|
1326
|
+
else pred.whereRaw(part.sql, part.params, part.opts);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (part.op === "LIKE") {
|
|
1330
|
+
// Verbatim LIKE — caller controls the wildcards (no escape clause),
|
|
1331
|
+
// exactly as the pre-migration WhereBuilder emitted `"f" LIKE ?`. The
|
|
1332
|
+
// identifier quoting follows the predicate's OWN dialect (the builder
|
|
1333
|
+
// it replays onto), so the LIKE column matches the surrounding query's
|
|
1334
|
+
// quoting on mysql (backtick) as well as sqlite/postgres (double-quote).
|
|
1335
|
+
var likeDialect = (pred && typeof pred._dialect === "function") ? pred._dialect() : "sqlite";
|
|
1336
|
+
var likeSql = safeSql.quoteIdentifier(part.field, likeDialect, { allowReserved: true }) + " LIKE ?";
|
|
1337
|
+
if (or) pred.orWhereRaw(likeSql, [part.value]);
|
|
1338
|
+
else pred.whereRaw(likeSql, [part.value]);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (part.op === "IN") {
|
|
1342
|
+
if (or) pred.orWhereIn(part.field, part.value);
|
|
1343
|
+
else pred.whereIn(part.field, part.value);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (part.op === "NOT IN") {
|
|
1347
|
+
// b.sql exposes no orWhereNotIn; emit an OR NOT-IN leaf as a
|
|
1348
|
+
// single-member OR group so the join precedence is preserved.
|
|
1349
|
+
if (or) pred.orWhereGroup(function (g) { g.whereNotIn(part.field, part.value); });
|
|
1350
|
+
else pred.whereNotIn(part.field, part.value);
|
|
1351
|
+
return;
|
|
1092
1352
|
}
|
|
1093
|
-
|
|
1353
|
+
if (or) pred.orWhereOp(part.field, part.op, part.value);
|
|
1354
|
+
else pred.whereOp(part.field, part.op, part.value);
|
|
1094
1355
|
}
|
|
1095
1356
|
|
|
1096
1357
|
function _validateField(field) {
|