@blamejs/core 0.14.27 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/index.js +4 -0
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +158 -77
- package/lib/atomic-file.js +29 -1
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +228 -100
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +22 -0
- package/lib/consent.js +82 -29
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +387 -91
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +517 -256
- package/lib/db-schema.js +209 -44
- package/lib/db.js +202 -95
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/framework-error.js +11 -0
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +596 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +14 -0
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +293 -154
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +31 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/outbox.js +136 -82
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/retention.js +82 -39
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +116 -72
- package/lib/sql.js +3885 -0
- package/lib/static.js +45 -7
- package/lib/subject.js +89 -49
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +16 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/db-schema.js
CHANGED
|
@@ -37,10 +37,18 @@
|
|
|
37
37
|
* swallowing the original error.
|
|
38
38
|
*/
|
|
39
39
|
var nodePath = require("node:path");
|
|
40
|
+
var lazyRequire = require("./lazy-require");
|
|
40
41
|
var atomicFile = require("./atomic-file");
|
|
42
|
+
var frameworkSchema = require("./framework-schema");
|
|
41
43
|
var safeSql = require("./safe-sql");
|
|
44
|
+
var sql = require("./sql");
|
|
42
45
|
var observability = require("./observability");
|
|
43
46
|
|
|
47
|
+
// Lazy to break the db-schema -> compliance -> (audit/db) load chain.
|
|
48
|
+
// resolveDriftMode reads the globally-pinned posture so a regulated
|
|
49
|
+
// deployment refuses to boot under undeclared schema drift by default.
|
|
50
|
+
var compliance = lazyRequire(function () { return require("./compliance"); });
|
|
51
|
+
|
|
44
52
|
// SQLite raw-SQL helper. node:sqlite DatabaseSync exposes a method on the
|
|
45
53
|
// database object that runs raw SQL without bind parameters — used for DDL,
|
|
46
54
|
// BEGIN/COMMIT/ROLLBACK, and PRAGMA. Bracket notation here avoids a
|
|
@@ -92,20 +100,22 @@ function runInTransaction(db, fn, opts) {
|
|
|
92
100
|
|
|
93
101
|
// ---- Internal migrations table ----
|
|
94
102
|
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
103
|
+
// Logical name; the physical name + configured prefix resolve through
|
|
104
|
+
// frameworkSchema.tableName, and every statement composes b.sql
|
|
105
|
+
// (quoteName: true) so the resolved name is quoted by construction.
|
|
106
|
+
var MIGRATIONS_TABLE = "_blamejs_migrations"; // allow:hand-rolled-sql — logical name declaration; physical name + prefix resolve via frameworkSchema.tableName
|
|
107
|
+
function _migrationsTable() { return frameworkSchema.tableName(MIGRATIONS_TABLE); }
|
|
108
|
+
// b.sql opts for the local single-node sqlite handle this module's helpers
|
|
109
|
+
// run against (database.exec / database.prepare, never clusterStorage):
|
|
110
|
+
// "sqlite" dialect + quoteName so the resolved framework name quotes.
|
|
111
|
+
var _SQL_OPTS = { dialect: "sqlite", quoteName: true };
|
|
100
112
|
|
|
101
113
|
function ensureMigrationsTable(database) {
|
|
102
|
-
runSql(database,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
")"
|
|
108
|
-
);
|
|
114
|
+
runSql(database, sql.createTable(_migrationsTable(), [
|
|
115
|
+
{ name: "name", type: "text", primaryKey: true },
|
|
116
|
+
{ name: "description", type: "text" },
|
|
117
|
+
{ name: "appliedAt", type: "text", notNull: true },
|
|
118
|
+
], _SQL_OPTS).sql);
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
// ---- Declarative reconcile ----
|
|
@@ -114,7 +124,7 @@ function ensureMigrationsTable(database) {
|
|
|
114
124
|
// additive ALTER TABLE ADD COLUMN + CREATE INDEX IF NOT EXISTS for every
|
|
115
125
|
// table in `schema`. Never drops columns or tables (data-loss safety).
|
|
116
126
|
//
|
|
117
|
-
// `opts.onDrift`
|
|
127
|
+
// `opts.onDrift` controls detection of config-vs-live divergence — a
|
|
118
128
|
// compliance-evidence concern: the live DB should match the declared data
|
|
119
129
|
// model so an auditor can trust the schema config as ground truth (the
|
|
120
130
|
// change-/configuration-management control families in ISO 27001:2022
|
|
@@ -131,14 +141,24 @@ function ensureMigrationsTable(database) {
|
|
|
131
141
|
// contract; this is detection + an operator-chosen reaction only.
|
|
132
142
|
//
|
|
133
143
|
// onDrift values (config-time enum; bad value throws):
|
|
134
|
-
// "ignore"
|
|
135
|
-
//
|
|
136
|
-
// with drift are not broken.
|
|
144
|
+
// "ignore" — no detection side effects. Existing deployments with
|
|
145
|
+
// benign drift are not broken.
|
|
137
146
|
// "warn" — detect + emit a "db.schema.drift" observability event per
|
|
138
147
|
// drifted table; never throws.
|
|
139
148
|
// "refuse" — detect + THROW on the first drifted table, so a strict-
|
|
140
|
-
// schema posture refuses to boot under divergence.
|
|
141
|
-
//
|
|
149
|
+
// schema posture refuses to boot under divergence.
|
|
150
|
+
//
|
|
151
|
+
// Default (v0.15.0): "ignore" on an unpinned / non-regulated deployment
|
|
152
|
+
// (back-compat); "refuse" when a regulated compliance posture is
|
|
153
|
+
// globally pinned (b.compliance.set) and the operator did not pass an
|
|
154
|
+
// explicit onDrift. The live DB diverging from the declared data model
|
|
155
|
+
// is a change-/configuration-management finding the auditor reads as
|
|
156
|
+
// ground truth (ISO 27001:2022 A.8.9 + SOC 2 CC8.1 turn on "the running
|
|
157
|
+
// system equals the approved definition"); under a regulated posture
|
|
158
|
+
// the safe default is to refuse boot rather than silently serve a
|
|
159
|
+
// schema no one approved. Operators who knowingly run with drift under
|
|
160
|
+
// a regulated posture opt back to the prior behaviour with an explicit
|
|
161
|
+
// onDrift: "ignore" (or "warn" to keep the signal without the throw).
|
|
142
162
|
//
|
|
143
163
|
// Returns a { tables: [...], drifted: boolean } report.
|
|
144
164
|
function reconcile(database, schema, opts) {
|
|
@@ -165,6 +185,13 @@ function reconcileTable(database, table, opts) {
|
|
|
165
185
|
throw new Error("schema entry '" + table.name + "' missing 'columns' object");
|
|
166
186
|
}
|
|
167
187
|
var driftMode = resolveDriftMode(opts);
|
|
188
|
+
// Identifier quoting follows the handle's dialect (double-quote on
|
|
189
|
+
// sqlite/postgres, backtick on mysql) so the reconciler's CREATE / ALTER
|
|
190
|
+
// / FK DDL is portable. Reserved-word column names stay safe by being
|
|
191
|
+
// quoted; the operator's verbatim TYPE strings are emitted unchanged in
|
|
192
|
+
// type position (after a quoted identifier), never in identifier position.
|
|
193
|
+
var dialect = _handleDialect(database);
|
|
194
|
+
function q(ident) { return safeSql.quoteIdentifier(ident, dialect, { allowReserved: true }); }
|
|
168
195
|
|
|
169
196
|
var name = table.name;
|
|
170
197
|
validateIdent(name, "table name");
|
|
@@ -172,7 +199,7 @@ function reconcileTable(database, table, opts) {
|
|
|
172
199
|
var colDefs = [];
|
|
173
200
|
for (var col in table.columns) {
|
|
174
201
|
validateIdent(col, "column name");
|
|
175
|
-
colDefs.push(
|
|
202
|
+
colDefs.push(q(col) + " " + table.columns[col]);
|
|
176
203
|
}
|
|
177
204
|
if (colDefs.length === 0) {
|
|
178
205
|
throw new Error("schema entry '" + name + "' has no columns");
|
|
@@ -188,7 +215,7 @@ function reconcileTable(database, table, opts) {
|
|
|
188
215
|
throw new Error("primaryKey '" + c + "' is not declared in columns of table '" + name + "'");
|
|
189
216
|
}
|
|
190
217
|
});
|
|
191
|
-
colDefs.push("PRIMARY KEY (" + pkCols.map(function (c) { return
|
|
218
|
+
colDefs.push("PRIMARY KEY (" + pkCols.map(function (c) { return q(c); }).join(", ") + ")");
|
|
192
219
|
}
|
|
193
220
|
|
|
194
221
|
// Structured FOREIGN KEY declarations. Each entry:
|
|
@@ -217,21 +244,31 @@ function reconcileTable(database, table, opts) {
|
|
|
217
244
|
if (localCols.length !== refCols.length) {
|
|
218
245
|
throw new Error("foreignKey on '" + name + "': local-column count must match referenced-column count");
|
|
219
246
|
}
|
|
220
|
-
var clause = "FOREIGN KEY (" + localCols.map(function (c) { return
|
|
221
|
-
|
|
247
|
+
var clause = "FOREIGN KEY (" + localCols.map(function (c) { return q(c); }).join(", ") + ")" +
|
|
248
|
+
" REFERENCES " + q(refTable) + " (" + refCols.map(function (c) { return q(c); }).join(", ") + ")";
|
|
222
249
|
if (fk.onDelete) clause += " ON DELETE " + _validateAction(fk.onDelete, "ON DELETE", name);
|
|
223
250
|
if (fk.onUpdate) clause += " ON UPDATE " + _validateAction(fk.onUpdate, "ON UPDATE", name);
|
|
224
251
|
colDefs.push(clause);
|
|
225
252
|
}
|
|
226
253
|
}
|
|
227
254
|
|
|
228
|
-
|
|
255
|
+
// Operator-schema reconcile: colDefs carries the operator's VERBATIM
|
|
256
|
+
// per-column DDL strings (e.g. "TEXT PRIMARY KEY", "INTEGER NOT NULL
|
|
257
|
+
// DEFAULT 0") plus composite FOREIGN KEY clauses with referential
|
|
258
|
+
// actions — a grammar b.sql.createTable's structured { name, type,
|
|
259
|
+
// notNull, references } column specs cannot faithfully reproduce
|
|
260
|
+
// (no table-level composite-FK or arbitrary-inline-constraint slot).
|
|
261
|
+
// Every identifier here is validated (validateIdent) + quoted by
|
|
262
|
+
// construction, so quote-by-construction safety is preserved.
|
|
263
|
+
// allow:hand-rolled-sql — operator verbatim column DDL + composite FK clauses outside b.sql.createTable's structured API
|
|
264
|
+
runSql(database, "CREATE TABLE IF NOT EXISTS " + q(name) + " (" + colDefs.join(", ") + ")");
|
|
229
265
|
|
|
230
266
|
var existingCols = listColumns(database, name);
|
|
231
267
|
for (var newCol in table.columns) {
|
|
232
268
|
if (!existingCols.has(newCol)) {
|
|
233
269
|
try {
|
|
234
|
-
|
|
270
|
+
// allow:hand-rolled-sql — operator verbatim ADD COLUMN DDL (validated + quoted identifier); type string is operator-controlled
|
|
271
|
+
runSql(database, "ALTER TABLE " + q(name) + " ADD COLUMN " + q(newCol) + " " + table.columns[newCol]);
|
|
235
272
|
} catch (e) {
|
|
236
273
|
throw new Error("failed to add column '" + newCol + "' to '" + name + "': " + e.message);
|
|
237
274
|
}
|
|
@@ -244,9 +281,10 @@ function reconcileTable(database, table, opts) {
|
|
|
244
281
|
}
|
|
245
282
|
}
|
|
246
283
|
|
|
247
|
-
// Schema-drift detection
|
|
248
|
-
//
|
|
249
|
-
//
|
|
284
|
+
// Schema-drift detection. Default mode is posture-driven: "refuse"
|
|
285
|
+
// under a regulated pinned posture, "ignore" otherwise (resolveDriftMode).
|
|
286
|
+
// Compares the live table's columns against the declared model AFTER the
|
|
287
|
+
// additive ADD COLUMN pass so the diff reflects what reconcile could not fix:
|
|
250
288
|
// - extra = live-but-undeclared (out-of-band ALTER / hand-edit);
|
|
251
289
|
// - missing = declared-but-still-absent (ADD COLUMN could not apply).
|
|
252
290
|
// Dropped columns are never acted on — reconcile stays non-destructive.
|
|
@@ -310,17 +348,51 @@ function _validateAction(action, label, tableName) {
|
|
|
310
348
|
return up;
|
|
311
349
|
}
|
|
312
350
|
|
|
313
|
-
// onDrift reaction modes. "ignore"
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
351
|
+
// onDrift reaction modes. "ignore" takes no action on detected drift;
|
|
352
|
+
// "warn" emits an observability signal and reports; "refuse" throws so a
|
|
353
|
+
// strict-schema posture refuses to boot when the live DB has diverged
|
|
354
|
+
// from the declared model.
|
|
317
355
|
var DRIFT_MODES = ["ignore", "warn", "refuse"];
|
|
318
356
|
|
|
319
|
-
//
|
|
320
|
-
// (
|
|
321
|
-
//
|
|
357
|
+
// Compliance postures under which schema conformance is an audit-evidence
|
|
358
|
+
// floor (change-/configuration-management control families: ISO 27001:2022
|
|
359
|
+
// A.8.9, SOC 2 CC8.1). When one of these is the globally-pinned posture
|
|
360
|
+
// and the operator left onDrift unset, the default flips from "ignore" to
|
|
361
|
+
// "refuse" so an unapproved live schema fails boot rather than serving
|
|
362
|
+
// silently. Membership match is exact against compliance().current().
|
|
363
|
+
var REGULATED_DRIFT_REFUSE = Object.freeze({
|
|
364
|
+
"hipaa": true, "pci-dss": true, "gdpr": true, "soc2": true,
|
|
365
|
+
"iso-27001-2022": true, "dora": true, "fedramp-rev5-moderate": true,
|
|
366
|
+
"nist-800-53": true, "nist-800-53-r5-privacy": true, "dpdp": true,
|
|
367
|
+
"lgpd-br": true, "pipl-cn": true, "uk-gdpr": true,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// _pinnedRegulatedDrift — the posture-driven default when onDrift is unset.
|
|
371
|
+
// Returns "refuse" when a regulated posture is globally pinned, "ignore"
|
|
372
|
+
// otherwise. Drop-safe: any failure resolving the posture (compliance not
|
|
373
|
+
// loaded, no posture pinned) yields the back-compat "ignore" — the gate
|
|
374
|
+
// only tightens the default when a regulated posture is provably pinned,
|
|
375
|
+
// never the reverse.
|
|
376
|
+
function _pinnedRegulatedDrift() {
|
|
377
|
+
try {
|
|
378
|
+
var pinned = compliance().current();
|
|
379
|
+
if (typeof pinned === "string" && REGULATED_DRIFT_REFUSE[pinned] === true) {
|
|
380
|
+
return "refuse";
|
|
381
|
+
}
|
|
382
|
+
} catch (_e) { /* compliance unavailable — fall through to back-compat */ }
|
|
383
|
+
return "ignore";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// resolveDriftMode — config-time enum validation. Unset => the
|
|
387
|
+
// posture-driven default ("refuse" under a regulated pinned posture,
|
|
388
|
+
// "ignore" otherwise; see REGULATED_DRIFT_REFUSE). An explicit value
|
|
389
|
+
// always wins, including "ignore" to opt back out under a regulated
|
|
390
|
+
// posture. A bad value is an operator typo at config time => THROW
|
|
391
|
+
// (entry-point tier).
|
|
322
392
|
function resolveDriftMode(opts) {
|
|
323
|
-
if (!opts || opts.onDrift === undefined || opts.onDrift === null)
|
|
393
|
+
if (!opts || opts.onDrift === undefined || opts.onDrift === null) {
|
|
394
|
+
return _pinnedRegulatedDrift();
|
|
395
|
+
}
|
|
324
396
|
var mode = opts.onDrift;
|
|
325
397
|
if (typeof mode !== "string" || DRIFT_MODES.indexOf(mode) === -1) {
|
|
326
398
|
throw new TypeError(
|
|
@@ -345,17 +417,99 @@ function reconcileIndex(database, tableName, idx) {
|
|
|
345
417
|
}
|
|
346
418
|
validateIdent(indexName, "index name");
|
|
347
419
|
cols.forEach(function (c) { validateIdent(c, "indexed column"); });
|
|
348
|
-
var
|
|
420
|
+
var dialect = _handleDialect(database);
|
|
421
|
+
function q(ident) { return safeSql.quoteIdentifier(ident, dialect, { allowReserved: true }); }
|
|
422
|
+
var quotedCols = cols.map(function (c) { return q(c); }).join(", ");
|
|
423
|
+
// MySQL has no CREATE INDEX IF NOT EXISTS; a re-run of a declared index
|
|
424
|
+
// would error "Duplicate key name". The reconciler is idempotent by
|
|
425
|
+
// contract, so on MySQL a duplicate-index error is swallowed (the index
|
|
426
|
+
// already exists, which is the desired end state). Postgres + SQLite use
|
|
427
|
+
// the IF NOT EXISTS form natively.
|
|
428
|
+
if (dialect === "mysql") {
|
|
429
|
+
try {
|
|
430
|
+
runSql(database,
|
|
431
|
+
"CREATE " + (unique ? "UNIQUE " : "") + "INDEX " + q(indexName) +
|
|
432
|
+
" ON " + q(tableName) + " (" + quotedCols + ")");
|
|
433
|
+
} catch (e) {
|
|
434
|
+
if (!/exist|duplicate/i.test((e && e.message) || "")) throw e;
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
349
438
|
runSql(database,
|
|
350
|
-
"CREATE " + (unique ? "UNIQUE " : "") + "INDEX IF NOT EXISTS
|
|
351
|
-
|
|
439
|
+
"CREATE " + (unique ? "UNIQUE " : "") + "INDEX IF NOT EXISTS " + q(indexName) +
|
|
440
|
+
" ON " + q(tableName) + " (" + quotedCols + ")"
|
|
352
441
|
);
|
|
353
442
|
}
|
|
354
443
|
|
|
444
|
+
// The dialect of a data-layer handle. db.init / db.from drive the
|
|
445
|
+
// framework's local node:sqlite handle (the default). An operator who
|
|
446
|
+
// reconciles / migrates / seeds their OWN Postgres / MySQL handle declares
|
|
447
|
+
// the dialect on the handle via `handle.dialect` so the SQL matches the
|
|
448
|
+
// backend. Absent / unknown falls back to "sqlite" — every existing
|
|
449
|
+
// local-handle caller is byte-identical. Shared by db-schema's reconciler,
|
|
450
|
+
// migrations.js, and seeders.js (the three sync data-layer files that drive
|
|
451
|
+
// a handle directly), so the resolution lives in one place.
|
|
452
|
+
function handleDialect(database) {
|
|
453
|
+
var d = database && database.dialect;
|
|
454
|
+
if (d === "postgres" || d === "mysql" || d === "sqlite") return d;
|
|
455
|
+
return "sqlite";
|
|
456
|
+
}
|
|
457
|
+
// Back-compat internal alias used throughout this module.
|
|
458
|
+
var _handleDialect = handleDialect;
|
|
459
|
+
|
|
460
|
+
// b.sql opts for a statement run directly against `database` (db.prepare /
|
|
461
|
+
// runSqlOnHandle, never clusterStorage): the handle's dialect + quoteName so
|
|
462
|
+
// the resolved framework table name quotes by construction.
|
|
463
|
+
function sqlOpts(database) {
|
|
464
|
+
return { dialect: handleDialect(database), quoteName: true };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// A registry/lock PRIMARY-KEY (or composite-PK / indexed) TEXT column type.
|
|
468
|
+
// MySQL refuses an unbounded TEXT/BLOB in a key without a prefix length, so
|
|
469
|
+
// a key-participating text column is VARCHAR(191) there (utf8mb4
|
|
470
|
+
// index-safe); Postgres + SQLite index TEXT directly. The value is emitted
|
|
471
|
+
// verbatim by b.sql in type position (after a quoted identifier), never as
|
|
472
|
+
// an identifier.
|
|
473
|
+
function keyTextType(database) {
|
|
474
|
+
return handleDialect(database) === "mysql" ? "VARCHAR(191)" : "text";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// List the live column names of a table. SQLite reads `PRAGMA table_info`;
|
|
478
|
+
// Postgres + MySQL read information_schema.columns (PRAGMA is SQLite-only —
|
|
479
|
+
// it throws "syntax error at PRAGMA" on the others). The table name binds
|
|
480
|
+
// as a `?` parameter (never concatenated into the SQL text), so an operator
|
|
481
|
+
// table name with metacharacters can't break the introspection query. On
|
|
482
|
+
// Postgres the unqualified information_schema query matches the table in
|
|
483
|
+
// any schema on the search_path; an operator running the same table name in
|
|
484
|
+
// multiple schemas qualifies via the `schema.table` handle convention
|
|
485
|
+
// elsewhere — listColumns reconciles by bare name here, matching the
|
|
486
|
+
// reconciler's CREATE TABLE (which is also bare-named).
|
|
355
487
|
function listColumns(database, tableName) {
|
|
356
|
-
var
|
|
488
|
+
var dialect = _handleDialect(database);
|
|
357
489
|
var set = new Set();
|
|
358
|
-
|
|
490
|
+
if (dialect === "sqlite") {
|
|
491
|
+
var rows = database.prepare('PRAGMA table_info("' + tableName + '")').all();
|
|
492
|
+
for (var i = 0; i < rows.length; i++) set.add(rows[i].name);
|
|
493
|
+
return set;
|
|
494
|
+
}
|
|
495
|
+
// Postgres + MySQL: information_schema.columns is SQL-standard on both.
|
|
496
|
+
// The column-name column is `column_name` on both; the table name binds.
|
|
497
|
+
// A fixed catalog-introspection SELECT against the SQL-standard
|
|
498
|
+
// information_schema.columns view (a schema-qualified system table b.sql's
|
|
499
|
+
// verb builders don't model); the ONLY value (table name) binds as a `?`,
|
|
500
|
+
// every column/table reference is a static literal — no injection surface.
|
|
501
|
+
// allow:hand-rolled-sql — static information_schema introspection, single bound param
|
|
502
|
+
var infoSql = "SELECT column_name FROM information_schema.columns " +
|
|
503
|
+
"WHERE table_name = ?";
|
|
504
|
+
var stmt = database.prepare(infoSql);
|
|
505
|
+
var irows = stmt.all.apply(stmt, [tableName]);
|
|
506
|
+
for (var j = 0; j < irows.length; j++) {
|
|
507
|
+
// node-postgres folds unquoted output column names to lowercase, so the
|
|
508
|
+
// result key is `column_name` on every driver; read it directly.
|
|
509
|
+
var name = irows[j].column_name;
|
|
510
|
+
if (name === undefined) name = irows[j].COLUMN_NAME; // some MySQL drivers upper-case
|
|
511
|
+
if (name !== undefined && name !== null) set.add(name);
|
|
512
|
+
}
|
|
359
513
|
return set;
|
|
360
514
|
}
|
|
361
515
|
|
|
@@ -384,7 +538,9 @@ function runMigrations(database, migrationDir) {
|
|
|
384
538
|
}).map(function (e) { return e.name; }).sort();
|
|
385
539
|
|
|
386
540
|
var appliedSet = new Set();
|
|
387
|
-
|
|
541
|
+
var namesQ = sql.select(_migrationsTable(), _SQL_OPTS).columns(["name"]).toSql();
|
|
542
|
+
var namesStmt = database.prepare(namesQ.sql);
|
|
543
|
+
namesStmt.all.apply(namesStmt, namesQ.params).forEach(function (r) {
|
|
388
544
|
appliedSet.add(r.name);
|
|
389
545
|
});
|
|
390
546
|
|
|
@@ -413,9 +569,11 @@ function runMigrations(database, migrationDir) {
|
|
|
413
569
|
try {
|
|
414
570
|
runInTransaction(database, function () {
|
|
415
571
|
mig.up(database);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
572
|
+
var insQ = sql.insert(_migrationsTable(), _SQL_OPTS)
|
|
573
|
+
.values({ name: file, description: mig.description || "",
|
|
574
|
+
appliedAt: new Date().toISOString() }).toSql();
|
|
575
|
+
var insStmt = database.prepare(insQ.sql);
|
|
576
|
+
insStmt.run.apply(insStmt, insQ.params);
|
|
419
577
|
});
|
|
420
578
|
} catch (e) {
|
|
421
579
|
throw new Error("migration '" + file + "' failed: " + e.message);
|
|
@@ -434,6 +592,13 @@ module.exports = {
|
|
|
434
592
|
runSql: runSql,
|
|
435
593
|
runSqlOnHandle: runSqlOnHandle,
|
|
436
594
|
runInTransaction: runInTransaction,
|
|
595
|
+
// Shared data-layer dialect resolution — composed by migrations.js +
|
|
596
|
+
// seeders.js so the handle-dialect / b.sql-opts / key-text-type logic
|
|
597
|
+
// lives in exactly one place.
|
|
598
|
+
handleDialect: handleDialect,
|
|
599
|
+
sqlOpts: sqlOpts,
|
|
600
|
+
keyTextType: keyTextType,
|
|
601
|
+
listColumns: listColumns,
|
|
437
602
|
MIGRATIONS_TABLE: MIGRATIONS_TABLE,
|
|
438
603
|
DRIFT_MODES: DRIFT_MODES,
|
|
439
604
|
};
|