@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
|
@@ -48,15 +48,35 @@
|
|
|
48
48
|
var C = require("./constants");
|
|
49
49
|
var { generateToken } = require("./crypto");
|
|
50
50
|
var externalDb = require("./external-db");
|
|
51
|
+
var frameworkSchema = require("./framework-schema");
|
|
52
|
+
var lazyRequire = require("./lazy-require");
|
|
51
53
|
var { ClusterProviderError } = require("./framework-error");
|
|
52
54
|
|
|
53
55
|
var _err = ClusterProviderError.factory;
|
|
54
56
|
|
|
57
|
+
// Lazy requires — cluster.js requires this module while cluster-storage /
|
|
58
|
+
// sql are still mid-load (cluster -> cluster-provider-db -> external-db ->
|
|
59
|
+
// external-db-migrate -> cluster-storage -> cluster), so a top-of-file
|
|
60
|
+
// require would resolve to an unfinished module. clusterStorage.placeholderize
|
|
61
|
+
// translates the b.sql `?` output to Postgres `$N`; sql is the b.sql builder.
|
|
62
|
+
// Both are resolved at first SQL emission, by which point the cycle has settled.
|
|
63
|
+
var clusterStorage = lazyRequire(function () { return require("./cluster-storage"); });
|
|
64
|
+
var sql = lazyRequire(function () { return require("./sql"); });
|
|
65
|
+
|
|
55
66
|
function create(config) {
|
|
56
67
|
if (!config || !config.externalDbBackend) {
|
|
57
68
|
throw _err("INVALID_CONFIG",
|
|
58
69
|
"cluster-provider-db requires { externalDbBackend: <name> }", true);
|
|
59
70
|
}
|
|
71
|
+
|
|
72
|
+
// The coordination tables, resolved through frameworkSchema so the
|
|
73
|
+
// configurable framework-table prefix is honored. These names are
|
|
74
|
+
// already `_blamejs_`-prefixed; the resolve is a no-op under the default
|
|
75
|
+
// prefix and namespaces them under a custom one. Resolved here (not at
|
|
76
|
+
// module load) because cluster.js requires this module while
|
|
77
|
+
// framework-schema is mid-load — its tableName export is not yet bound.
|
|
78
|
+
var LEADER_TABLE = frameworkSchema.tableName("_blamejs_leader"); // allow:hand-rolled-sql — single canonical logical-name reference
|
|
79
|
+
var STATE_TABLE = frameworkSchema.tableName("_blamejs_cluster_state"); // allow:hand-rolled-sql — single canonical logical-name reference
|
|
60
80
|
var backendName = config.externalDbBackend;
|
|
61
81
|
var dialect = (config.dialect || "postgres").toLowerCase();
|
|
62
82
|
if (dialect !== "postgres" && dialect !== "sqlite" && dialect !== "mysql") {
|
|
@@ -65,16 +85,45 @@ function create(config) {
|
|
|
65
85
|
true);
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
// The backtick / double-quote identifier-quote char b.sql raw fragments
|
|
89
|
+
// (the upsert fencing increment + conflict guard) must use so a fragment
|
|
90
|
+
// composes into the dialect-final statement with consistent quoting.
|
|
91
|
+
var qchar = dialect === "mysql" ? "`" : "\"";
|
|
92
|
+
function _qraw(col) { return qchar + col + qchar; }
|
|
93
|
+
|
|
94
|
+
// Existing-row self-reference inside an upsert's DO UPDATE / conflict guard.
|
|
95
|
+
// Postgres' ON CONFLICT DO UPDATE puts BOTH the target row and the `excluded`
|
|
96
|
+
// proposed row in scope with identical columns, so a bare column is ambiguous
|
|
97
|
+
// (SQLSTATE 42702) — the existing-row reference must be table-qualified.
|
|
98
|
+
// SQLite resolves a bare column to the target (no `excluded` ambiguity); MySQL
|
|
99
|
+
// uses the ON DUPLICATE KEY IF()-fold (no `excluded` table) and b.sql reads the
|
|
100
|
+
// bare column for that fold — so both keep the unqualified form.
|
|
101
|
+
function _selfCol(col) {
|
|
102
|
+
return dialect === "postgres" ? (LEADER_TABLE + "." + _qraw(col)) : _qraw(col);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Emit a b.sql builder to dialect-final { sql, params }: b.sql always
|
|
106
|
+
// emits `?` placeholders, so translate them to `$N` for Postgres (the
|
|
107
|
+
// externalDb driver receives the SQL verbatim and never renumbers).
|
|
108
|
+
// SQLite + MySQL both accept `?`.
|
|
109
|
+
function _emit(builder) {
|
|
110
|
+
var built = builder.toSql();
|
|
111
|
+
return {
|
|
112
|
+
sql: clusterStorage().placeholderize(built.sql, dialect),
|
|
113
|
+
params: built.params,
|
|
114
|
+
};
|
|
72
115
|
}
|
|
73
116
|
|
|
74
117
|
function _q(sql, params) {
|
|
75
118
|
return externalDb.query(sql, params || [], { backend: backendName });
|
|
76
119
|
}
|
|
77
120
|
|
|
121
|
+
// Run a b.sql builder against the backend.
|
|
122
|
+
function _run(builder) {
|
|
123
|
+
var e = _emit(builder);
|
|
124
|
+
return _q(e.sql, e.params);
|
|
125
|
+
}
|
|
126
|
+
|
|
78
127
|
async function ensureSchema() {
|
|
79
128
|
// Postgres + MySQL: BIGINT for ms-precision timestamps. SQLite:
|
|
80
129
|
// INTEGER (which is wide enough to hold a 64-bit value).
|
|
@@ -90,38 +139,38 @@ function create(config) {
|
|
|
90
139
|
// belt-and-braces — application code only ever writes 'leader' /
|
|
91
140
|
// 'state' so the check is informational. Skip on MySQL to avoid
|
|
92
141
|
// CREATE TABLE failures on installations where CHECK is parsed
|
|
93
|
-
// but then dropped silently (which would cause version drift).
|
|
94
|
-
|
|
95
|
-
|
|
142
|
+
// but then dropped silently (which would cause version drift). The
|
|
143
|
+
// scope CHECK is a static, operator-controlled literal carried as the
|
|
144
|
+
// last column's verbatim constraint (b.sql guards it with allowLiterals
|
|
145
|
+
// since it is not operator input).
|
|
146
|
+
var leaderCheck = dialect === "mysql" ? "" : ", CHECK (scope = 'leader')"; // allow:hand-rolled-sql — static DDL CHECK literal
|
|
147
|
+
var stateCheck = dialect === "mysql" ? "" : ", CHECK (scope = 'state')"; // allow:hand-rolled-sql — static DDL CHECK literal
|
|
96
148
|
|
|
97
|
-
await _q(
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
")"
|
|
107
|
-
);
|
|
149
|
+
await _q(sql().createTable(LEADER_TABLE, [
|
|
150
|
+
{ name: "scope", type: pkText, primaryKey: true },
|
|
151
|
+
{ name: "nodeId", type: bodyText, notNull: true },
|
|
152
|
+
{ name: "leaseId", type: bodyText, notNull: true },
|
|
153
|
+
{ name: "acquiredAt", type: intType, notNull: true },
|
|
154
|
+
{ name: "expiresAt", type: intType, notNull: true },
|
|
155
|
+
{ name: "fencingToken", type: intType, notNull: true },
|
|
156
|
+
{ name: "endpoint", type: bodyText, constraints: leaderCheck },
|
|
157
|
+
], { dialect: dialect }).sql);
|
|
108
158
|
// Migration for installs that pre-date the endpoint column. Both
|
|
109
159
|
// Postgres (≥9.6) and SQLite (≥3.35, March 2021) support ADD COLUMN
|
|
110
160
|
// IF NOT EXISTS; MySQL 8.0.29+ does as well. We go through try/catch
|
|
111
161
|
// to keep the path version-agnostic — the only "expected" failure
|
|
112
162
|
// here is "column already exists," which we swallow.
|
|
113
163
|
try {
|
|
114
|
-
await _q(
|
|
164
|
+
await _q(sql().alterTable(LEADER_TABLE,
|
|
165
|
+
{ addColumn: { name: "endpoint", type: bodyText } }, { dialect: dialect }).sql);
|
|
115
166
|
} catch (_e) { /* column already exists — fine */ }
|
|
116
167
|
|
|
117
|
-
await _q(
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
")"
|
|
124
|
-
);
|
|
168
|
+
await _q(sql().createTable(STATE_TABLE, [
|
|
169
|
+
{ name: "scope", type: pkText, primaryKey: true },
|
|
170
|
+
{ name: "vaultKeyFp", type: bodyText, notNull: true },
|
|
171
|
+
{ name: "recordedAt", type: intType, notNull: true },
|
|
172
|
+
{ name: "recordedByNode", type: bodyText, notNull: true, constraints: stateCheck },
|
|
173
|
+
], { dialect: dialect }).sql);
|
|
125
174
|
}
|
|
126
175
|
|
|
127
176
|
async function acquireLease(nodeId, leaseTtlMs, opts) {
|
|
@@ -134,60 +183,45 @@ function create(config) {
|
|
|
134
183
|
var nowMs = Date.now();
|
|
135
184
|
var expiresAt = nowMs + leaseTtlMs;
|
|
136
185
|
|
|
186
|
+
// One upsert builder serves every dialect. Postgres / SQLite emit
|
|
187
|
+
// `INSERT ... ON CONFLICT (scope) DO UPDATE SET ... WHERE expiresAt < ?
|
|
188
|
+
// RETURNING ...` (atomic acquire-or-steal in one statement). MySQL has
|
|
189
|
+
// no `ON CONFLICT ... WHERE` / `RETURNING`, so b.sql folds the conflict
|
|
190
|
+
// guard into `IF(expiresAt < ?, <new>, <old>)` per column (the still-
|
|
191
|
+
// valid lease is preserved, the expired one overwritten) and emits a
|
|
192
|
+
// readback SELECT keyed on scope='leader'. The fencing-token bump
|
|
193
|
+
// (`fencingToken + 1`) is the only non-proposed-value assignment; every
|
|
194
|
+
// other column re-binds the proposed value (equivalent to EXCLUDED.col /
|
|
195
|
+
// VALUES(col)). guardColumn names expiresAt so the MySQL fold assigns it
|
|
196
|
+
// LAST — IF() evaluates each column against the PRE-update row state, so
|
|
197
|
+
// expiresAt must change after the columns whose guard reads it.
|
|
198
|
+
var acquire = sql().upsert(LEADER_TABLE, { dialect: dialect })
|
|
199
|
+
.columns(["scope", "nodeId", "leaseId", "acquiredAt", "expiresAt", "fencingToken", "endpoint"])
|
|
200
|
+
.values({
|
|
201
|
+
scope: "leader", nodeId: nodeId, leaseId: leaseId,
|
|
202
|
+
acquiredAt: nowMs, expiresAt: expiresAt, fencingToken: 1, endpoint: endpoint,
|
|
203
|
+
})
|
|
204
|
+
.doUpdate({
|
|
205
|
+
nodeId: "?", leaseId: "?", acquiredAt: "?", expiresAt: "?",
|
|
206
|
+
fencingToken: _selfCol("fencingToken") + " + 1",
|
|
207
|
+
endpoint: "?",
|
|
208
|
+
}, [nodeId, leaseId, nowMs, expiresAt, endpoint])
|
|
209
|
+
.conflictWhere(_selfCol("expiresAt") + " < ?", [nowMs], { guardColumn: "expiresAt" })
|
|
210
|
+
.returning(["nodeId", "leaseId", "acquiredAt", "expiresAt", "fencingToken", "endpoint"]);
|
|
211
|
+
|
|
137
212
|
var row;
|
|
138
213
|
if (dialect === "mysql") {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// reveals who currently holds the row — same as Postgres'
|
|
146
|
-
// RETURNING but as a separate statement.
|
|
147
|
-
var insertSql =
|
|
148
|
-
"INSERT INTO _blamejs_leader " +
|
|
149
|
-
" (scope, nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint) " +
|
|
150
|
-
"VALUES " +
|
|
151
|
-
" ('leader', ?, ?, ?, ?, 1, ?) " +
|
|
152
|
-
"ON DUPLICATE KEY UPDATE " +
|
|
153
|
-
" nodeId = IF(expiresAt < ?, VALUES(nodeId), nodeId)," +
|
|
154
|
-
" leaseId = IF(expiresAt < ?, VALUES(leaseId), leaseId)," +
|
|
155
|
-
" acquiredAt = IF(expiresAt < ?, VALUES(acquiredAt), acquiredAt)," +
|
|
156
|
-
" fencingToken = IF(expiresAt < ?, fencingToken + 1, fencingToken)," +
|
|
157
|
-
" endpoint = IF(expiresAt < ?, VALUES(endpoint), endpoint)," +
|
|
158
|
-
// expiresAt MUST be the last assignment — IF() evaluates each
|
|
159
|
-
// column against the row state BEFORE that column's update is
|
|
160
|
-
// applied, so checking expiresAt for the other columns first
|
|
161
|
-
// and overwriting it last keeps the predicate consistent.
|
|
162
|
-
" expiresAt = IF(expiresAt < ?, VALUES(expiresAt), expiresAt)";
|
|
163
|
-
await _q(insertSql, [
|
|
164
|
-
nodeId, leaseId, nowMs, expiresAt, endpoint,
|
|
165
|
-
nowMs, nowMs, nowMs, nowMs, nowMs, nowMs,
|
|
166
|
-
]);
|
|
167
|
-
var sel = await _q(
|
|
168
|
-
"SELECT nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint " +
|
|
169
|
-
"FROM _blamejs_leader WHERE scope = 'leader'"
|
|
170
|
-
);
|
|
214
|
+
var mBuilt = acquire.toSql();
|
|
215
|
+
await _q(clusterStorage().placeholderize(mBuilt.sql, dialect), mBuilt.params);
|
|
216
|
+
// b.sql's MySQL upsert returns the readback SELECT alongside (the
|
|
217
|
+
// RETURNING-equivalent); run it to learn who currently holds the row.
|
|
218
|
+
var rb = mBuilt.readbackSql;
|
|
219
|
+
var sel = await _q(clusterStorage().placeholderize(rb.sql, dialect), rb.params);
|
|
171
220
|
if (!sel.rows || sel.rows.length === 0) return null;
|
|
172
221
|
row = sel.rows[0];
|
|
173
222
|
} else {
|
|
174
|
-
|
|
175
|
-
var
|
|
176
|
-
"INSERT INTO _blamejs_leader " +
|
|
177
|
-
" (scope, nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint) " +
|
|
178
|
-
"VALUES " +
|
|
179
|
-
" ('leader', " + _placeholder(1) + ", " + _placeholder(2) + ", " +
|
|
180
|
-
" " + _placeholder(3) + ", " + _placeholder(4) + ", 1, " + _placeholder(5) + ") " +
|
|
181
|
-
"ON CONFLICT (scope) DO UPDATE SET " +
|
|
182
|
-
" nodeId = EXCLUDED.nodeId," +
|
|
183
|
-
" leaseId = EXCLUDED.leaseId," +
|
|
184
|
-
" acquiredAt = EXCLUDED.acquiredAt," +
|
|
185
|
-
" expiresAt = EXCLUDED.expiresAt," +
|
|
186
|
-
" fencingToken = _blamejs_leader.fencingToken + 1," +
|
|
187
|
-
" endpoint = EXCLUDED.endpoint " +
|
|
188
|
-
"WHERE _blamejs_leader.expiresAt < " + _placeholder(6) + " " +
|
|
189
|
-
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
190
|
-
var result = await _q(sql, [nodeId, leaseId, nowMs, expiresAt, endpoint, nowMs]);
|
|
223
|
+
acquire.onConflict(["scope"]);
|
|
224
|
+
var result = await _run(acquire);
|
|
191
225
|
if (!result.rows || result.rows.length === 0) return null;
|
|
192
226
|
row = result.rows[0];
|
|
193
227
|
}
|
|
@@ -219,24 +253,24 @@ function create(config) {
|
|
|
219
253
|
// returns either no row OR a row with a different leaseId, and
|
|
220
254
|
// we throw LEASE_LOST. Don't bump fencingToken on renewal — only
|
|
221
255
|
// on a fresh acquire.
|
|
256
|
+
var renewCols = ["nodeId", "leaseId", "acquiredAt", "expiresAt", "fencingToken", "endpoint"];
|
|
222
257
|
var row;
|
|
223
258
|
if (dialect === "mysql") {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
259
|
+
// MySQL has no RETURNING — UPDATE then read back, with a takeover
|
|
260
|
+
// detected when the read-back row no longer carries our leaseId.
|
|
261
|
+
var rvBuilt = sql().update(LEADER_TABLE, { dialect: dialect })
|
|
262
|
+
.set({ expiresAt: newExpiresAt, endpoint: endpoint })
|
|
263
|
+
.where("scope", "leader").where("nodeId", lease.nodeId).where("leaseId", lease.leaseId)
|
|
264
|
+
.toSql();
|
|
265
|
+
var rv = await _q(clusterStorage().placeholderize(rvBuilt.sql, dialect), rvBuilt.params);
|
|
230
266
|
var affected = rv && (rv.affectedRows || rv.rowCount || 0);
|
|
231
267
|
if (!affected) {
|
|
232
268
|
throw _err("LEASE_LOST",
|
|
233
269
|
"lease for node '" + lease.nodeId + "' was taken over (renewal rejected)",
|
|
234
270
|
false);
|
|
235
271
|
}
|
|
236
|
-
var sel = await
|
|
237
|
-
"
|
|
238
|
-
"FROM _blamejs_leader WHERE scope = 'leader'"
|
|
239
|
-
);
|
|
272
|
+
var sel = await _run(sql().select(LEADER_TABLE, { dialect: dialect })
|
|
273
|
+
.columns(renewCols).where("scope", "leader"));
|
|
240
274
|
if (!sel.rows || sel.rows.length === 0 ||
|
|
241
275
|
sel.rows[0].nodeId !== lease.nodeId ||
|
|
242
276
|
sel.rows[0].leaseId !== lease.leaseId) {
|
|
@@ -246,14 +280,10 @@ function create(config) {
|
|
|
246
280
|
}
|
|
247
281
|
row = sel.rows[0];
|
|
248
282
|
} else {
|
|
249
|
-
var sql
|
|
250
|
-
|
|
251
|
-
"
|
|
252
|
-
|
|
253
|
-
"WHERE scope = 'leader' AND nodeId = " + _placeholder(3) +
|
|
254
|
-
" AND leaseId = " + _placeholder(4) + " " +
|
|
255
|
-
"RETURNING nodeId, leaseId, acquiredAt, expiresAt, fencingToken, endpoint";
|
|
256
|
-
var result = await _q(sql, [newExpiresAt, endpoint, lease.nodeId, lease.leaseId]);
|
|
283
|
+
var result = await _run(sql().update(LEADER_TABLE, { dialect: dialect })
|
|
284
|
+
.set({ expiresAt: newExpiresAt, endpoint: endpoint })
|
|
285
|
+
.where("scope", "leader").where("nodeId", lease.nodeId).where("leaseId", lease.leaseId)
|
|
286
|
+
.returning(renewCols));
|
|
257
287
|
if (!result.rows || result.rows.length === 0) {
|
|
258
288
|
throw _err("LEASE_LOST",
|
|
259
289
|
"lease for node '" + lease.nodeId + "' was taken over (renewal rejected)",
|
|
@@ -276,19 +306,15 @@ function create(config) {
|
|
|
276
306
|
// Clear our row so the next acquire wins immediately. Match on
|
|
277
307
|
// leaseId so a takeover-then-release race doesn't clear someone
|
|
278
308
|
// else's lease.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"
|
|
282
|
-
"WHERE scope = 'leader' AND nodeId = " + _placeholder(1) +
|
|
283
|
-
" AND leaseId = " + _placeholder(2);
|
|
284
|
-
await _q(sql, [lease.nodeId, lease.leaseId]);
|
|
309
|
+
await _run(sql().update(LEADER_TABLE, { dialect: dialect })
|
|
310
|
+
.set({ expiresAt: 0 })
|
|
311
|
+
.where("scope", "leader").where("nodeId", lease.nodeId).where("leaseId", lease.leaseId));
|
|
285
312
|
}
|
|
286
313
|
|
|
287
314
|
async function currentLeader() {
|
|
288
|
-
var result = await
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
);
|
|
315
|
+
var result = await _run(sql().select(LEADER_TABLE, { dialect: dialect })
|
|
316
|
+
.columns(["nodeId", "expiresAt", "fencingToken", "endpoint"])
|
|
317
|
+
.where("scope", "leader"));
|
|
292
318
|
if (!result.rows || result.rows.length === 0) return null;
|
|
293
319
|
var row = result.rows[0];
|
|
294
320
|
if (Number(row.expiresAt) < Date.now()) return null;
|
package/lib/cluster-storage.js
CHANGED
|
@@ -74,6 +74,38 @@ function tableName(local) {
|
|
|
74
74
|
return local;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @primitive b.clusterStorage.dialect
|
|
79
|
+
* @signature b.clusterStorage.dialect()
|
|
80
|
+
* @since 0.15.0
|
|
81
|
+
* @status stable
|
|
82
|
+
* @related b.clusterStorage.execute, b.cluster.dialect, b.frameworkSchema.ensureSchema
|
|
83
|
+
*
|
|
84
|
+
* Resolve the SQL dialect every framework-table data-layer file must pass
|
|
85
|
+
* to `b.sql` so the emitted SQL matches the active backend. In cluster
|
|
86
|
+
* mode it returns the operator-configured backend dialect (`"postgres"` |
|
|
87
|
+
* `"mysql"` | `"sqlite"`, set at `b.cluster.init`); in single-node mode
|
|
88
|
+
* the framework state lives in local node:sqlite, so it returns
|
|
89
|
+
* `"sqlite"`. This is the canonical dialect source for framework-state
|
|
90
|
+
* SQL — `b.sql` defaults to `"sqlite"` when no dialect is passed, which is
|
|
91
|
+
* correct only on the single-node path and on Postgres by accident (both
|
|
92
|
+
* double-quote identifiers); on MySQL the default would emit double-quoted
|
|
93
|
+
* identifiers MySQL reads as string literals, so framework-table SQL must
|
|
94
|
+
* thread this value explicitly.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* var b = require("@blamejs/core");
|
|
98
|
+
* var dialect = b.clusterStorage.dialect();
|
|
99
|
+
* // → "sqlite" (single-node)
|
|
100
|
+
* // → "postgres" (cluster mode, postgres backend)
|
|
101
|
+
* // → "mysql" (cluster mode, mysql backend)
|
|
102
|
+
* var built = b.sql.select("_blamejs_cache", { dialect: dialect })
|
|
103
|
+
* .where("cacheKey", "k").toSql();
|
|
104
|
+
*/
|
|
105
|
+
function dialect() {
|
|
106
|
+
return cluster.isClusterMode() ? cluster.dialect() : "sqlite";
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
// Precomputed rewrite table for resolveTables(). Built once at module
|
|
78
110
|
// load from the static frozen frameworkSchema.LOCAL_TO_EXTERNAL mapping —
|
|
79
111
|
// not from operator/request input. Order longest-first so prefix matches
|
|
@@ -127,7 +159,19 @@ function _replaceWordBoundaryAll(haystack, needle, replacement) {
|
|
|
127
159
|
return out + haystack.slice(cursor);
|
|
128
160
|
}
|
|
129
161
|
|
|
130
|
-
|
|
162
|
+
// Rewrite table for resolveTables(), derived from the static frozen
|
|
163
|
+
// frameworkSchema.LOCAL_TO_EXTERNAL mapping — never from operator/request
|
|
164
|
+
// input. The external names are PREFIX-AWARE: resolved through
|
|
165
|
+
// frameworkSchema.tableName(local) so a configured framework-table prefix
|
|
166
|
+
// (set config-time via db.init({tablePrefix})) is honored in cluster-mode
|
|
167
|
+
// DML, matching the prefix the DDL builders created the tables under.
|
|
168
|
+
// Order longest-first so prefix matches don't collide (audit_log before
|
|
169
|
+
// audit). Entries that are identity under the CURRENT prefix are filtered
|
|
170
|
+
// so the loop body has no no-op iterations — under the default prefix this
|
|
171
|
+
// is byte-identical to the old local→external map (the already-prefixed
|
|
172
|
+
// `_blamejs_*` names map to themselves and drop out); under a custom prefix
|
|
173
|
+
// those same names rewrite to `<prefix>*` and stay in.
|
|
174
|
+
function _buildRewriteTable() {
|
|
131
175
|
var mapping = frameworkSchema.LOCAL_TO_EXTERNAL;
|
|
132
176
|
var names = Object.keys(mapping).sort(function (a, b) {
|
|
133
177
|
return b.length - a.length;
|
|
@@ -135,12 +179,28 @@ var _REWRITE_TABLE = (function () {
|
|
|
135
179
|
var entries = [];
|
|
136
180
|
for (var i = 0; i < names.length; i++) {
|
|
137
181
|
var local = names[i];
|
|
138
|
-
var external =
|
|
139
|
-
if (local === external) continue;
|
|
182
|
+
var external = frameworkSchema.tableName(local); // prefix-aware external name
|
|
183
|
+
if (local === external) continue; // no-op under the current prefix
|
|
140
184
|
entries.push({ local: local, external: external });
|
|
141
185
|
}
|
|
142
186
|
return Object.freeze(entries);
|
|
143
|
-
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
var _REWRITE_TABLE = null;
|
|
190
|
+
var _rewriteTablePrefix = null;
|
|
191
|
+
|
|
192
|
+
// The rewrite table for the current framework-table prefix, rebuilt if the
|
|
193
|
+
// prefix changed since the last build. db.init({tablePrefix}) sets the prefix
|
|
194
|
+
// once at boot (config-time), so the rebuild fires at most once; the prefix
|
|
195
|
+
// read is a cheap module-var getter, not request input.
|
|
196
|
+
function _rewriteTable() {
|
|
197
|
+
var prefix = frameworkSchema.getTablePrefix();
|
|
198
|
+
if (_REWRITE_TABLE === null || prefix !== _rewriteTablePrefix) {
|
|
199
|
+
_REWRITE_TABLE = _buildRewriteTable();
|
|
200
|
+
_rewriteTablePrefix = prefix;
|
|
201
|
+
}
|
|
202
|
+
return _REWRITE_TABLE;
|
|
203
|
+
}
|
|
144
204
|
|
|
145
205
|
// Rewrite bare table names in SQL when running in cluster mode. We only
|
|
146
206
|
// touch tokens that are exactly one of the framework's known table names
|
|
@@ -173,9 +233,10 @@ var _REWRITE_TABLE = (function () {
|
|
|
173
233
|
*/
|
|
174
234
|
function resolveTables(sql) {
|
|
175
235
|
if (!cluster.isClusterMode()) return sql;
|
|
236
|
+
var rewrite = _rewriteTable();
|
|
176
237
|
var translated = sql;
|
|
177
|
-
for (var i = 0; i <
|
|
178
|
-
var entry =
|
|
238
|
+
for (var i = 0; i < rewrite.length; i++) {
|
|
239
|
+
var entry = rewrite[i];
|
|
179
240
|
translated = _replaceWordBoundaryAll(translated, entry.local, entry.external);
|
|
180
241
|
}
|
|
181
242
|
return translated;
|
|
@@ -196,12 +257,16 @@ function resolveTables(sql) {
|
|
|
196
257
|
* @related b.clusterStorage.execute, b.cluster.dialect
|
|
197
258
|
*
|
|
198
259
|
* Translate `?` placeholders to numbered `$1`, `$2`, … form for
|
|
199
|
-
* Postgres backends; passthrough for `"sqlite"` and `"mysql"`. The
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
260
|
+
* Postgres backends; passthrough for `"sqlite"` and `"mysql"`. The walker
|
|
261
|
+
* skips a `?` inside a single-quoted string literal (`WHERE s = '?'`), a
|
|
262
|
+
* double-quoted or backtick-quoted identifier (`"c?l"`), and a `--` or
|
|
263
|
+
* block comment — so only a true bind marker is renumbered. This skip set
|
|
264
|
+
* is a SUPERSET of `b.safeSql.countPlaceholders`'s, so the count used to
|
|
265
|
+
* size params and the renumbering done here can never diverge (a `?` one
|
|
266
|
+
* scanner counts but the other rewrites would mis-align bound values).
|
|
267
|
+
* Doubled-quote escapes (`''` / `""`) inside their span are recognized.
|
|
268
|
+
* The `execute` family calls this on every cluster-mode dispatch; reach
|
|
269
|
+
* for it directly only when shipping raw SQL through a non-`execute` path.
|
|
205
270
|
*
|
|
206
271
|
* @example
|
|
207
272
|
* var b = require("@blamejs/core");
|
|
@@ -213,21 +278,41 @@ function resolveTables(sql) {
|
|
|
213
278
|
*/
|
|
214
279
|
function placeholderize(sql, dialect) {
|
|
215
280
|
if (dialect !== "postgres") return sql;
|
|
216
|
-
// Walk the SQL and replace `?` with $1, $2, … but skip ones inside
|
|
217
|
-
// single-quoted string literals.
|
|
218
281
|
var out = "";
|
|
219
282
|
var n = 0;
|
|
220
|
-
var
|
|
221
|
-
|
|
283
|
+
var i = 0;
|
|
284
|
+
var len = sql.length;
|
|
285
|
+
while (i < len) {
|
|
222
286
|
var c = sql.charAt(i);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
287
|
+
var nx = i + 1 < len ? sql.charAt(i + 1) : "";
|
|
288
|
+
// Quote contexts: ' string literal, " identifier, ` mysql identifier.
|
|
289
|
+
// A doubled quote escapes itself within the span.
|
|
290
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
291
|
+
out += c;
|
|
292
|
+
i += 1;
|
|
293
|
+
while (i < len) {
|
|
294
|
+
var q = sql.charAt(i);
|
|
295
|
+
if (q === c) {
|
|
296
|
+
if (sql.charAt(i + 1) === c) { out += c + c; i += 2; continue; }
|
|
297
|
+
out += c; i += 1; break;
|
|
298
|
+
}
|
|
299
|
+
out += q; i += 1;
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (c === "-" && nx === "-") { // line comment
|
|
304
|
+
while (i < len && sql.charAt(i) !== "\n") { out += sql.charAt(i); i += 1; }
|
|
305
|
+
continue;
|
|
228
306
|
}
|
|
229
|
-
if (
|
|
307
|
+
if (c === "/" && nx === "*") { // block comment
|
|
308
|
+
out += "/*"; i += 2;
|
|
309
|
+
while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) { out += sql.charAt(i); i += 1; }
|
|
310
|
+
if (i < len) { out += "*/"; i += 2; }
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (c === "?") { n += 1; out += "$" + n; i += 1; continue; }
|
|
230
314
|
out += c;
|
|
315
|
+
i += 1;
|
|
231
316
|
}
|
|
232
317
|
return out;
|
|
233
318
|
}
|
|
@@ -299,6 +384,17 @@ async function execute(sql, params) {
|
|
|
299
384
|
var result = await externalDb.query(translated, params, {
|
|
300
385
|
backend: cluster.externalDbBackend(),
|
|
301
386
|
});
|
|
387
|
+
// Coerce backend-native types back to the framework's canonical JS shape
|
|
388
|
+
// for every clusterStorage reader at once: node-postgres returns BIGINT /
|
|
389
|
+
// int8 as a decimal string and BYTEA as a Buffer, so a framework column
|
|
390
|
+
// read back from Postgres would otherwise be the wrong JS type (a counter
|
|
391
|
+
// compared as a string, a hash/nonce mis-typed). coerceRows only touches
|
|
392
|
+
// columns in the framework's type map; operator columns pass through, and
|
|
393
|
+
// it is idempotent (already-correct types are left alone), so a reader
|
|
394
|
+
// that also coerces locally is unaffected.
|
|
395
|
+
if (result && Array.isArray(result.rows) && result.rows.length > 0) {
|
|
396
|
+
result.rows = frameworkSchema.coerceRows(result.rows);
|
|
397
|
+
}
|
|
302
398
|
return result;
|
|
303
399
|
}
|
|
304
400
|
|
|
@@ -449,6 +545,7 @@ module.exports = {
|
|
|
449
545
|
executeAll: executeAll,
|
|
450
546
|
transaction: transaction,
|
|
451
547
|
tableName: tableName,
|
|
548
|
+
dialect: dialect,
|
|
452
549
|
resolveTables: resolveTables,
|
|
453
550
|
placeholderize: placeholderize,
|
|
454
551
|
ClusterStorageError: ClusterStorageError,
|