@blamejs/core 0.6.2 → 0.6.3
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 +1 -0
- package/README.md +2 -1
- package/lib/external-db.js +372 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.6.x
|
|
10
10
|
|
|
11
|
+
- **0.6.2** (2026-05-01) — input validation + identifier-quoting consistency
|
|
11
12
|
- **0.6.1** (2026-05-01) — security tightenings + operator-facing jargon sweep
|
|
12
13
|
- **0.6.0** (2026-05-01) — wiki restructured into 22 focused pages + missing-primitive coverage
|
|
13
14
|
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The modern Node app is a 1,200-package supply-chain liability with no LTS calend
|
|
|
17
17
|
|
|
18
18
|
## Status
|
|
19
19
|
|
|
20
|
-
Pre-1.0. Usable end-to-end — operators can build production apps on it today; the surface is still subject to change before 1.0. Recent line is **v0.6.
|
|
20
|
+
Pre-1.0. Usable end-to-end — operators can build production apps on it today; the surface is still subject to change before 1.0. Recent line is **v0.6.3** ([releases](https://github.com/blamejs/blamejs/releases) · [npm](https://www.npmjs.com/package/@blamejs/core) · [container](https://github.com/blamejs/blamejs/pkgs/container/blamejs-wiki)).
|
|
21
21
|
|
|
22
22
|
```js
|
|
23
23
|
var b = require("@blamejs/core");
|
|
@@ -63,6 +63,7 @@ Full primitive-by-primitive docs live at [blamejs.com](https://blamejs.com), whi
|
|
|
63
63
|
- **Validation** — [Safe Parsers](https://blamejs.com/safe-parsers)
|
|
64
64
|
- **Communication** — [WebSockets](https://blamejs.com/websockets) · [Mail](https://blamejs.com/mail) · [Notifications](https://blamejs.com/notifications)
|
|
65
65
|
- **Tools** — [Observability](https://blamejs.com/observability) · [Testing](https://blamejs.com/testing) · [i18n & Locale](https://blamejs.com/i18n-locale) · [Format Helpers](https://blamejs.com/format-helpers)
|
|
66
|
+
- **Compliance** — [Compliance Patterns](https://blamejs.com/compliance-patterns)
|
|
66
67
|
- **Production** — [Cluster Mode](https://blamejs.com/cluster) · [Reliability](https://blamejs.com/reliability) · [Backup & Restore](https://blamejs.com/backup-restore)
|
|
67
68
|
|
|
68
69
|
## CLI
|
package/lib/external-db.js
CHANGED
|
@@ -54,6 +54,7 @@ var retryHelper = require("./retry");
|
|
|
54
54
|
var C = require("./constants");
|
|
55
55
|
var lazyRequire = require("./lazy-require");
|
|
56
56
|
var safeAsync = require("./safe-async");
|
|
57
|
+
var safeSql = require("./safe-sql");
|
|
57
58
|
var { ExternalDbError } = require("./framework-error");
|
|
58
59
|
|
|
59
60
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -187,6 +188,9 @@ function init(opts) {
|
|
|
187
188
|
residencyTag: cfg.residencyTag || "unrestricted",
|
|
188
189
|
breaker: new retryHelper.CircuitBreaker("externalDb:" + name, cfg.breaker),
|
|
189
190
|
retryConfig: cfg.retry || null,
|
|
191
|
+
replicas: _buildReplicas(name, cfg),
|
|
192
|
+
replicaIdx: 0, // round-robin cursor
|
|
193
|
+
replicaFallbackToPrimary: cfg.replicaFallbackToPrimary !== false,
|
|
190
194
|
};
|
|
191
195
|
}
|
|
192
196
|
|
|
@@ -380,6 +384,12 @@ async function shutdown() {
|
|
|
380
384
|
if (!initialized) return;
|
|
381
385
|
for (var name in backends) {
|
|
382
386
|
try { await backends[name].pool.drain(); } catch (_e) { /* best effort */ }
|
|
387
|
+
var bk = backends[name];
|
|
388
|
+
if (bk && bk.replicas) {
|
|
389
|
+
for (var i = 0; i < bk.replicas.length; i++) {
|
|
390
|
+
try { await bk.replicas[i].pool.drain(); } catch (_e) { /* best effort */ }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
383
393
|
}
|
|
384
394
|
backends = {};
|
|
385
395
|
defaultBackend = null;
|
|
@@ -400,9 +410,173 @@ function _requireInit() {
|
|
|
400
410
|
if (!initialized) throw _err("NOT_INITIALIZED", "externalDb.init() must be called first", true);
|
|
401
411
|
}
|
|
402
412
|
|
|
413
|
+
// ---- Read-replica routing ----
|
|
414
|
+
//
|
|
415
|
+
// Operators with a primary + replicas declare replicas alongside the
|
|
416
|
+
// primary backend config:
|
|
417
|
+
//
|
|
418
|
+
// externalDb.init({
|
|
419
|
+
// backends: {
|
|
420
|
+
// main: {
|
|
421
|
+
// connect, query, // primary
|
|
422
|
+
// replicas: [
|
|
423
|
+
// { connect: replica1, query, weight: 1 },
|
|
424
|
+
// { connect: replica2, query, weight: 2 },
|
|
425
|
+
// ],
|
|
426
|
+
// replicaFallbackToPrimary: true, // default; on all-replicas-unhealthy,
|
|
427
|
+
// // read.query falls back to primary
|
|
428
|
+
// },
|
|
429
|
+
// },
|
|
430
|
+
// });
|
|
431
|
+
//
|
|
432
|
+
// await externalDb.read.query("SELECT * FROM users"); // → replica
|
|
433
|
+
// await externalDb.write.query("INSERT INTO users ..."); // → primary
|
|
434
|
+
// await externalDb.query("..."); // → primary (legacy, unchanged)
|
|
435
|
+
//
|
|
436
|
+
// Load balancing: weighted round-robin (default weight 1). Weights
|
|
437
|
+
// expand into a static plan at init — a [w1, w2, w3] vector becomes a
|
|
438
|
+
// pre-built index sequence, then read.query() advances replicaIdx.
|
|
439
|
+
//
|
|
440
|
+
// Health: each replica tracks `lastFailureAt`. After UNHEALTHY_COOLDOWN_MS
|
|
441
|
+
// since the last failure, the replica re-enters the rotation. Operators
|
|
442
|
+
// observing all-replicas-down see read.query() fall back to primary
|
|
443
|
+
// (overridable via replicaFallbackToPrimary: false).
|
|
444
|
+
|
|
445
|
+
var REPLICA_UNHEALTHY_COOLDOWN_MS = C.TIME.seconds(30);
|
|
446
|
+
|
|
447
|
+
function _buildReplicas(backendName, cfg) {
|
|
448
|
+
if (!cfg.replicas) return null;
|
|
449
|
+
if (!Array.isArray(cfg.replicas) || cfg.replicas.length === 0) {
|
|
450
|
+
throw _err("INVALID_CONFIG",
|
|
451
|
+
"backend '" + backendName + "': replicas must be a non-empty array", true);
|
|
452
|
+
}
|
|
453
|
+
var out = [];
|
|
454
|
+
for (var i = 0; i < cfg.replicas.length; i++) {
|
|
455
|
+
var r = cfg.replicas[i];
|
|
456
|
+
if (!r || typeof r.connect !== "function") {
|
|
457
|
+
throw _err("INVALID_CONFIG",
|
|
458
|
+
"backend '" + backendName + "': replicas[" + i + "].connect must be a function", true);
|
|
459
|
+
}
|
|
460
|
+
if (typeof r.query !== "function") {
|
|
461
|
+
throw _err("INVALID_CONFIG",
|
|
462
|
+
"backend '" + backendName + "': replicas[" + i + "].query must be a function", true);
|
|
463
|
+
}
|
|
464
|
+
var weight = r.weight !== undefined ? r.weight : 1;
|
|
465
|
+
if (typeof weight !== "number" || !isFinite(weight) || weight <= 0 ||
|
|
466
|
+
Math.floor(weight) !== weight) {
|
|
467
|
+
throw _err("INVALID_CONFIG",
|
|
468
|
+
"backend '" + backendName + "': replicas[" + i + "].weight must be a positive integer", true);
|
|
469
|
+
}
|
|
470
|
+
out.push({
|
|
471
|
+
index: i,
|
|
472
|
+
pool: new Pool(backendName + ":replica:" + i, r),
|
|
473
|
+
query: r.query,
|
|
474
|
+
weight: weight,
|
|
475
|
+
lastFailureAt: 0,
|
|
476
|
+
consecutiveFailures: 0,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function _pickReplica(b) {
|
|
483
|
+
if (!b.replicas || b.replicas.length === 0) return null;
|
|
484
|
+
var now = Date.now();
|
|
485
|
+
// Build a healthy candidate set.
|
|
486
|
+
var healthy = [];
|
|
487
|
+
for (var i = 0; i < b.replicas.length; i++) {
|
|
488
|
+
var r = b.replicas[i];
|
|
489
|
+
if (now - r.lastFailureAt >= REPLICA_UNHEALTHY_COOLDOWN_MS) healthy.push(r);
|
|
490
|
+
}
|
|
491
|
+
if (healthy.length === 0) return null;
|
|
492
|
+
// Weighted round-robin: walk by weight, advancing replicaIdx by 1 each
|
|
493
|
+
// call and modding by total weight. Each replica's "slot" in the
|
|
494
|
+
// sequence repeats `weight` times.
|
|
495
|
+
var totalWeight = 0;
|
|
496
|
+
for (var w = 0; w < healthy.length; w++) totalWeight += healthy[w].weight;
|
|
497
|
+
var cursor = (b.replicaIdx++) % totalWeight;
|
|
498
|
+
var acc = 0;
|
|
499
|
+
for (var c = 0; c < healthy.length; c++) {
|
|
500
|
+
acc += healthy[c].weight;
|
|
501
|
+
if (cursor < acc) return healthy[c];
|
|
502
|
+
}
|
|
503
|
+
return healthy[0]; // unreachable; defensive
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function _readQuery(sql, params, opts) {
|
|
507
|
+
_requireInit();
|
|
508
|
+
opts = opts || {};
|
|
509
|
+
var b = _pickBackend(opts);
|
|
510
|
+
if (!b.replicas || b.replicas.length === 0) {
|
|
511
|
+
// No replicas configured — read.query() returns primary.
|
|
512
|
+
return query(sql, params, opts);
|
|
513
|
+
}
|
|
514
|
+
var replica = _pickReplica(b);
|
|
515
|
+
if (!replica) {
|
|
516
|
+
if (b.replicaFallbackToPrimary) return query(sql, params, opts);
|
|
517
|
+
throw _err("ALL_REPLICAS_UNHEALTHY",
|
|
518
|
+
"backend '" + b.name + "': all replicas unhealthy and fallback disabled", true);
|
|
519
|
+
}
|
|
520
|
+
var t0 = Date.now();
|
|
521
|
+
try {
|
|
522
|
+
var client = await replica.pool.acquire();
|
|
523
|
+
try {
|
|
524
|
+
var res = await replica.query(client, sql, params || []);
|
|
525
|
+
replica.pool.release(client);
|
|
526
|
+
replica.consecutiveFailures = 0;
|
|
527
|
+
_emit("system.externaldb.read", "success", {
|
|
528
|
+
backend: b.name,
|
|
529
|
+
replicaIdx: replica.index,
|
|
530
|
+
durationMs: Date.now() - t0,
|
|
531
|
+
rowCount: res && res.rowCount,
|
|
532
|
+
});
|
|
533
|
+
return res;
|
|
534
|
+
} catch (e) {
|
|
535
|
+
// Connection-shape errors mark unhealthy + destroy.
|
|
536
|
+
if (e && (e.code === "ECONNRESET" || e.code === "ECONNREFUSED" ||
|
|
537
|
+
e.code === "ETIMEDOUT" || e.code === "ENOTFOUND" ||
|
|
538
|
+
e.code === "EPIPE")) {
|
|
539
|
+
await replica.pool.destroy(client);
|
|
540
|
+
replica.lastFailureAt = Date.now();
|
|
541
|
+
replica.consecutiveFailures += 1;
|
|
542
|
+
} else {
|
|
543
|
+
replica.pool.release(client);
|
|
544
|
+
}
|
|
545
|
+
throw e;
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
_emit("system.externaldb.read", "failure", {
|
|
549
|
+
backend: b.name,
|
|
550
|
+
replicaIdx: replica.index,
|
|
551
|
+
durationMs: Date.now() - t0,
|
|
552
|
+
errorCode: e.code || null,
|
|
553
|
+
}, (e && e.message) || String(e));
|
|
554
|
+
// Fallback to primary on a failed replica read when allowed.
|
|
555
|
+
if (b.replicaFallbackToPrimary) {
|
|
556
|
+
return query(sql, params, opts);
|
|
557
|
+
}
|
|
558
|
+
throw e;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
var read = {
|
|
563
|
+
query: _readQuery,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// write namespace — alias for the primary path. Lets operators express
|
|
567
|
+
// intent symmetrically with read.query without a magic-comment hint.
|
|
568
|
+
var write = {
|
|
569
|
+
query: function (sql, params, opts) { return query(sql, params, opts); },
|
|
570
|
+
transaction: function (fn, opts) { return transaction(fn, opts); },
|
|
571
|
+
};
|
|
572
|
+
|
|
403
573
|
function _resetForTest() {
|
|
404
574
|
Object.keys(backends).forEach(function (n) {
|
|
405
575
|
try { backends[n].pool.drain(); } catch (_e) {}
|
|
576
|
+
var bk = backends[n];
|
|
577
|
+
if (bk && bk.replicas) {
|
|
578
|
+
bk.replicas.forEach(function (r) { try { r.pool.drain(); } catch (_e) {} });
|
|
579
|
+
}
|
|
406
580
|
});
|
|
407
581
|
backends = {};
|
|
408
582
|
defaultBackend = null;
|
|
@@ -411,6 +585,198 @@ function _resetForTest() {
|
|
|
411
585
|
db.reset();
|
|
412
586
|
}
|
|
413
587
|
|
|
588
|
+
// ---- configurePool — runtime resize of an existing backend's pool ----
|
|
589
|
+
//
|
|
590
|
+
// Operators tune pool sizing without restarting the app. Existing idle
|
|
591
|
+
// clients are kept; new acquisitions respect the new max. min is honored
|
|
592
|
+
// the next time the pool refills. idleTimeoutMs takes effect on the next
|
|
593
|
+
// reaper tick.
|
|
594
|
+
function configurePool(backendName, opts) {
|
|
595
|
+
_requireInit();
|
|
596
|
+
if (typeof backendName !== "string" || backendName.length === 0) {
|
|
597
|
+
throw _err("INVALID_CONFIG", "configurePool: backendName must be a non-empty string", true);
|
|
598
|
+
}
|
|
599
|
+
var bk = backends[backendName];
|
|
600
|
+
if (!bk) throw _err("UNKNOWN_BACKEND", "configurePool: no backend named '" + backendName + "'", true);
|
|
601
|
+
if (!opts || typeof opts !== "object") {
|
|
602
|
+
throw _err("INVALID_CONFIG", "configurePool: opts must be an object", true);
|
|
603
|
+
}
|
|
604
|
+
var allowed = ["min", "max", "idleTimeoutMs"];
|
|
605
|
+
for (var k in opts) {
|
|
606
|
+
if (!Object.prototype.hasOwnProperty.call(opts, k)) continue;
|
|
607
|
+
if (allowed.indexOf(k) === -1) {
|
|
608
|
+
throw _err("INVALID_CONFIG",
|
|
609
|
+
"configurePool: unknown option '" + k + "'. Allowed: " + allowed.join(", "), true);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function _requirePosInt(name, value) {
|
|
613
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0 || Math.floor(value) !== value) {
|
|
614
|
+
throw _err("INVALID_CONFIG",
|
|
615
|
+
"configurePool: " + name + " must be a positive integer, got " + JSON.stringify(value), true);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (opts.min !== undefined) _requirePosInt("min", opts.min);
|
|
619
|
+
if (opts.max !== undefined) _requirePosInt("max", opts.max);
|
|
620
|
+
if (opts.idleTimeoutMs !== undefined) _requirePosInt("idleTimeoutMs", opts.idleTimeoutMs);
|
|
621
|
+
if (opts.min !== undefined && opts.max !== undefined && opts.min > opts.max) {
|
|
622
|
+
throw _err("INVALID_CONFIG", "configurePool: min must be <= max", true);
|
|
623
|
+
}
|
|
624
|
+
Object.assign(bk.pool.config, opts);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ---- adapters.connectAs — Postgres role-aware connect wrapper ----
|
|
628
|
+
//
|
|
629
|
+
// Wraps an operator's connect() so that every fresh client runs
|
|
630
|
+
// `SET ROLE`, `SET search_path`, `SET application_name`, and any other
|
|
631
|
+
// operator-supplied GUCs at acquire time. The pattern enables the
|
|
632
|
+
// search_path-views shape: the same SQL `SELECT * FROM sessions`
|
|
633
|
+
// resolves to `public.sessions` for app_user and to
|
|
634
|
+
// `analytics.sessions` (a view with PHI redacted) for analytics_user.
|
|
635
|
+
// See the "Compliance Patterns" wiki page.
|
|
636
|
+
//
|
|
637
|
+
// Identifier inputs (role, schemas in searchPath) are validated via
|
|
638
|
+
// safeSql.validateIdentifier — bad shapes throw at the call site. String
|
|
639
|
+
// values (applicationName, statement_timeout) are quoted as SQL string
|
|
640
|
+
// literals with single-quote escaping per the SQL standard.
|
|
641
|
+
//
|
|
642
|
+
// connect: b.externalDb.adapters.connectAs(rawConnect, {
|
|
643
|
+
// role: "analytics_user",
|
|
644
|
+
// searchPath: ["analytics", "public"],
|
|
645
|
+
// applicationName: "wiki:analytics",
|
|
646
|
+
// statementTimeoutMs: C.TIME.seconds(30),
|
|
647
|
+
// gucs: {
|
|
648
|
+
// idle_in_transaction_session_timeout: "60s",
|
|
649
|
+
// },
|
|
650
|
+
// })
|
|
651
|
+
//
|
|
652
|
+
// `query` is the same query function the backend declares; the wrapper
|
|
653
|
+
// uses it to issue the SET statements.
|
|
654
|
+
function _connectAs(rawConnect, query, opts) {
|
|
655
|
+
if (typeof rawConnect !== "function") {
|
|
656
|
+
throw _err("INVALID_CONFIG", "connectAs: connect must be a function", true);
|
|
657
|
+
}
|
|
658
|
+
if (typeof query !== "function") {
|
|
659
|
+
throw _err("INVALID_CONFIG", "connectAs: query must be a function", true);
|
|
660
|
+
}
|
|
661
|
+
opts = opts || {};
|
|
662
|
+
var allowed = ["role", "searchPath", "applicationName", "statementTimeoutMs", "gucs"];
|
|
663
|
+
for (var k in opts) {
|
|
664
|
+
if (!Object.prototype.hasOwnProperty.call(opts, k)) continue;
|
|
665
|
+
if (allowed.indexOf(k) === -1) {
|
|
666
|
+
throw _err("INVALID_CONFIG",
|
|
667
|
+
"connectAs: unknown option '" + k + "'. Allowed: " + allowed.join(", "), true);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Validate inputs at config time so a malformed name surfaces at
|
|
672
|
+
// boot rather than on the first connection.
|
|
673
|
+
if (opts.role !== undefined) {
|
|
674
|
+
safeSql.validateIdentifier(String(opts.role), { allowReserved: false });
|
|
675
|
+
}
|
|
676
|
+
var pathSegments = null;
|
|
677
|
+
if (opts.searchPath !== undefined) {
|
|
678
|
+
var raw = Array.isArray(opts.searchPath) ? opts.searchPath : [opts.searchPath];
|
|
679
|
+
if (raw.length === 0) {
|
|
680
|
+
throw _err("INVALID_CONFIG", "connectAs: searchPath must have at least one schema", true);
|
|
681
|
+
}
|
|
682
|
+
pathSegments = [];
|
|
683
|
+
for (var pi = 0; pi < raw.length; pi++) {
|
|
684
|
+
safeSql.validateIdentifier(String(raw[pi]), { allowReserved: false });
|
|
685
|
+
pathSegments.push(String(raw[pi]));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (opts.applicationName !== undefined && typeof opts.applicationName !== "string") {
|
|
689
|
+
throw _err("INVALID_CONFIG", "connectAs: applicationName must be a string", true);
|
|
690
|
+
}
|
|
691
|
+
if (opts.statementTimeoutMs !== undefined) {
|
|
692
|
+
if (typeof opts.statementTimeoutMs !== "number" || !isFinite(opts.statementTimeoutMs) ||
|
|
693
|
+
opts.statementTimeoutMs <= 0 || Math.floor(opts.statementTimeoutMs) !== opts.statementTimeoutMs) {
|
|
694
|
+
throw _err("INVALID_CONFIG",
|
|
695
|
+
"connectAs: statementTimeoutMs must be a positive integer", true);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (opts.gucs !== undefined && (typeof opts.gucs !== "object" || opts.gucs === null)) {
|
|
699
|
+
throw _err("INVALID_CONFIG", "connectAs: gucs must be an object", true);
|
|
700
|
+
}
|
|
701
|
+
if (opts.gucs) {
|
|
702
|
+
for (var gname in opts.gucs) {
|
|
703
|
+
// GUC names: Postgres NAMEDATALEN-shaped identifiers.
|
|
704
|
+
safeSql.validateIdentifier(gname, { allowReserved: true });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Pre-compute the SET statements once — every fresh client runs the
|
|
709
|
+
// same list, so building it per-connect would burn microbenchmarks.
|
|
710
|
+
var stmts = [];
|
|
711
|
+
if (opts.role) {
|
|
712
|
+
stmts.push('SET ROLE "' + opts.role + '"');
|
|
713
|
+
}
|
|
714
|
+
if (pathSegments) {
|
|
715
|
+
var pathSql = pathSegments.map(function (s) { return '"' + s + '"'; }).join(", ");
|
|
716
|
+
stmts.push("SET search_path TO " + pathSql);
|
|
717
|
+
}
|
|
718
|
+
if (opts.applicationName !== undefined) {
|
|
719
|
+
// Single-quoted string literal — SQL-standard escape doubles embedded
|
|
720
|
+
// single quotes.
|
|
721
|
+
var an = String(opts.applicationName).replace(/'/g, "''");
|
|
722
|
+
stmts.push("SET application_name TO '" + an + "'");
|
|
723
|
+
}
|
|
724
|
+
if (opts.statementTimeoutMs !== undefined) {
|
|
725
|
+
stmts.push("SET statement_timeout TO " + opts.statementTimeoutMs);
|
|
726
|
+
}
|
|
727
|
+
if (opts.gucs) {
|
|
728
|
+
for (var gn in opts.gucs) {
|
|
729
|
+
var gv = opts.gucs[gn];
|
|
730
|
+
if (typeof gv === "number") {
|
|
731
|
+
stmts.push('SET "' + gn + '" TO ' + gv);
|
|
732
|
+
} else {
|
|
733
|
+
var gvs = String(gv).replace(/'/g, "''");
|
|
734
|
+
stmts.push('SET "' + gn + '" TO \'' + gvs + "'");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return async function wrappedConnect() {
|
|
740
|
+
var client = await rawConnect();
|
|
741
|
+
try {
|
|
742
|
+
for (var i = 0; i < stmts.length; i++) {
|
|
743
|
+
await query(client, stmts[i], []);
|
|
744
|
+
}
|
|
745
|
+
} catch (e) {
|
|
746
|
+
// Initialization failed — the operator's close hook isn't visible
|
|
747
|
+
// here, so we throw and let the pool's catch destroy the partial
|
|
748
|
+
// client.
|
|
749
|
+
throw e;
|
|
750
|
+
}
|
|
751
|
+
return client;
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Operators import the helper as `b.externalDb.adapters.connectAs(connect, opts)`
|
|
756
|
+
// — declarative wrapping with shared input validation.
|
|
757
|
+
function _adaptersConnectAs(connect, opts) {
|
|
758
|
+
// The backend's query function is needed to issue SET statements on a
|
|
759
|
+
// freshly-acquired client. Operators pass it via opts.query — same
|
|
760
|
+
// function they declare on the backend itself.
|
|
761
|
+
if (!opts || typeof opts !== "object") {
|
|
762
|
+
throw _err("INVALID_CONFIG",
|
|
763
|
+
"adapters.connectAs: opts must be an object", true);
|
|
764
|
+
}
|
|
765
|
+
if (typeof opts.query !== "function") {
|
|
766
|
+
throw _err("INVALID_CONFIG",
|
|
767
|
+
"adapters.connectAs: opts.query is required (the backend's query function)", true);
|
|
768
|
+
}
|
|
769
|
+
// Pull query off and pass the remaining role-aware opts.
|
|
770
|
+
var query = opts.query;
|
|
771
|
+
var roleOpts = {};
|
|
772
|
+
for (var k in opts) {
|
|
773
|
+
if (Object.prototype.hasOwnProperty.call(opts, k) && k !== "query") {
|
|
774
|
+
roleOpts[k] = opts[k];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return _connectAs(connect, query, roleOpts);
|
|
778
|
+
}
|
|
779
|
+
|
|
414
780
|
module.exports = {
|
|
415
781
|
init: init,
|
|
416
782
|
query: query,
|
|
@@ -418,6 +784,12 @@ module.exports = {
|
|
|
418
784
|
healthCheck: healthCheck,
|
|
419
785
|
listBackends: listBackends,
|
|
420
786
|
shutdown: shutdown,
|
|
787
|
+
configurePool: configurePool,
|
|
788
|
+
read: read,
|
|
789
|
+
write: write,
|
|
790
|
+
adapters: {
|
|
791
|
+
connectAs: _adaptersConnectAs,
|
|
792
|
+
},
|
|
421
793
|
Pool: Pool,
|
|
422
794
|
_resetForTest: _resetForTest,
|
|
423
795
|
};
|