@blamejs/core 0.14.27 → 0.15.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/CHANGELOG.md +4 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- 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 +218 -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 +73 -24
- 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 +497 -255
- package/lib/db-schema.js +209 -44
- package/lib/db.js +176 -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 +287 -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 +109 -72
- package/lib/sql.js +3885 -0
- package/lib/static.js +45 -7
- package/lib/subject.js +55 -17
- 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,12 @@ class Query {
|
|
|
283
359
|
return this._addCondition(fieldOrObj, op, value);
|
|
284
360
|
}
|
|
285
361
|
|
|
286
|
-
|
|
362
|
+
// Resolve a (field, op, value) predicate through the framework gates
|
|
363
|
+
// (JSONB value guard, sealed-field → derived-hash rewrite, column
|
|
364
|
+
// membership) and return the post-rewrite { field, op, value } that
|
|
365
|
+
// b.sql will emit. Shared by _addCondition and the WhereBuilder so the
|
|
366
|
+
// gates run identically whether the leaf is top-level or grouped.
|
|
367
|
+
_resolvePredicate(field, op, value) {
|
|
287
368
|
if (!ALLOWED_OPS.has(op)) {
|
|
288
369
|
throw new Error("invalid where operator: " + op);
|
|
289
370
|
}
|
|
@@ -343,43 +424,54 @@ class Query {
|
|
|
343
424
|
field = lookup.field;
|
|
344
425
|
value = lookup.value;
|
|
345
426
|
}
|
|
346
|
-
|
|
427
|
+
_validateField(field);
|
|
347
428
|
// Gate the post-sealed-rewrite physical column (derived-hash
|
|
348
429
|
// columns are declared physical columns, so the rewrite target
|
|
349
430
|
// passes membership).
|
|
350
431
|
this._assertColumnMember(field, "where");
|
|
351
432
|
if (op === "IN") {
|
|
352
|
-
// node:sqlite ? does not support array-binding.
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
433
|
+
// node:sqlite ? does not support array-binding; b.sql expands the
|
|
434
|
+
// IN-list to (?, ?, ?) and binds each element. Validate the shape
|
|
435
|
+
// here so the failure is db-query's clear message, not a builder
|
|
436
|
+
// error deeper in the stack.
|
|
356
437
|
if (!Array.isArray(value) || value.length === 0) {
|
|
357
438
|
throw new Error("where IN requires a non-empty array of values");
|
|
358
439
|
}
|
|
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
440
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return this;
|
|
441
|
+
return { field: field, op: op, value: value };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Apply a resolved predicate onto a b.sql Predicate using the given
|
|
445
|
+
// joiner ("AND" via where* / "OR" via orWhere*). LIKE auto-escape,
|
|
446
|
+
// IN-list expansion, IS NULL, and JSONB emission are all owned by
|
|
447
|
+
// b.sql's _cmp from here.
|
|
448
|
+
_emitPredicate(pred, joiner, field, op, value) {
|
|
449
|
+
if (op === "IN") {
|
|
450
|
+
if (joiner === "OR") pred.orWhereIn(field, value);
|
|
451
|
+
else pred.whereIn(field, value);
|
|
452
|
+
return;
|
|
377
453
|
}
|
|
378
|
-
|
|
379
|
-
|
|
454
|
+
if (joiner === "OR") pred.orWhereOp(field, op, value);
|
|
455
|
+
else pred.whereOp(field, op, value);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
_addCondition(field, op, value) {
|
|
459
|
+
var resolved = this._resolvePredicate(field, op, value);
|
|
460
|
+
var self = this;
|
|
461
|
+
this._pushLeaf("AND", function (pred) {
|
|
462
|
+
self._emitPredicate(pred, "AND", resolved.field, resolved.op, resolved.value);
|
|
463
|
+
});
|
|
380
464
|
return this;
|
|
381
465
|
}
|
|
382
466
|
|
|
467
|
+
// Append a WHERE leaf. `apply(pred)` replays it onto a b.sql Predicate
|
|
468
|
+
// (AND-joined at the chain level — the leaf's own apply decides AND vs
|
|
469
|
+
// OR internally). orWhere() rewrites the last leaf rather than
|
|
470
|
+
// appending, to preserve `(prev OR new)` grouping precedence.
|
|
471
|
+
_pushLeaf(joiner, apply) {
|
|
472
|
+
this._conditions.push({ joiner: joiner, apply: apply });
|
|
473
|
+
}
|
|
474
|
+
|
|
383
475
|
_isSealedField(field) {
|
|
384
476
|
var sealed = cryptoField.getSealedFields(this._cryptoFieldKey());
|
|
385
477
|
return sealed.indexOf(field) !== -1;
|
|
@@ -400,31 +492,35 @@ class Query {
|
|
|
400
492
|
|
|
401
493
|
// whereRaw — append a parenthesized raw SQL fragment with positional
|
|
402
494
|
// placeholders and the parameter values that fill them. Composes with
|
|
403
|
-
// .where() (AND-joined
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
495
|
+
// .where() (AND-joined). The fragment must NOT contain operator-
|
|
496
|
+
// supplied SQL — it's caller-controlled text used to build expressions
|
|
497
|
+
// the chainable .where() can't express (compound OR, row-value
|
|
498
|
+
// comparison for cursor pagination, etc.). b.sql's whereRaw guards the
|
|
499
|
+
// fragment (b.guardSql + embedded-literal + placeholder-count); the
|
|
500
|
+
// count + literal validation that db-query historically did inline now
|
|
501
|
+
// lives in that one choke-point.
|
|
502
|
+
whereRaw(sql_, params, opts) {
|
|
503
|
+
if (typeof sql_ !== "string" || sql_.length === 0) {
|
|
410
504
|
throw new Error("whereRaw: sql must be a non-empty string");
|
|
411
505
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
|
|
506
|
+
var p = Array.isArray(params) ? params.slice() : (params == null ? [] : [params]);
|
|
507
|
+
// Fail-fast at the chain-build boundary (matching the pre-b.sql
|
|
508
|
+
// contract — the operator catches a bad fragment at the whereRaw call,
|
|
509
|
+
// not deep inside a terminal). The embedded-literal + placeholder-count
|
|
510
|
+
// refusals keep db-query's stable SafeSqlError `sql/raw-literal` /
|
|
511
|
+
// explicit count-mismatch contract; b.sql's whereRaw (applied at the
|
|
512
|
+
// terminal) is the additional emission-time guard (b.guardSql, stacked-
|
|
513
|
+
// statement, encoding). allowLiterals opts the operator out of the
|
|
514
|
+
// literal refusal for a static, operator-controlled literal.
|
|
515
|
+
if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql_, "whereRaw");
|
|
516
|
+
var holders = safeSql.countPlaceholders(sql_);
|
|
422
517
|
if (holders !== p.length) {
|
|
423
518
|
throw new Error("whereRaw: " + holders + " placeholder(s) in sql but " +
|
|
424
519
|
p.length + " param(s) supplied");
|
|
425
520
|
}
|
|
426
|
-
this.
|
|
427
|
-
|
|
521
|
+
this._pushLeaf("AND", function (pred) {
|
|
522
|
+
pred.whereRaw(sql_, p, opts);
|
|
523
|
+
});
|
|
428
524
|
return this;
|
|
429
525
|
}
|
|
430
526
|
|
|
@@ -476,49 +572,46 @@ class Query {
|
|
|
476
572
|
return this;
|
|
477
573
|
}
|
|
478
574
|
|
|
479
|
-
// ---- Build SELECT components ----
|
|
480
|
-
|
|
481
|
-
_whereClause() {
|
|
482
|
-
return this._where.length === 0 ? "" : " WHERE " + this._where.join(" AND ");
|
|
483
|
-
}
|
|
575
|
+
// ---- Build SELECT components on a b.sql builder ----
|
|
484
576
|
|
|
485
|
-
|
|
486
|
-
|
|
577
|
+
// Apply the recorded projection / order / limit / offset onto a b.sql
|
|
578
|
+
// SELECT builder. Projection columns + orderBy fields already passed
|
|
579
|
+
// _validateField + the column gate at record time.
|
|
580
|
+
_applySelectClauses(qb) {
|
|
581
|
+
if (this._select) qb.columns(this._select);
|
|
487
582
|
if (this._orderBy) {
|
|
488
583
|
var entries = Array.isArray(this._orderBy) ? this._orderBy : [this._orderBy];
|
|
489
|
-
var fragments = [];
|
|
490
584
|
for (var i = 0; i < entries.length; i++) {
|
|
491
|
-
|
|
585
|
+
qb.orderBy(entries[i].field, entries[i].direction === "DESC" ? "desc" : "asc");
|
|
492
586
|
}
|
|
493
|
-
s += " ORDER BY " + fragments.join(", ");
|
|
494
587
|
}
|
|
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(", ");
|
|
588
|
+
if (this._limit !== null) qb.limit(this._limit);
|
|
589
|
+
if (this._offset !== null) qb.offset(this._offset);
|
|
590
|
+
return qb;
|
|
503
591
|
}
|
|
504
592
|
|
|
505
593
|
// ---- Terminal methods (sync) ----
|
|
506
594
|
|
|
507
595
|
first() {
|
|
508
|
-
var
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
596
|
+
var qb = sql.select(this._table, this._sqlOpts());
|
|
597
|
+
this._applyConditions(qb);
|
|
598
|
+
this._applySelectClauses(qb);
|
|
599
|
+
qb.limit(1);
|
|
600
|
+
var built = qb.toSql();
|
|
601
|
+
var stmt = this._db.prepare(built.sql);
|
|
602
|
+
var row = stmt.get.apply(stmt, built.params);
|
|
512
603
|
// 4th arg (dbHandle) lets unsealRow fetch + unwrap the row-scoped
|
|
513
604
|
// K_row for vault.row: cells (declarePerRowKey tables).
|
|
514
605
|
return row ? cryptoField.unsealRow(this._cryptoFieldKey(), row, undefined, this._db) : null;
|
|
515
606
|
}
|
|
516
607
|
|
|
517
608
|
all() {
|
|
518
|
-
var
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
var
|
|
609
|
+
var qb = sql.select(this._table, this._sqlOpts());
|
|
610
|
+
this._applyConditions(qb);
|
|
611
|
+
this._applySelectClauses(qb);
|
|
612
|
+
var built = qb.toSql();
|
|
613
|
+
var stmt = this._db.prepare(built.sql);
|
|
614
|
+
var rows = stmt.all.apply(stmt, built.params);
|
|
522
615
|
var out = new Array(rows.length);
|
|
523
616
|
var key = this._cryptoFieldKey();
|
|
524
617
|
var dbHandle = this._db;
|
|
@@ -535,8 +628,10 @@ class Query {
|
|
|
535
628
|
// StreamLimit ceiling enforced from the module-level db
|
|
536
629
|
// config; per-call opts.streamLimit overrides for one-off bumps.
|
|
537
630
|
stream(opts) {
|
|
538
|
-
var
|
|
539
|
-
|
|
631
|
+
var qb = sql.select(this._table, this._sqlOpts());
|
|
632
|
+
this._applyConditions(qb);
|
|
633
|
+
this._applySelectClauses(qb);
|
|
634
|
+
var built = qb.toSql();
|
|
540
635
|
var perCallLimit;
|
|
541
636
|
// db.js exports getStreamLimit so this module reads the live
|
|
542
637
|
// ceiling without bouncing through the lib's circular load.
|
|
@@ -550,11 +645,11 @@ class Query {
|
|
|
550
645
|
}
|
|
551
646
|
perCallLimit = opts.streamLimit;
|
|
552
647
|
}
|
|
553
|
-
var stmt = this._db.prepare(sql);
|
|
648
|
+
var stmt = this._db.prepare(built.sql);
|
|
554
649
|
var key = this._cryptoFieldKey();
|
|
555
650
|
var dbHandle = this._db;
|
|
556
651
|
var iter;
|
|
557
|
-
try { iter = stmt.iterate.apply(stmt,
|
|
652
|
+
try { iter = stmt.iterate.apply(stmt, built.params); }
|
|
558
653
|
catch (e) {
|
|
559
654
|
var r = new Readable({ objectMode: true, read: function () {} });
|
|
560
655
|
setImmediate(function () { r.destroy(e); });
|
|
@@ -583,9 +678,11 @@ class Query {
|
|
|
583
678
|
}
|
|
584
679
|
|
|
585
680
|
count() {
|
|
586
|
-
var
|
|
587
|
-
|
|
588
|
-
var
|
|
681
|
+
var qb = sql.select(this._table, this._sqlOpts()).count("*", "n");
|
|
682
|
+
this._applyConditions(qb);
|
|
683
|
+
var built = qb.toSql();
|
|
684
|
+
var stmt = this._db.prepare(built.sql);
|
|
685
|
+
var row = stmt.get.apply(stmt, built.params);
|
|
589
686
|
return row ? row.n : 0;
|
|
590
687
|
}
|
|
591
688
|
|
|
@@ -605,7 +702,7 @@ class Query {
|
|
|
605
702
|
// (vault.row: cells). rowId MUST be withId._id — the same value
|
|
606
703
|
// b.subject.eraseHard / b.retention destroy on, so a later shred
|
|
607
704
|
// makes these cells undecryptable. Materialize stores the random
|
|
608
|
-
// row-secret AAD-sealed in
|
|
705
|
+
// row-secret AAD-sealed in the per-row-key store.
|
|
609
706
|
var sealOpts;
|
|
610
707
|
var cfKey = this._cryptoFieldKey();
|
|
611
708
|
if (cryptoField.hasPerRowKey(cfKey)) {
|
|
@@ -613,13 +710,9 @@ class Query {
|
|
|
613
710
|
sealOpts = { kRow: kRow, rowId: withId._id };
|
|
614
711
|
}
|
|
615
712
|
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);
|
|
713
|
+
var built = sql.insert(this._table, this._sqlOpts()).values(sealed).toSql();
|
|
714
|
+
var insertStmt = this._db.prepare(built.sql);
|
|
715
|
+
insertStmt.run.apply(insertStmt, built.params);
|
|
623
716
|
// Return the original row with _id filled in (plaintext, never sealed)
|
|
624
717
|
return Object.assign({}, withId);
|
|
625
718
|
}
|
|
@@ -646,7 +739,7 @@ class Query {
|
|
|
646
739
|
if (!changes || typeof changes !== "object") {
|
|
647
740
|
throw new Error("update requires a changes object");
|
|
648
741
|
}
|
|
649
|
-
if (this.
|
|
742
|
+
if (!this._hasConditions()) {
|
|
650
743
|
throw new Error("refusing unconditional update — call where(...) first");
|
|
651
744
|
}
|
|
652
745
|
// Residency gates on the plaintext change set — an UPDATE that
|
|
@@ -670,27 +763,106 @@ class Query {
|
|
|
670
763
|
setKeys.forEach(_validateField);
|
|
671
764
|
var selfUpd = this;
|
|
672
765
|
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
766
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
var
|
|
767
|
+
// No engine ships a portable UPDATE ... LIMIT, so a single-row update
|
|
768
|
+
// resolves exactly one row then writes it. The shape is dialect-aware
|
|
769
|
+
// (sqlite rowid sub-select / postgres PK sub-select / mysql
|
|
770
|
+
// resolve-then-write — _buildSingleRowWrite). A null result means the
|
|
771
|
+
// WHERE matched no row, so there is nothing to update (0 changes).
|
|
772
|
+
var built;
|
|
682
773
|
if (single) {
|
|
683
|
-
|
|
684
|
-
|
|
774
|
+
built = this._buildSingleRowWrite(sealed);
|
|
775
|
+
if (built === null) return 0;
|
|
685
776
|
} else {
|
|
686
|
-
|
|
777
|
+
var qb = sql.update(this._table, this._sqlOpts()).set(sealed);
|
|
778
|
+
this._applyConditions(qb);
|
|
779
|
+
built = qb.toSql();
|
|
687
780
|
}
|
|
688
|
-
var
|
|
689
|
-
var
|
|
690
|
-
var info = updStmt.run.apply(updStmt, allParams);
|
|
781
|
+
var updStmt = this._db.prepare(built.sql);
|
|
782
|
+
var info = updStmt.run.apply(updStmt, built.params);
|
|
691
783
|
return info.changes;
|
|
692
784
|
}
|
|
693
785
|
|
|
786
|
+
// The single-row-write row locator, by dialect. No engine ships
|
|
787
|
+
// UPDATE ... LIMIT portably (node:sqlite is built without
|
|
788
|
+
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT), so the single-row idiom is a
|
|
789
|
+
// sub-SELECT that resolves exactly one row then matches it:
|
|
790
|
+
//
|
|
791
|
+
// sqlite — the implicit `rowid` system column (every non-WITHOUT-
|
|
792
|
+
// ROWID table has one); `WHERE "rowid" = (SELECT "rowid"
|
|
793
|
+
// FROM t WHERE ... LIMIT 1)`.
|
|
794
|
+
// postgres — the table's PRIMARY KEY (`_id`, the db.from() convention).
|
|
795
|
+
// Postgres accepts LIMIT in a scalar subquery, so the same
|
|
796
|
+
// `= (SELECT "_id" ... LIMIT 1)` shape works — and using the
|
|
797
|
+
// real, UNIQUE `_id` column keeps b.sql's quote-by-
|
|
798
|
+
// construction intact (ctid is an unquotable system column
|
|
799
|
+
// that would force a raw-identifier escape and is unstable
|
|
800
|
+
// across VACUUM).
|
|
801
|
+
// mysql — also the PRIMARY KEY, but MySQL refuses LIMIT in a
|
|
802
|
+
// subquery that directly references the same table in an
|
|
803
|
+
// `IN`/`=` predicate; wrapping the inner SELECT in a derived
|
|
804
|
+
// table (`... IN (SELECT "_id" FROM (SELECT "_id" ... LIMIT
|
|
805
|
+
// 1) AS _s)`) is the standard work-around.
|
|
806
|
+
//
|
|
807
|
+
// The inner SELECT is composed through b.sql (same table + conditions)
|
|
808
|
+
// and spliced via whereSub — passing the inner BUILDER (not concatenated
|
|
809
|
+
// SQL) so b.sql concatenates the sub-query's sql + params itself and the
|
|
810
|
+
// final statement still runs through b.sql's output validator.
|
|
811
|
+
_rowLocatorColumn(dialect) {
|
|
812
|
+
return dialect === "sqlite" ? "rowid" : this._pkColumn();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// The PRIMARY KEY column for single-row writes on non-sqlite dialects.
|
|
816
|
+
// db.from() tables key on `_id` (auto-generated when absent on insert);
|
|
817
|
+
// an operator running a table with a different PK overrides it via the
|
|
818
|
+
// `primaryKey` construction opt.
|
|
819
|
+
_pkColumn() {
|
|
820
|
+
return this._primaryKey || "_id";
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
_buildSingleRowWrite(sealed) {
|
|
824
|
+
if (this._dialect() === "mysql") {
|
|
825
|
+
// MySQL forbids referencing the UPDATE/DELETE target table in a
|
|
826
|
+
// subquery (error 1093), so the single-statement sub-SELECT idiom
|
|
827
|
+
// the other dialects use is unavailable. Resolve the one row's PK in
|
|
828
|
+
// a prior SELECT, then write `WHERE pk = ?` with the resolved value
|
|
829
|
+
// bound — every value still binds, the identifier still quotes by
|
|
830
|
+
// construction, and the write is a single validated statement with no
|
|
831
|
+
// self-referential subquery. Returns null when no row matched.
|
|
832
|
+
var pkVal = this._resolveSinglePk();
|
|
833
|
+
if (pkVal === null) return null;
|
|
834
|
+
return sql.update(this._table, this._sqlOpts())
|
|
835
|
+
.set(sealed)
|
|
836
|
+
.where(this._pkColumn(), pkVal)
|
|
837
|
+
.toSql();
|
|
838
|
+
}
|
|
839
|
+
var col = this._rowLocatorColumn(this._dialect());
|
|
840
|
+
var inner = sql.select(this._table, this._sqlOpts()).columns([col]);
|
|
841
|
+
this._applyConditions(inner);
|
|
842
|
+
inner.limit(1);
|
|
843
|
+
return sql.update(this._table, this._sqlOpts())
|
|
844
|
+
.set(sealed)
|
|
845
|
+
.whereSub(col, "=", inner)
|
|
846
|
+
.toSql();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Resolve the PK of exactly one row matching the recorded WHERE (LIMIT
|
|
850
|
+
// 1). Used by the MySQL single-row write path, where a self-referential
|
|
851
|
+
// subquery is rejected by the engine. The SELECT is a clean, fully-bound
|
|
852
|
+
// b.sql statement; returns the PK value, or null when nothing matched.
|
|
853
|
+
_resolveSinglePk() {
|
|
854
|
+
var pk = this._pkColumn();
|
|
855
|
+
var pick = sql.select(this._table, this._sqlOpts()).columns([pk]);
|
|
856
|
+
this._applyConditions(pick);
|
|
857
|
+
pick.limit(1);
|
|
858
|
+
var built = pick.toSql();
|
|
859
|
+
var stmt = this._db.prepare(built.sql);
|
|
860
|
+
var row = stmt.get.apply(stmt, built.params);
|
|
861
|
+
if (!row) return null;
|
|
862
|
+
var v = row[pk];
|
|
863
|
+
return (v === undefined || v === null) ? null : v;
|
|
864
|
+
}
|
|
865
|
+
|
|
694
866
|
// Per-row-key UPDATE. Sealed columns on a declarePerRowKey table are
|
|
695
867
|
// K_row cells (vault.row:), so each affected row must be re-sealed
|
|
696
868
|
// under its OWN K_row — a single set-based UPDATE can't carry per-row
|
|
@@ -699,11 +871,12 @@ class Query {
|
|
|
699
871
|
// under it (derived hashes computed from plaintext as usual), and
|
|
700
872
|
// UPDATE that single row by _id. `single` stops after the first row.
|
|
701
873
|
_updatePerRowKey(cfKey, changes, single) {
|
|
702
|
-
var
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
var
|
|
874
|
+
var idSelect = sql.select(this._table, this._sqlOpts()).columns(["_id"]);
|
|
875
|
+
this._applyConditions(idSelect);
|
|
876
|
+
if (single) idSelect.limit(1);
|
|
877
|
+
var idBuilt = idSelect.toSql();
|
|
878
|
+
var idStmt = this._db.prepare(idBuilt.sql);
|
|
879
|
+
var idRows = idStmt.all.apply(idStmt, idBuilt.params);
|
|
707
880
|
var changed = 0;
|
|
708
881
|
for (var r = 0; r < idRows.length; r++) {
|
|
709
882
|
var rowId = idRows[r]._id;
|
|
@@ -717,10 +890,10 @@ class Query {
|
|
|
717
890
|
setKeys.forEach(_validateField);
|
|
718
891
|
var selfUpd = this;
|
|
719
892
|
setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
|
|
720
|
-
var
|
|
721
|
-
|
|
722
|
-
var updStmt = this._db.prepare(
|
|
723
|
-
var info = updStmt.run.apply(updStmt,
|
|
893
|
+
var built = sql.update(this._table, this._sqlOpts())
|
|
894
|
+
.set(sealed).where("_id", rowId).toSql();
|
|
895
|
+
var updStmt = this._db.prepare(built.sql);
|
|
896
|
+
var info = updStmt.run.apply(updStmt, built.params);
|
|
724
897
|
changed += (info && info.changes) || 0;
|
|
725
898
|
}
|
|
726
899
|
return changed;
|
|
@@ -737,9 +910,9 @@ class Query {
|
|
|
737
910
|
// Atomic counter increment.
|
|
738
911
|
//
|
|
739
912
|
// `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
|
|
913
|
+
// `UPDATE table SET col = COALESCE(col, 0) + ? WHERE ...` so concurrent
|
|
914
|
+
// writers can't collide on a fetch/mutate/store sequence (which would
|
|
915
|
+
// lose increments under racing transactions). Pass a negative delta to
|
|
743
916
|
// decrement.
|
|
744
917
|
//
|
|
745
918
|
// Returns the number of rows changed (matches updateMany shape).
|
|
@@ -753,19 +926,22 @@ class Query {
|
|
|
753
926
|
if (typeof delta !== "number" || !Number.isFinite(delta) || !Number.isInteger(delta)) {
|
|
754
927
|
throw new Error("increment(column, delta): delta must be a finite integer (default 1)");
|
|
755
928
|
}
|
|
756
|
-
if (this.
|
|
929
|
+
if (!this._hasConditions()) {
|
|
757
930
|
throw new Error("refusing unconditional increment — call where(...) first");
|
|
758
931
|
}
|
|
759
|
-
var whereSql = this._where.join(" AND ");
|
|
760
|
-
var qt = this._quotedTable();
|
|
761
|
-
var qc = '"' + column + '"';
|
|
762
932
|
// Use COALESCE so a NULL counter starts at 0 instead of producing
|
|
763
933
|
// NULL + delta = NULL silently (which would silently drop the
|
|
764
|
-
// operation under SQLite's NULL-arithmetic rules).
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
var
|
|
934
|
+
// operation under SQLite's NULL-arithmetic rules). The quoted column
|
|
935
|
+
// expression is built by b.safeSql under the active dialect so the
|
|
936
|
+
// increment RHS references the same quoted identifier b.sql's set
|
|
937
|
+
// target uses (double-quote on sqlite/postgres, backtick on mysql).
|
|
938
|
+
var qc = safeSql.quoteIdentifier(column, this._dialect(), { allowReserved: true });
|
|
939
|
+
var qb = sql.update(this._table, this._sqlOpts())
|
|
940
|
+
.setRaw(column, "COALESCE(" + qc + ", 0) + ?", [delta]);
|
|
941
|
+
this._applyConditions(qb);
|
|
942
|
+
var built = qb.toSql();
|
|
943
|
+
var stmt = this._db.prepare(built.sql);
|
|
944
|
+
var info = stmt.run.apply(stmt, built.params);
|
|
769
945
|
return info.changes;
|
|
770
946
|
}
|
|
771
947
|
|
|
@@ -785,10 +961,10 @@ class Query {
|
|
|
785
961
|
}
|
|
786
962
|
var sub = new WhereBuilder(this);
|
|
787
963
|
closure(sub);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
964
|
+
if (sub._parts.length === 0) return this;
|
|
965
|
+
this._pushLeaf("AND", function (pred) {
|
|
966
|
+
pred.whereGroup(function (g) { sub.replay(g); });
|
|
967
|
+
});
|
|
792
968
|
return this;
|
|
793
969
|
}
|
|
794
970
|
|
|
@@ -796,36 +972,61 @@ class Query {
|
|
|
796
972
|
// `.where(a).orWhere(b)` produces `WHERE (a) OR (b)` rather than
|
|
797
973
|
// `WHERE (a) AND (b)`. Accepts the same arg shapes as `.where`:
|
|
798
974
|
// object-literal map, `(field, value)`, `(field, op, value)`, or a
|
|
799
|
-
// `(qb) => ...` closure.
|
|
975
|
+
// `(qb) => ...` closure. Replays as `(prevLeaf OR newLeaf)` so the
|
|
976
|
+
// grouping precedence matches the pre-b.sql `( prev OR ( new ) )` form.
|
|
800
977
|
orWhere(fieldOrObjOrFn, op, value) {
|
|
801
|
-
if (this.
|
|
978
|
+
if (this._conditions.length === 0) {
|
|
802
979
|
throw new Error("orWhere(...): no prior where(...) — start the chain with where(...)");
|
|
803
980
|
}
|
|
981
|
+
var argc = arguments.length;
|
|
982
|
+
var prevLeaf = this._conditions.pop();
|
|
983
|
+
var orApply;
|
|
804
984
|
if (typeof fieldOrObjOrFn === "function") {
|
|
805
985
|
var sub = new WhereBuilder(this);
|
|
806
986
|
fieldOrObjOrFn(sub);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
987
|
+
if (sub._parts.length === 0) {
|
|
988
|
+
// Empty OR closure — restore the prior leaf untouched.
|
|
989
|
+
this._conditions.push(prevLeaf);
|
|
990
|
+
return this;
|
|
991
|
+
}
|
|
992
|
+
orApply = function (pred) {
|
|
993
|
+
pred.orWhereGroup(function (g) { sub.replay(g); });
|
|
994
|
+
};
|
|
995
|
+
} else if (fieldOrObjOrFn !== null && typeof fieldOrObjOrFn === "object" &&
|
|
996
|
+
!Array.isArray(fieldOrObjOrFn)) {
|
|
997
|
+
// Object map — all equalities OR'd as one group leaf.
|
|
998
|
+
var self = this;
|
|
999
|
+
var resolvedList = Object.keys(fieldOrObjOrFn).map(function (k) {
|
|
1000
|
+
return self._resolvePredicate(k, "=", fieldOrObjOrFn[k]);
|
|
1001
|
+
});
|
|
1002
|
+
orApply = function (pred) {
|
|
1003
|
+
pred.orWhereGroup(function (g) {
|
|
1004
|
+
for (var i = 0; i < resolvedList.length; i++) {
|
|
1005
|
+
self._emitPredicate(g, "AND", resolvedList[i].field, resolvedList[i].op,
|
|
1006
|
+
resolvedList[i].value);
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
};
|
|
821
1010
|
} else {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1011
|
+
// 2-arg orWhere(field, value) is the equality shorthand; 3-arg
|
|
1012
|
+
// orWhere(field, op, value) carries an explicit operator. Mirror
|
|
1013
|
+
// .where()'s arguments.length discrimination so a 2-arg value of
|
|
1014
|
+
// (e.g.) the number 5 is never mistaken for an operator.
|
|
1015
|
+
var resolved = (argc === 2)
|
|
1016
|
+
? this._resolvePredicate(fieldOrObjOrFn, "=", op)
|
|
1017
|
+
: this._resolvePredicate(fieldOrObjOrFn, op, value);
|
|
1018
|
+
var selfP = this;
|
|
1019
|
+
orApply = function (pred) {
|
|
1020
|
+
selfP._emitPredicate(pred, "OR", resolved.field, resolved.op, resolved.value);
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
// Re-push a single leaf that emits ( prevLeaf OR newLeaf ).
|
|
1024
|
+
this._pushLeaf("AND", function (pred) {
|
|
1025
|
+
pred.whereGroup(function (g) {
|
|
1026
|
+
prevLeaf.apply(g);
|
|
1027
|
+
orApply(g);
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
829
1030
|
return this;
|
|
830
1031
|
}
|
|
831
1032
|
|
|
@@ -851,22 +1052,24 @@ class Query {
|
|
|
851
1052
|
}
|
|
852
1053
|
if (term.length === 0) return this;
|
|
853
1054
|
var match = (opts && opts.match) || "substring";
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1055
|
+
if (match !== "exact" && match !== "prefix" && match !== "substring") {
|
|
1056
|
+
throw new Error("search: opts.match must be 'substring' | 'prefix' | 'exact'");
|
|
1057
|
+
}
|
|
1058
|
+
// b.sql's whereLike owns the wildcard handling end-to-end: it escapes
|
|
1059
|
+
// the user's `%` / `_` metacharacters with `~`, adds the LIVE wrapping
|
|
1060
|
+
// wildcard per mode, and emits `"field" LIKE ? ESCAPE '~'` (a
|
|
1061
|
+
// builder-emitted ESCAPE clause, so no raw-fragment guard refusal). An
|
|
1062
|
+
// OR group across every search field; the first leaf leads, the rest
|
|
1063
|
+
// OR-join.
|
|
1064
|
+
var fieldList = fields.slice();
|
|
1065
|
+
this._pushLeaf("AND", function (pred) {
|
|
1066
|
+
pred.whereGroup(function (g) {
|
|
1067
|
+
for (var i = 0; i < fieldList.length; i++) {
|
|
1068
|
+
if (i === 0) g.whereLike(fieldList[i], term, match);
|
|
1069
|
+
else g.orWhereLike(fieldList[i], term, match);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
870
1073
|
return this;
|
|
871
1074
|
}
|
|
872
1075
|
|
|
@@ -907,20 +1110,41 @@ class Query {
|
|
|
907
1110
|
}
|
|
908
1111
|
|
|
909
1112
|
_delete(single) {
|
|
910
|
-
if (this.
|
|
1113
|
+
if (!this._hasConditions()) {
|
|
911
1114
|
throw new Error("refusing unconditional delete — call where(...) first");
|
|
912
1115
|
}
|
|
913
|
-
var
|
|
914
|
-
var sql;
|
|
915
|
-
var qt = this._quotedTable();
|
|
1116
|
+
var built;
|
|
916
1117
|
if (single) {
|
|
917
|
-
|
|
918
|
-
|
|
1118
|
+
// No engine ships a portable DELETE ... LIMIT, so single-row delete
|
|
1119
|
+
// mirrors the single-row update idiom: sqlite splices a rowid
|
|
1120
|
+
// sub-select, postgres a PK sub-select (both via b.sql whereSub, the
|
|
1121
|
+
// inner builder object — b.sql concatenates the sub-query's sql +
|
|
1122
|
+
// params, no hand-rolled string), and mysql resolves the one PK in a
|
|
1123
|
+
// prior SELECT then deletes `WHERE pk = ?` (the engine forbids a
|
|
1124
|
+
// subquery referencing the DELETE target table). A null PK means the
|
|
1125
|
+
// WHERE matched nothing — 0 rows deleted.
|
|
1126
|
+
if (this._dialect() === "mysql") {
|
|
1127
|
+
var pkVal = this._resolveSinglePk();
|
|
1128
|
+
if (pkVal === null) return 0;
|
|
1129
|
+
built = sql.delete(this._table, this._sqlOpts())
|
|
1130
|
+
.where(this._pkColumn(), pkVal)
|
|
1131
|
+
.toSql();
|
|
1132
|
+
} else {
|
|
1133
|
+
var col = this._rowLocatorColumn(this._dialect());
|
|
1134
|
+
var inner = sql.select(this._table, this._sqlOpts()).columns([col]);
|
|
1135
|
+
this._applyConditions(inner);
|
|
1136
|
+
inner.limit(1);
|
|
1137
|
+
built = sql.delete(this._table, this._sqlOpts())
|
|
1138
|
+
.whereSub(col, "=", inner)
|
|
1139
|
+
.toSql();
|
|
1140
|
+
}
|
|
919
1141
|
} else {
|
|
920
|
-
|
|
1142
|
+
var dqb = sql.delete(this._table, this._sqlOpts());
|
|
1143
|
+
this._applyConditions(dqb);
|
|
1144
|
+
built = dqb.toSql();
|
|
921
1145
|
}
|
|
922
|
-
var delStmt = this._db.prepare(sql);
|
|
923
|
-
var info = delStmt.run.apply(delStmt,
|
|
1146
|
+
var delStmt = this._db.prepare(built.sql);
|
|
1147
|
+
var info = delStmt.run.apply(delStmt, built.params);
|
|
924
1148
|
return info.changes;
|
|
925
1149
|
}
|
|
926
1150
|
}
|
|
@@ -934,11 +1158,13 @@ class Query {
|
|
|
934
1158
|
// `.orGte` / `.orLt` / `.orLte` / `.orIn` / `.orLike` ORs an
|
|
935
1159
|
// expression. `.raw(sql, params)` AND's an arbitrary fragment.
|
|
936
1160
|
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
1161
|
+
// Each part is recorded structurally ({ joiner, kind, ... }) and replayed
|
|
1162
|
+
// onto a b.sql Predicate via replay(pred) — b.sql owns the quoting +
|
|
1163
|
+
// binding + LIKE escape + IN-list expansion. The owning Query runs the
|
|
1164
|
+
// column-membership gate as each part is recorded.
|
|
939
1165
|
class WhereBuilder {
|
|
940
1166
|
constructor(gate) {
|
|
941
|
-
this._parts = []; // [{ joiner: "
|
|
1167
|
+
this._parts = []; // [{ joiner, kind: "cmp"|"raw", ... }]
|
|
942
1168
|
// The owning Query, so grouped/OR sub-expressions enforce the
|
|
943
1169
|
// same column-membership gate as the top-level chain.
|
|
944
1170
|
this._gate = gate || null;
|
|
@@ -949,19 +1175,17 @@ class WhereBuilder {
|
|
|
949
1175
|
}
|
|
950
1176
|
_validateField(field);
|
|
951
1177
|
if (this._gate) this._gate._assertColumnMember(field, "whereGroup");
|
|
952
|
-
var qf = '"' + field + '"';
|
|
953
1178
|
if (op === "IN" || op === "NOT IN") {
|
|
954
1179
|
if (!Array.isArray(value) || value.length === 0) {
|
|
955
1180
|
throw new Error("WhereBuilder: " + op + " requires a non-empty array of values");
|
|
956
1181
|
}
|
|
957
|
-
|
|
958
|
-
this._parts.push({ joiner: joiner, sql: qf + " " + op + " (" + placeholders + ")", params: value.slice() });
|
|
1182
|
+
this._parts.push({ joiner: joiner, kind: "cmp", field: field, op: op, value: value.slice() });
|
|
959
1183
|
return this;
|
|
960
1184
|
}
|
|
961
|
-
if (!ALLOWED_OPS.has(op)) {
|
|
1185
|
+
if (!ALLOWED_OPS.has(op) && op !== "NOT IN") {
|
|
962
1186
|
throw new Error("WhereBuilder: invalid operator '" + op + "'");
|
|
963
1187
|
}
|
|
964
|
-
this._parts.push({ joiner: joiner,
|
|
1188
|
+
this._parts.push({ joiner: joiner, kind: "cmp", field: field, op: op, value: value });
|
|
965
1189
|
return this;
|
|
966
1190
|
}
|
|
967
1191
|
eq(f, v) { return this._push("AND", f, "=", v); }
|
|
@@ -980,56 +1204,67 @@ class WhereBuilder {
|
|
|
980
1204
|
orLte(f, v) { return this._push("OR", f, "<=", v); }
|
|
981
1205
|
orIn(f, vs) { return this._push("OR", f, "IN", vs); }
|
|
982
1206
|
orLike(f, v) { return this._push("OR", f, "LIKE", v); }
|
|
983
|
-
raw(
|
|
984
|
-
if (typeof
|
|
1207
|
+
raw(sql_, params, opts) {
|
|
1208
|
+
if (typeof sql_ !== "string" || sql_.length === 0) {
|
|
985
1209
|
throw new Error("WhereBuilder.raw: sql must be a non-empty string");
|
|
986
1210
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1211
|
+
var p = Array.isArray(params) ? params.slice() : (params == null ? [] : [params]);
|
|
1212
|
+
// Same fail-fast literal + placeholder-count contract as Query.whereRaw
|
|
1213
|
+
// (stable SafeSqlError code); b.sql re-guards at the terminal.
|
|
1214
|
+
if (!(opts && opts.allowLiterals === true)) _assertRawNoStringLiteral(sql_, "WhereBuilder.raw");
|
|
1215
|
+
if (safeSql.countPlaceholders(sql_) !== p.length) {
|
|
990
1216
|
throw new Error("WhereBuilder.raw: placeholder count mismatch");
|
|
991
1217
|
}
|
|
992
|
-
this._parts.push({ joiner: "AND",
|
|
1218
|
+
this._parts.push({ joiner: "AND", kind: "raw", sql: sql_, params: p, opts: opts });
|
|
993
1219
|
return this;
|
|
994
1220
|
}
|
|
1221
|
+
// Replay the recorded parts onto a b.sql Predicate. The first part
|
|
1222
|
+
// leads the group (its joiner is the group's first leaf); each later
|
|
1223
|
+
// part AND/OR-joins per its recorded joiner. b.sql performs identifier
|
|
1224
|
+
// quoting, value binding, and IN-list expansion.
|
|
1225
|
+
replay(pred) {
|
|
1226
|
+
for (var i = 0; i < this._parts.length; i++) {
|
|
1227
|
+
_replayPart(pred, this._parts[i], this._parts[i].joiner === "OR" && i > 0);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
995
1230
|
build() {
|
|
1231
|
+
// Back-compat shim for any external reader that called build() to get
|
|
1232
|
+
// a { sql, params } pair. Replay onto a transient b.sql SELECT's
|
|
1233
|
+
// predicate and extract. Returns { sql: "", params: [] } when empty.
|
|
996
1234
|
if (this._parts.length === 0) return { sql: "", params: [] };
|
|
997
|
-
var
|
|
998
|
-
var
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
return { sql: sql, params: params };
|
|
1235
|
+
var self = this;
|
|
1236
|
+
var built = sql.select("t", { dialect: "sqlite" })
|
|
1237
|
+
.whereGroup(function (g) { self.replay(g); })
|
|
1238
|
+
.toSql();
|
|
1239
|
+
// Strip the "SELECT * FROM t WHERE (" prefix + trailing ")".
|
|
1240
|
+
var m = /WHERE \((.*)\)$/.exec(built.sql);
|
|
1241
|
+
return { sql: m ? m[1] : "", params: built.params };
|
|
1006
1242
|
}
|
|
1007
1243
|
}
|
|
1008
1244
|
|
|
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,
|
|
1245
|
+
// Refuse a raw SQL fragment that embeds a single-quoted string literal.
|
|
1246
|
+
// A whereRaw / WhereBuilder.raw fragment is a STATIC template whose every
|
|
1247
|
+
// value binds through a `?` placeholder; an embedded `'...'` literal is
|
|
1248
|
+
// the signature of operator input concatenated into the query builder
|
|
1249
|
+
// (CWE-89 / CWE-564). Double-quoted identifiers (`"col"`), line comments,
|
|
1019
1250
|
// and block comments are skipped. Operators with a deliberate static
|
|
1020
|
-
// literal pass `{ allowLiterals: true }`.
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1251
|
+
// literal pass `{ allowLiterals: true }`. db-query runs this eagerly at
|
|
1252
|
+
// the chain-build boundary so the operator-facing `sql/raw-literal`
|
|
1253
|
+
// SafeSqlError contract is stable; b.sql's whereRaw re-guards the same
|
|
1254
|
+
// fragment at the terminal (b.guardSql + the emission-time validator).
|
|
1255
|
+
// Single linear pass, no backtracking regex; shares the scan shape with
|
|
1256
|
+
// b.safeSql.countPlaceholders.
|
|
1257
|
+
function _assertRawNoStringLiteral(rawSql, where) {
|
|
1023
1258
|
var i = 0;
|
|
1024
|
-
var len =
|
|
1259
|
+
var len = rawSql.length;
|
|
1025
1260
|
while (i < len) {
|
|
1026
|
-
var ch =
|
|
1027
|
-
var next = i + 1 < len ?
|
|
1261
|
+
var ch = rawSql.charAt(i);
|
|
1262
|
+
var next = i + 1 < len ? rawSql.charAt(i + 1) : "";
|
|
1028
1263
|
if (ch === '"') {
|
|
1029
1264
|
i += 1;
|
|
1030
1265
|
while (i < len) {
|
|
1031
|
-
if (
|
|
1032
|
-
if (
|
|
1266
|
+
if (rawSql.charAt(i) === '"') {
|
|
1267
|
+
if (rawSql.charAt(i + 1) === '"') { i += 2; continue; }
|
|
1033
1268
|
i += 1; break;
|
|
1034
1269
|
}
|
|
1035
1270
|
i += 1;
|
|
@@ -1037,12 +1272,12 @@ function _assertRawNoStringLiteral(sql, where) {
|
|
|
1037
1272
|
continue;
|
|
1038
1273
|
}
|
|
1039
1274
|
if (ch === "-" && next === "-") {
|
|
1040
|
-
while (i < len &&
|
|
1275
|
+
while (i < len && rawSql.charAt(i) !== "\n") i += 1;
|
|
1041
1276
|
continue;
|
|
1042
1277
|
}
|
|
1043
1278
|
if (ch === "/" && next === "*") {
|
|
1044
1279
|
i += 2;
|
|
1045
|
-
while (i < len && !(
|
|
1280
|
+
while (i < len && !(rawSql.charAt(i) === "*" && rawSql.charAt(i + 1) === "/")) i += 1;
|
|
1046
1281
|
i += 2;
|
|
1047
1282
|
continue;
|
|
1048
1283
|
}
|
|
@@ -1057,40 +1292,47 @@ function _assertRawNoStringLiteral(sql, where) {
|
|
|
1057
1292
|
}
|
|
1058
1293
|
}
|
|
1059
1294
|
|
|
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
|
-
|
|
1295
|
+
// Apply one recorded WhereBuilder part onto a b.sql Predicate. `or`
|
|
1296
|
+
// selects the OR-joining method (after the first leaf in a group); the
|
|
1297
|
+
// first leaf ignores its joiner (it leads the group). NOT IN and LIKE
|
|
1298
|
+
// are the two ops with a behavior the bare structured Predicate does not
|
|
1299
|
+
// expose 1:1: NOT IN has no orWhere* form, and the WhereBuilder LIKE is a
|
|
1300
|
+
// caller-controlled-wildcard LIKE (the value binds verbatim — no
|
|
1301
|
+
// auto-escape, matching the pre-b.sql WhereBuilder semantics, distinct
|
|
1302
|
+
// from .search() which escapes). Both compose through the guarded raw /
|
|
1303
|
+
// group surface without weakening anything.
|
|
1304
|
+
function _replayPart(pred, part, or) {
|
|
1305
|
+
if (part.kind === "raw") {
|
|
1306
|
+
if (or) pred.orWhereRaw(part.sql, part.params, part.opts);
|
|
1307
|
+
else pred.whereRaw(part.sql, part.params, part.opts);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (part.op === "LIKE") {
|
|
1311
|
+
// Verbatim LIKE — caller controls the wildcards (no escape clause),
|
|
1312
|
+
// exactly as the pre-migration WhereBuilder emitted `"f" LIKE ?`. The
|
|
1313
|
+
// identifier quoting follows the predicate's OWN dialect (the builder
|
|
1314
|
+
// it replays onto), so the LIKE column matches the surrounding query's
|
|
1315
|
+
// quoting on mysql (backtick) as well as sqlite/postgres (double-quote).
|
|
1316
|
+
var likeDialect = (pred && typeof pred._dialect === "function") ? pred._dialect() : "sqlite";
|
|
1317
|
+
var likeSql = safeSql.quoteIdentifier(part.field, likeDialect, { allowReserved: true }) + " LIKE ?";
|
|
1318
|
+
if (or) pred.orWhereRaw(likeSql, [part.value]);
|
|
1319
|
+
else pred.whereRaw(likeSql, [part.value]);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (part.op === "IN") {
|
|
1323
|
+
if (or) pred.orWhereIn(part.field, part.value);
|
|
1324
|
+
else pred.whereIn(part.field, part.value);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (part.op === "NOT IN") {
|
|
1328
|
+
// b.sql exposes no orWhereNotIn; emit an OR NOT-IN leaf as a
|
|
1329
|
+
// single-member OR group so the join precedence is preserved.
|
|
1330
|
+
if (or) pred.orWhereGroup(function (g) { g.whereNotIn(part.field, part.value); });
|
|
1331
|
+
else pred.whereNotIn(part.field, part.value);
|
|
1332
|
+
return;
|
|
1092
1333
|
}
|
|
1093
|
-
|
|
1334
|
+
if (or) pred.orWhereOp(part.field, part.op, part.value);
|
|
1335
|
+
else pred.whereOp(part.field, part.op, part.value);
|
|
1094
1336
|
}
|
|
1095
1337
|
|
|
1096
1338
|
function _validateField(field) {
|