@voltro/cli 0.31.0 → 0.33.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 +368 -0
- package/THIRD-PARTY-NOTICES.md +1 -29
- package/dist/apiBuild-C-x9YacA.js +2 -0
- package/dist/{apiBuild-CLPt-pX6.js → apiBuild-h9VHtnlw.js} +2 -2
- package/dist/bin.js +3 -3
- package/dist/checkCommand-DRovTKza.js +2 -0
- package/dist/{checkCommand-D7r6_pk7.js → checkCommand-xGhRFFg2.js} +330 -290
- package/dist/{commands-CNVVv03h.js → commands-CJfepbm4.js} +139 -13
- package/dist/{dbCommand-BH8GAYuf.js → dbCommand-DTLKAfbA.js} +431 -263
- package/dist/dbCommand-b1gum4td.js +2 -0
- package/dist/{dev-DdQEn2Ia.js → dev-C_P8FLSx.js} +2141 -2081
- package/dist/{dev-DOZVMVfi.js → dev-iiMtlkfs.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-BeEAnjqw.js → inspectMetrics-1xzTKAFx.js} +750 -685
- package/dist/{sdkgen-v9WV8zRi.js → sdkgen-C81QIkiL.js} +1 -1
- package/dist/serveCommand-DdaM4Hup.js +1608 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-CI2QdKy9.js → start-C0koT0UO.js} +1 -1
- package/dist/startEntry.js +2 -2
- package/package.json +431 -18
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/database/misc.md +37 -0
- package/templates/agent-docs/database/schema.md +4 -2
- package/templates/agent-docs/deployment.md +87 -1
- package/templates/agent-docs/plugins/audit.md +24 -0
- package/templates/agent-docs/plugins.md +50 -0
- package/templates/agent-docs/scheduling.md +4 -1
- package/templates/agent-docs/security.md +31 -1
- package/templates/agent-docs/whats-new.md +149 -59
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +2 -2
- package/dist/apiBuild-BOqL1dlg.js +0 -2
- package/dist/checkCommand-ClRvH_SN.js +0 -2
- package/dist/dbCommand-BwQ_Jl98.js +0 -2
- package/dist/serveCommand-KsKXXWn4.js +0 -1578
|
@@ -8,19 +8,19 @@ import { pathToFileURL as h } from "node:url";
|
|
|
8
8
|
import { Effect as g } from "effect";
|
|
9
9
|
import { createLogger as _ } from "@voltro/logger";
|
|
10
10
|
import { promises as v, writeSync as y } from "node:fs";
|
|
11
|
-
import { InMemoryDataStore as ee } from "@voltro/runtime";
|
|
12
|
-
import { SqlClient as
|
|
13
|
-
import { actorsTable as te, frameworkTables as
|
|
14
|
-
import { createHash as
|
|
15
|
-
import { DEFAULT_CDC_CHANNEL as
|
|
11
|
+
import { InMemoryDataStore as b, makeFieldCipher as ee } from "@voltro/runtime";
|
|
12
|
+
import { SqlClient as x } from "@effect/sql";
|
|
13
|
+
import { actorsTable as te, frameworkTables as S, isEncrypted as C, isMigrationDefinition as ne, quoteIdent as re, registerCoreTables as ie } from "@voltro/database";
|
|
14
|
+
import { createHash as ae } from "node:crypto";
|
|
15
|
+
import { DEFAULT_CDC_CHANNEL as oe, applyPlan as se, applySchema as w, assessRollingDeployGate as ce, declaredSnapshot as le, destructiveScope as ue, detectReactiveTriggerDrift as de, fingerprintSchema as fe, ignoreTablesFromEnv as T, introspectSchema as pe, pendingFileMigrationIds as me, planMigrations as E, reactiveTriggerRepairSql as he, rollbackFileBasedMigration as ge, rollingDeployUnsafeOps as _e, runFileBasedMigrations as ve, shortFingerprint as D, squashMigrationPlans as ye, unblockLossy as be } from "@voltro/database/sql";
|
|
16
16
|
//#region src/cliError.ts
|
|
17
|
-
var
|
|
17
|
+
var O = class extends Error {
|
|
18
18
|
isCliError = !0;
|
|
19
19
|
code;
|
|
20
20
|
constructor(e, t = 1) {
|
|
21
21
|
super(e), this.name = "CliError", this.code = t;
|
|
22
22
|
}
|
|
23
|
-
},
|
|
23
|
+
}, xe = (e) => typeof e == "object" && !!e && e.isCliError === !0, Se = (e) => typeof e != "string", Ce = (e, t, n, r) => {
|
|
24
24
|
let i = [], a = new Map(t.map((e) => [e.name, e])), o = new Map(n.map((e) => [e.name, e])), s = new Set(e.leaveUnset ?? []);
|
|
25
25
|
r > 0 && i.push({
|
|
26
26
|
kind: "target-not-empty",
|
|
@@ -37,7 +37,7 @@ var k = class extends Error {
|
|
|
37
37
|
});
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
|
-
if (
|
|
40
|
+
if (Se(n)) {
|
|
41
41
|
c.push({
|
|
42
42
|
target: t,
|
|
43
43
|
source: n.expr,
|
|
@@ -80,23 +80,23 @@ var k = class extends Error {
|
|
|
80
80
|
remintIds: f,
|
|
81
81
|
fatal: i.some((e) => e.fatal)
|
|
82
82
|
};
|
|
83
|
-
},
|
|
83
|
+
}, we = (e, t) => [
|
|
84
84
|
`back up '${e.from}' to a restorable dump`,
|
|
85
85
|
`copy rows: INSERT INTO ${e.into} (${t.assignments.map((e) => e.target).join(", ")}) SELECT … FROM ${e.from}`,
|
|
86
86
|
...t.remintIds ? ["record old id → new id in a translation table", `rewrite every reference to '${e.from}', JSON columns included`] : [],
|
|
87
87
|
"verify: row counts match and a sample round-trips",
|
|
88
88
|
`drop '${e.from}' — LAST, and only after the verify`
|
|
89
|
-
],
|
|
89
|
+
], Te = (e, t, n) => {
|
|
90
90
|
let r = [`adopt · ${e.from} (${n} row(s)) → ${e.into}`, ""];
|
|
91
91
|
for (let e of t.assignments) r.push(` ${e.target.padEnd(24)} ← ${e.raw ? `${e.source} (raw)` : e.source}`);
|
|
92
92
|
t.problems.length > 0 && r.push("");
|
|
93
93
|
for (let e of t.problems) r.push(` ${e.fatal ? "✗" : "⚠"} ${e.detail}`);
|
|
94
94
|
if (r.push(""), t.fatal) return r.push(" REFUSED — fix the problems above. Nothing was written."), r;
|
|
95
95
|
r.push(" would run, in this order:");
|
|
96
|
-
for (let [n, i] of
|
|
96
|
+
for (let [n, i] of we(e, t).entries()) r.push(` ${n + 1}. ${i}`);
|
|
97
97
|
return r;
|
|
98
|
-
},
|
|
99
|
-
let a =
|
|
98
|
+
}, Ee = (e) => e === "mysql" || e === "mariadb" ? (e) => `\`${e}\`` : e === "mssql" ? (e) => `[${e}]` : (e) => `"${e}"`, De = (e) => `${e}__adopt_snapshot`, Oe = (e, t, n, r, i) => {
|
|
99
|
+
let a = Ee(n), o = De(e.from), s = t.assignments.map((e) => e.target), c = t.assignments.map((e) => e.raw ? e.source : a(e.source));
|
|
100
100
|
return i !== void 0 && (s.unshift(r), c.unshift(i)), {
|
|
101
101
|
snapshot: n === "mssql" ? `SELECT * INTO ${a(o)} FROM ${a(e.from)}` : `CREATE TABLE ${a(o)} AS SELECT * FROM ${a(e.from)}`,
|
|
102
102
|
copy: `INSERT INTO ${a(e.into)} (${s.map(a).join(", ")}) SELECT ${c.join(", ")} FROM ${a(e.from)}`,
|
|
@@ -106,18 +106,18 @@ var k = class extends Error {
|
|
|
106
106
|
drop: `DROP TABLE ${a(e.from)}`,
|
|
107
107
|
dropSnapshot: `DROP TABLE ${a(o)}`
|
|
108
108
|
};
|
|
109
|
-
},
|
|
109
|
+
}, ke = (e) => {
|
|
110
110
|
let t = e[0];
|
|
111
111
|
if (t === void 0) return 0;
|
|
112
112
|
let n = t.n ?? t.N ?? Object.values(t)[0];
|
|
113
113
|
return Number(n ?? 0);
|
|
114
|
-
},
|
|
114
|
+
}, Ae = async (e, t, n, r, i, a, o = {}) => {
|
|
115
115
|
if (t.fatal) throw Error("adopt: the plan was refused — nothing was run");
|
|
116
|
-
let s =
|
|
116
|
+
let s = Oe(e, t, n, r, i), c = De(e.from), l = ke(await a(s.verifySource));
|
|
117
117
|
await a(s.snapshot);
|
|
118
118
|
for (let e of s.translations) await a(e);
|
|
119
119
|
await a(s.copy);
|
|
120
|
-
let u =
|
|
120
|
+
let u = ke(await a(s.verifyTarget));
|
|
121
121
|
return u === l ? o.keepSource === !0 ? {
|
|
122
122
|
copied: u,
|
|
123
123
|
sourceRows: l,
|
|
@@ -137,7 +137,7 @@ var k = class extends Error {
|
|
|
137
137
|
snapshotTable: c,
|
|
138
138
|
sourceDropped: !1
|
|
139
139
|
};
|
|
140
|
-
},
|
|
140
|
+
}, k = [
|
|
141
141
|
"token",
|
|
142
142
|
"secret",
|
|
143
143
|
"password",
|
|
@@ -146,7 +146,7 @@ var k = class extends Error {
|
|
|
146
146
|
"credential",
|
|
147
147
|
"privatekey",
|
|
148
148
|
"private_key"
|
|
149
|
-
],
|
|
149
|
+
], je = [
|
|
150
150
|
{
|
|
151
151
|
table: "_voltro_audit_log",
|
|
152
152
|
column: "subject"
|
|
@@ -191,7 +191,7 @@ var k = class extends Error {
|
|
|
191
191
|
table: "_voltro_row_history",
|
|
192
192
|
column: "subjectId"
|
|
193
193
|
}
|
|
194
|
-
],
|
|
194
|
+
], Me = (e, t) => {
|
|
195
195
|
switch (t) {
|
|
196
196
|
case "postgres": return `LOWER(CAST(${e} AS text))`;
|
|
197
197
|
case "mysql":
|
|
@@ -200,15 +200,55 @@ var k = class extends Error {
|
|
|
200
200
|
case "sqlite":
|
|
201
201
|
case "turso": return `LOWER(CAST(${e} AS TEXT))`;
|
|
202
202
|
}
|
|
203
|
-
},
|
|
203
|
+
}, A = (e, t, n = k) => n.map((n) => `${Me(e, t)} LIKE '%${n}%'`).join("\n OR "), Ne = ["_omitted", "__redacted"], Pe = (e, t) => {
|
|
204
|
+
let n = e.toLowerCase();
|
|
205
|
+
return t.some((e) => n.includes(e));
|
|
206
|
+
}, j = (e, t = []) => {
|
|
207
|
+
if (Array.isArray(e)) {
|
|
208
|
+
for (let n of e) j(n, t);
|
|
209
|
+
return t;
|
|
210
|
+
}
|
|
211
|
+
if (typeof e == "object" && e) for (let [n, r] of Object.entries(e)) t.push(n), j(r, t);
|
|
212
|
+
return t;
|
|
213
|
+
}, M = (e) => Array.isArray(e) ? e.map(M) : typeof e == "object" && e ? Object.fromEntries(Object.entries(e).map(([e, t]) => [e, Ne.includes(e) ? null : M(t)])) : e, Fe = (e, t = k) => {
|
|
214
|
+
let n;
|
|
215
|
+
try {
|
|
216
|
+
n = JSON.parse(e);
|
|
217
|
+
} catch {
|
|
218
|
+
return {
|
|
219
|
+
asKey: !1,
|
|
220
|
+
redactionMarkerOnly: !1,
|
|
221
|
+
undecidable: !0
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
asKey: j(n).some((e) => Pe(e, t)),
|
|
226
|
+
redactionMarkerOnly: !Pe(JSON.stringify(M(n)) ?? "", t),
|
|
227
|
+
undecidable: !1
|
|
228
|
+
};
|
|
229
|
+
}, Ie = (e) => ({
|
|
230
|
+
examined: e.length,
|
|
231
|
+
asKey: e.filter((e) => e.asKey).length,
|
|
232
|
+
redactionMarkerOnly: e.filter((e) => e.redactionMarkerOnly && !e.asKey).length,
|
|
233
|
+
undecidable: e.filter((e) => e.undecidable).length
|
|
234
|
+
}), N = (e) => {
|
|
235
|
+
let t = e.classification;
|
|
236
|
+
return t === void 0 || e.hits === void 0 || e.hits === 0 ? !1 : t.examined === e.hits && t.undecidable === 0 && t.asKey === 0 && t.redactionMarkerOnly === e.hits;
|
|
237
|
+
}, Le = (e, t, n, r) => {
|
|
238
|
+
let i = `WHERE ${A(t, n)}`, a = Me(t, n).replace(/^LOWER\((.*)\)$/s, "$1");
|
|
239
|
+
return n === "mssql" ? `SELECT TOP (${r}) ${a} AS v FROM ${e}\n ${i}` : `SELECT ${a} AS v FROM ${e}\n ${i}\n LIMIT ${r}`;
|
|
240
|
+
}, Re = async (e, t, n, r, i) => (await g.runPromise(e.unsafe(Le(t, n, r, i)))).map((e) => {
|
|
241
|
+
let t = Object.values(e)[0];
|
|
242
|
+
return typeof t == "string" ? t : JSON.stringify(t ?? null);
|
|
243
|
+
}), P = async (e, t) => {
|
|
204
244
|
let n = await g.runPromise(e.unsafe(t));
|
|
205
245
|
return Number(Object.values(n[0] ?? { n: 0 })[0] ?? 0);
|
|
206
|
-
},
|
|
246
|
+
}, ze = async (e, t, n, r) => {
|
|
207
247
|
let i = [];
|
|
208
248
|
for (let a of n) {
|
|
209
249
|
let n = r(a.table), o;
|
|
210
250
|
try {
|
|
211
|
-
o = await
|
|
251
|
+
o = await P(e, `SELECT COUNT(*) AS n FROM ${n}`);
|
|
212
252
|
} catch {
|
|
213
253
|
i.push({
|
|
214
254
|
table: a.table,
|
|
@@ -220,12 +260,25 @@ var k = class extends Error {
|
|
|
220
260
|
continue;
|
|
221
261
|
}
|
|
222
262
|
try {
|
|
223
|
-
let s = await
|
|
263
|
+
let s = await P(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${A(r(a.column), t)}`), c = [];
|
|
264
|
+
if (s > 0) for (let i of k) try {
|
|
265
|
+
let o = await P(e, `SELECT COUNT(*) AS n FROM ${n}\n WHERE ${A(r(a.column), t, [i])}`);
|
|
266
|
+
o > 0 && c.push({
|
|
267
|
+
needle: i,
|
|
268
|
+
rows: o
|
|
269
|
+
});
|
|
270
|
+
} catch {}
|
|
271
|
+
let l;
|
|
272
|
+
if (s > 0) try {
|
|
273
|
+
l = Ie((await Re(e, n, r(a.column), t, 500)).map((e) => Fe(e)));
|
|
274
|
+
} catch {}
|
|
224
275
|
i.push({
|
|
225
276
|
table: a.table,
|
|
226
277
|
column: a.column,
|
|
227
278
|
scanned: o,
|
|
228
|
-
hits: s
|
|
279
|
+
hits: s,
|
|
280
|
+
matchedNeedles: c,
|
|
281
|
+
...l === void 0 ? {} : { classification: l }
|
|
229
282
|
});
|
|
230
283
|
} catch {
|
|
231
284
|
i.push({
|
|
@@ -238,27 +291,29 @@ var k = class extends Error {
|
|
|
238
291
|
}
|
|
239
292
|
}
|
|
240
293
|
return i;
|
|
241
|
-
},
|
|
242
|
-
let n = [`credential scan · dialect ${t}`, ""],
|
|
294
|
+
}, Be = (e, t) => {
|
|
295
|
+
let n = k.length, r = [`credential scan · dialect ${t}`, ""], i = /* @__PURE__ */ new Set();
|
|
243
296
|
for (let t of e) {
|
|
244
297
|
if (t.unavailable === "column") {
|
|
245
|
-
|
|
298
|
+
r.push(` · ${t.table}.${t.column} — column is not present on this deployment (${t.scanned} row(s) exist but this column was not scanned)`);
|
|
246
299
|
continue;
|
|
247
300
|
}
|
|
248
301
|
if (t.scanned === void 0) {
|
|
249
|
-
|
|
302
|
+
i.has(t.table) || (i.add(t.table), r.push(` · ${t.table} — table not present (nothing scanned)`));
|
|
250
303
|
continue;
|
|
251
304
|
}
|
|
252
305
|
if (t.scanned === 0) {
|
|
253
|
-
|
|
306
|
+
r.push(` · ${t.table} — table is EMPTY (0 rows scanned; this is not a clean bill of health)`);
|
|
254
307
|
continue;
|
|
255
308
|
}
|
|
256
|
-
let e = (t.hits ?? 0)
|
|
257
|
-
|
|
309
|
+
let e = N(t), n = (t.hits ?? 0) === 0 ? "✓" : e ? "~" : "✗";
|
|
310
|
+
r.push(` ${n} ${t.table}.${t.column} — ${t.hits} of ${t.scanned} row(s) contain a credential-shaped NAME (anywhere in the value)`), (t.matchedNeedles?.length ?? 0) > 0 && r.push(` matched (rows per needle, may overlap): ${t.matchedNeedles.map((e) => `${e.needle} (${e.rows})`).join(", ")}`);
|
|
311
|
+
let a = t.classification;
|
|
312
|
+
a !== void 0 && (r.push(` examined ${a.examined} of ${t.hits} matched row(s)${a.examined < (t.hits ?? 0) ? " (capped at 500)" : ""}: ${a.asKey} as a JSON KEY, ${a.redactionMarkerOnly} only inside a redaction marker${a.undecidable > 0 ? `, ${a.undecidable} not parseable as JSON` : ""}`), e && r.push(` EXPLAINED — every match is \`${Ne.join("` / `")}\`, the framework's own`, " record that a `.sensitive()` column was left OUT. Nothing to purge, nothing to rotate."));
|
|
258
313
|
}
|
|
259
|
-
let
|
|
260
|
-
return
|
|
261
|
-
},
|
|
314
|
+
let a = e.reduce((e, t) => e + (N(t) ? 0 : t.hits ?? 0), 0), o = e.reduce((e, t) => e + (N(t) ? t.hits ?? 0 : 0), 0);
|
|
315
|
+
return r.push(""), o > 0 && r.push(` ${o} row(s) matched only the framework's own redaction markers and are NOT findings.`), a > 0 ? r.push(` ${a} row(s) carry something credential-shaped.`, " Purge them AND rotate the credentials — assume anything written to a log", " table has been read. Then stop putting the credential on the Subject:", " `connectionCredentials(...)` keeps it in the framework vault.") : e.every((e) => e.scanned === void 0 || e.scanned === 0 || e.unavailable !== void 0) ? r.push(" NOTHING WAS SCANNED. This run proves nothing about your data.") : r.push(" No credential-shaped keys found in the rows that exist."), r.push("", ` These are NAME heuristics (${n} of them), not proof. A credential in a`, " column whose key is not credential-shaped — `keyValue`, `pat`, `authz` —", " is invisible here. A clean run means \"no credential-shaped NAME found\",", " which is not the same as \"no credential\".", "", " Run this on EVERY environment. A dev database is not a sample of production."), r.join("\n");
|
|
316
|
+
}, Ve = (e) => e.some((e) => (e.hits ?? 0) > 0 && !N(e)) ? 1 : e.every((e) => e.unavailable !== void 0 || e.scanned === void 0 || e.scanned === 0) ? 2 : 0, F = /\.migration\.ts$/, I = async (e, t) => {
|
|
262
317
|
let n = [];
|
|
263
318
|
for (let r of e) {
|
|
264
319
|
let e;
|
|
@@ -278,7 +333,7 @@ var k = class extends Error {
|
|
|
278
333
|
idInFile: i.id,
|
|
279
334
|
idFromFile: d(r).replace(/\.migration\.ts$/, "")
|
|
280
335
|
});
|
|
281
|
-
let a = await v.readFile(r, "utf8").catch(() => ""), o =
|
|
336
|
+
let a = await v.readFile(r, "utf8").catch(() => ""), o = ae("sha256").update(a).digest("hex").slice(0, 16);
|
|
282
337
|
n.push({
|
|
283
338
|
file: r,
|
|
284
339
|
definition: i,
|
|
@@ -286,14 +341,14 @@ var k = class extends Error {
|
|
|
286
341
|
});
|
|
287
342
|
}
|
|
288
343
|
return [...n].sort((e, t) => e.definition.id < t.definition.id ? -1 : 1);
|
|
289
|
-
},
|
|
344
|
+
}, L = (e) => {
|
|
290
345
|
let t = (process.env.DB_DIALECT ?? "postgres").toLowerCase(), n = t === "memory" ? "postgres" : t;
|
|
291
|
-
return g.runPromise(w(
|
|
292
|
-
},
|
|
293
|
-
let e = yield* (yield*
|
|
346
|
+
return g.runPromise(w(S, n).pipe(g.provide(e)));
|
|
347
|
+
}, R = (e) => g.runPromise(g.gen(function* () {
|
|
348
|
+
let e = yield* (yield* x.SqlClient)`SELECT id, status, hash, "appliedAt" FROM "_voltro_migrations"`.pipe(g.orElseSucceed(() => []));
|
|
294
349
|
return new Map(e.map((e) => [e.id, e]));
|
|
295
|
-
}).pipe(g.provide(e))),
|
|
296
|
-
yield* (yield*
|
|
350
|
+
}).pipe(g.provide(e))), z = (e, t) => g.runPromise(g.gen(function* () {
|
|
351
|
+
yield* (yield* x.SqlClient)`
|
|
297
352
|
INSERT INTO "_voltro_migrations"
|
|
298
353
|
(id, name, hash, status, "appliedAt", "durationMs", "errorMessage", "errorStack")
|
|
299
354
|
VALUES
|
|
@@ -308,14 +363,14 @@ var k = class extends Error {
|
|
|
308
363
|
"errorStack" = EXCLUDED."errorStack",
|
|
309
364
|
"updatedAt" = now()
|
|
310
365
|
`;
|
|
311
|
-
}).pipe(g.provide(e))),
|
|
366
|
+
}).pipe(g.provide(e))), He = async (e, t, n) => {
|
|
312
367
|
if (e.length === 0) return {
|
|
313
368
|
applied: [],
|
|
314
369
|
failed: null,
|
|
315
370
|
skipped: []
|
|
316
371
|
};
|
|
317
|
-
await
|
|
318
|
-
let r = await
|
|
372
|
+
await L(t);
|
|
373
|
+
let r = await R(t), i = [], a = [], o = null;
|
|
319
374
|
for (let s of e) {
|
|
320
375
|
let e = r.get(s.definition.id);
|
|
321
376
|
if (e && e.status === "applied") {
|
|
@@ -336,7 +391,7 @@ var k = class extends Error {
|
|
|
336
391
|
name: s.definition.name
|
|
337
392
|
});
|
|
338
393
|
let c = Date.now(), l = [];
|
|
339
|
-
await
|
|
394
|
+
await z(t, {
|
|
340
395
|
id: s.definition.id,
|
|
341
396
|
name: s.definition.name,
|
|
342
397
|
hash: s.hash,
|
|
@@ -377,7 +432,7 @@ var k = class extends Error {
|
|
|
377
432
|
...a.stack ? { stack: a.stack } : {}
|
|
378
433
|
}
|
|
379
434
|
};
|
|
380
|
-
o = f, i.push(f), await
|
|
435
|
+
o = f, i.push(f), await z(t, {
|
|
381
436
|
id: s.definition.id,
|
|
382
437
|
name: s.definition.name,
|
|
383
438
|
hash: s.hash,
|
|
@@ -396,7 +451,7 @@ var k = class extends Error {
|
|
|
396
451
|
status: "applied",
|
|
397
452
|
durationMs: Date.now() - c,
|
|
398
453
|
steps: l
|
|
399
|
-
}), await
|
|
454
|
+
}), await z(t, {
|
|
400
455
|
id: s.definition.id,
|
|
401
456
|
name: s.definition.name,
|
|
402
457
|
hash: s.hash,
|
|
@@ -416,9 +471,9 @@ var k = class extends Error {
|
|
|
416
471
|
failed: o,
|
|
417
472
|
skipped: a
|
|
418
473
|
};
|
|
419
|
-
},
|
|
420
|
-
await
|
|
421
|
-
let i = await
|
|
474
|
+
}, Ue = async (e, t, n, r = {}) => {
|
|
475
|
+
await L(t);
|
|
476
|
+
let i = await R(t), a = e.filter((e) => i.get(e.definition.id)?.status === "applied");
|
|
422
477
|
if (a.length === 0) return n.info("migration: nothing to roll back"), {
|
|
423
478
|
applied: [],
|
|
424
479
|
failed: null,
|
|
@@ -471,7 +526,7 @@ var k = class extends Error {
|
|
|
471
526
|
status: "applied",
|
|
472
527
|
durationMs: Date.now() - r,
|
|
473
528
|
steps: i
|
|
474
|
-
}), await
|
|
529
|
+
}), await z(t, {
|
|
475
530
|
id: e.definition.id,
|
|
476
531
|
name: e.definition.name,
|
|
477
532
|
hash: e.hash,
|
|
@@ -487,9 +542,9 @@ var k = class extends Error {
|
|
|
487
542
|
failed: c,
|
|
488
543
|
skipped: []
|
|
489
544
|
};
|
|
490
|
-
},
|
|
491
|
-
await
|
|
492
|
-
let n = await
|
|
545
|
+
}, We = async (e, t) => {
|
|
546
|
+
await L(t);
|
|
547
|
+
let n = await R(t);
|
|
493
548
|
return e.map((e) => {
|
|
494
549
|
let t = n.get(e.definition.id);
|
|
495
550
|
return t ? {
|
|
@@ -506,7 +561,7 @@ var k = class extends Error {
|
|
|
506
561
|
hashMatches: !0
|
|
507
562
|
};
|
|
508
563
|
});
|
|
509
|
-
},
|
|
564
|
+
}, Ge = (e) => {
|
|
510
565
|
if (!e.failed) return "";
|
|
511
566
|
let t = e.failed, n = [
|
|
512
567
|
`✗ Migration failed: ${t.migrationId} / step '${t.error?.stepName ?? "?"}'`,
|
|
@@ -519,7 +574,59 @@ var k = class extends Error {
|
|
|
519
574
|
""
|
|
520
575
|
];
|
|
521
576
|
return e.applied.length > 0 && n.unshift("", `(${e.applied.length} migration(s) applied successfully before the failure.)`), n.join("\n");
|
|
522
|
-
},
|
|
577
|
+
}, Ke = (e) => {
|
|
578
|
+
let t = e.split(".");
|
|
579
|
+
if (t.length !== 2 || !t[0] || !t[1]) throw Error(`voltro db encrypt-column: expected <table>.<column>, got "${e}"`);
|
|
580
|
+
return {
|
|
581
|
+
table: t[0],
|
|
582
|
+
column: t[1]
|
|
583
|
+
};
|
|
584
|
+
}, qe = (e) => 49 + 4 * Math.ceil(e / 3), Je = (e, t) => {
|
|
585
|
+
let n = qe(e);
|
|
586
|
+
return {
|
|
587
|
+
longestPlaintextBytes: e,
|
|
588
|
+
longestCiphertext: n,
|
|
589
|
+
declaredMaxLength: t,
|
|
590
|
+
fits: t === void 0 || n <= t
|
|
591
|
+
};
|
|
592
|
+
}, Ye = (e, t) => `${e.table}.${e.column} is too narrow: its longest value is ${t.longestPlaintextBytes} byte(s), which encrypts to ${t.longestCiphertext} characters, and the column is declared as ${t.declaredMaxLength}. Widen it first (\`.maxLength(${t.longestCiphertext})\` or more, then \`voltro db apply\`) and run this again. Nothing was written.`, Xe = (e, t) => {
|
|
593
|
+
let n = t ? "would encrypt" : "encrypted";
|
|
594
|
+
return [
|
|
595
|
+
` ${e.table}.${e.column}`,
|
|
596
|
+
` scanned ${e.scanned}`,
|
|
597
|
+
` already encrypted ${e.alreadyEncrypted} (skipped — a re-run is safe)`,
|
|
598
|
+
` null / empty ${e.empty}`,
|
|
599
|
+
` ${n.padEnd(18)} ${e.encrypted}`
|
|
600
|
+
].join("\n");
|
|
601
|
+
}, Ze = (e, t, n) => {
|
|
602
|
+
if (e == null) return;
|
|
603
|
+
let r = typeof e == "string" ? e : String(e);
|
|
604
|
+
if (r === "" || C(r)) return;
|
|
605
|
+
let i = t.encrypt(r), a;
|
|
606
|
+
try {
|
|
607
|
+
a = t.decrypt(i);
|
|
608
|
+
} catch (e) {
|
|
609
|
+
throw Error(`${n.table}.${n.column}: the cipher produced a value it cannot read back (${e.message ?? String(e)}). Nothing was written.`);
|
|
610
|
+
}
|
|
611
|
+
if (a !== r) throw Error(`${n.table}.${n.column}: round-trip mismatch — the cipher did not return what it was given. Nothing was written.`);
|
|
612
|
+
return i;
|
|
613
|
+
}, Qe = (e) => Buffer.byteLength(e, "utf8"), $e = (e, t) => {
|
|
614
|
+
if (e !== void 0) try {
|
|
615
|
+
return t.decrypt(e), !0;
|
|
616
|
+
} catch {
|
|
617
|
+
return !1;
|
|
618
|
+
}
|
|
619
|
+
}, et = (e, t) => `${e.table}.${e.column} already holds values encrypted with a DIFFERENT key than ${t} — the active key cannot decrypt them. Continuing would encrypt the remaining rows under a second key, and the column would then be readable with neither key alone. Nothing was written. Point ${t} at the key the existing rows were written with.`, tt = (e) => [
|
|
620
|
+
"",
|
|
621
|
+
" NEXT — this converted the rows that exist. New writes are still plaintext",
|
|
622
|
+
" until the column carries the modifier:",
|
|
623
|
+
"",
|
|
624
|
+
...e.map((e) => ` ${e.column}: text().encrypted() // ${e.table}`),
|
|
625
|
+
"",
|
|
626
|
+
" Order does not matter: a read returns a non-ciphertext value unchanged, so",
|
|
627
|
+
" the column may hold a mix while you deploy. Run this again afterwards to",
|
|
628
|
+
" catch anything written in between — it skips what is already encrypted."
|
|
629
|
+
].join("\n"), B = _({ scope: "voltro:db" }), nt = /* @__PURE__ */ new Set([
|
|
523
630
|
"node_modules",
|
|
524
631
|
"dist",
|
|
525
632
|
"build",
|
|
@@ -527,17 +634,17 @@ var k = class extends Error {
|
|
|
527
634
|
".vite",
|
|
528
635
|
".next",
|
|
529
636
|
".git"
|
|
530
|
-
]),
|
|
637
|
+
]), V = async (e, t) => {
|
|
531
638
|
let n = [], r = async (e) => {
|
|
532
639
|
let i = await v.readdir(e, { withFileTypes: !0 }).catch(() => []);
|
|
533
640
|
for (let a of i) {
|
|
534
|
-
if (
|
|
641
|
+
if (nt.has(a.name)) continue;
|
|
535
642
|
let i = f(e, a.name);
|
|
536
643
|
a.isDirectory() ? await r(i) : a.isFile() && t.test(a.name) && n.push(i);
|
|
537
644
|
}
|
|
538
645
|
};
|
|
539
646
|
return await r(e), n;
|
|
540
|
-
},
|
|
647
|
+
}, H = (e) => m(e.find((e) => !e.startsWith("-")) ?? process.cwd()), U = () => {
|
|
541
648
|
let e = process.env.DB_MAX_CONNECTIONS ?? process.env.PG_MAX_CONNECTIONS, t = e !== void 0 && e !== "" ? Number(e) : NaN, n = Number.isInteger(t) && t > 0 ? { maxConnections: t } : {}, r = process.env.DB_SCHEMA, i = r && r.length > 0 ? { schema: r } : {}, a = process.env.DB_DIRECT_URL ?? process.env.DB_MIGRATE_URL ?? process.env.DB_URL ?? process.env.DB_PRIMARY_URL;
|
|
542
649
|
return a ? {
|
|
543
650
|
url: a,
|
|
@@ -552,63 +659,63 @@ var k = class extends Error {
|
|
|
552
659
|
...n,
|
|
553
660
|
...i
|
|
554
661
|
};
|
|
555
|
-
},
|
|
662
|
+
}, rt = (e) => {
|
|
556
663
|
let t = e;
|
|
557
664
|
if (typeof t != "object" || !t) return !1;
|
|
558
665
|
if (t.code === "ERR_OUT_OF_RANGE" || t.cause?.code === "ERR_OUT_OF_RANGE") return !0;
|
|
559
666
|
let n = `${t.message ?? ""} ${t.cause?.message ?? ""} ${t.stack ?? ""}`;
|
|
560
667
|
return /ERR_OUT_OF_RANGE|out of range/i.test(n) && /pg-protocol|parseDataRow|buffer-reader|BufferReader/i.test(n);
|
|
561
|
-
},
|
|
668
|
+
}, it = "Live-DB introspection got a corrupt / misaligned response from the database. This happens when `voltro db plan` / `apply` / `drift` / `migrate` runs through a connection POOLER (Supabase Supavisor / PgBouncer) on a LARGE schema — the pooler mis-frames a big catalog response. The introspection already batches by table to keep responses small; if a batch still trips it:\n 1. Lower the batch: set `VOLTRO_INTROSPECT_BATCH` to a smaller number of tables (default 20 — try 5 or 1). This keeps `db plan` on the normal pooler, no direct connection needed.\n 2. Or use a DIRECT (non-pooler) connection: set `DB_DIRECT_URL` (or `DB_MIGRATE_URL`) to the direct database URL — the runtime keeps `DB_URL`, so only the migration path is affected. (Supabase: direct = `db.<project-ref>.supabase.co` [IPv6-only on newer projects]; pooler = `aws-0-<region>.pooler.supabase.com`.)", W = () => {
|
|
562
669
|
let e = (process.env.DB_DIALECT ?? "postgres").toLowerCase();
|
|
563
|
-
if (e === "memory") throw new
|
|
670
|
+
if (e === "memory") throw new O("voltro db: subcommands require a real dialect, not DB_DIALECT=memory.");
|
|
564
671
|
return e;
|
|
565
|
-
}, G = (e) =>
|
|
566
|
-
let t =
|
|
567
|
-
if (r.length === 0) return
|
|
672
|
+
}, G = (e) => re(e, W()), K = (e) => e === "mysql" || e === "mariadb" ? "table_schema = DATABASE()" : e === "mssql" ? "table_schema = SCHEMA_NAME()" : "table_schema = 'public'", q = async () => (await r(W())).makeSqlLayer(U()), at = async (e) => {
|
|
673
|
+
let t = H(e), n = e.includes("--dry-run"), r = await I(await V(t, F), B);
|
|
674
|
+
if (r.length === 0) return B.info("no migrations found", { root: t }), 0;
|
|
568
675
|
if (n) {
|
|
569
676
|
let e = /* @__PURE__ */ new Set();
|
|
570
677
|
try {
|
|
571
|
-
let t = await
|
|
678
|
+
let t = await We(r, await q());
|
|
572
679
|
e = new Set(t.filter((e) => e.status === "applied").map((e) => e.id));
|
|
573
680
|
} catch {
|
|
574
|
-
|
|
681
|
+
B.warn("migrate --dry-run: could not read the migration ledger — showing ALL as pending");
|
|
575
682
|
}
|
|
576
683
|
let t = r.filter((t) => !e.has(t.definition.id));
|
|
577
|
-
if (t.length === 0) return
|
|
578
|
-
|
|
684
|
+
if (t.length === 0) return B.info("migrate --dry-run: nothing pending — every migration is applied"), 0;
|
|
685
|
+
B.info(`migrate --dry-run: ${t.length} pending (nothing applied)`);
|
|
579
686
|
for (let e of t) console.log(` pending ${e.definition.id}`);
|
|
580
687
|
return 0;
|
|
581
688
|
}
|
|
582
|
-
let i = await
|
|
583
|
-
return i.failed ? (process.stderr.write(
|
|
689
|
+
let i = await He(r, await q(), B);
|
|
690
|
+
return i.failed ? (process.stderr.write(Ge(i) + "\n"), 1) : (B.info("migrate complete", {
|
|
584
691
|
applied: i.applied.length,
|
|
585
692
|
skipped: i.skipped.length
|
|
586
693
|
}), 0);
|
|
587
|
-
},
|
|
588
|
-
let t =
|
|
589
|
-
if (r.length === 0) return
|
|
590
|
-
let i = await
|
|
591
|
-
return i.failed ? (process.stderr.write(
|
|
592
|
-
},
|
|
593
|
-
let t = await
|
|
694
|
+
}, ot = async (e) => {
|
|
695
|
+
let t = H(e), n = l(e, "--to"), r = await I(await V(t, F), B);
|
|
696
|
+
if (r.length === 0) return B.info("no migrations found", { root: t }), 0;
|
|
697
|
+
let i = await Ue(r, await q(), B, n ? { to: n } : {});
|
|
698
|
+
return i.failed ? (process.stderr.write(Ge(i) + "\n"), 1) : (B.info("rollback complete", { reverted: i.applied.length }), 0);
|
|
699
|
+
}, st = async (e) => {
|
|
700
|
+
let t = await I(await V(H(e), F), B);
|
|
594
701
|
if (t.length === 0) return console.log("no migrations found"), 0;
|
|
595
|
-
let n = await
|
|
702
|
+
let n = await We(t, await q());
|
|
596
703
|
for (let e of n) {
|
|
597
704
|
let t = e.status === "applied" ? "✓" : e.status === "failed" ? "✗" : e.status === "reverted" ? "↩" : "·", n = e.hashMatches ? "" : " (hash drift!)", r = e.appliedAt ? ` @ ${e.appliedAt.toISOString()}` : "";
|
|
598
705
|
console.log(` ${t} ${e.id} [${e.status}] ${e.name}${r}${n}`);
|
|
599
706
|
}
|
|
600
707
|
return 0;
|
|
601
|
-
},
|
|
708
|
+
}, ct = async (e) => {
|
|
602
709
|
let t = (l(e, "--store") ?? process.env.DB_DIALECT ?? "postgres").toLowerCase();
|
|
603
|
-
if (t === "memory") return
|
|
604
|
-
store: new
|
|
710
|
+
if (t === "memory") return B.warn("voltro db seed: using in-memory store (explicit opt-in via --store memory / DB_DIALECT=memory)"), B.warn(" rows written by this run will NOT persist beyond the process. set DB_DIALECT=postgres or unset to seed the real DB."), {
|
|
711
|
+
store: new b(),
|
|
605
712
|
dispose: async () => {},
|
|
606
713
|
dialectId: "memory"
|
|
607
714
|
};
|
|
608
715
|
let n = await r(t);
|
|
609
|
-
|
|
716
|
+
B.info(`voltro db seed: target store=${n.id} — rows will persist`);
|
|
610
717
|
let i = await n.makeStore({
|
|
611
|
-
sqlLayer: n.makeSqlLayer(
|
|
718
|
+
sqlLayer: n.makeSqlLayer(U()),
|
|
612
719
|
changeStrategy: "inline"
|
|
613
720
|
}), a = i;
|
|
614
721
|
return {
|
|
@@ -618,77 +725,77 @@ var k = class extends Error {
|
|
|
618
725
|
},
|
|
619
726
|
dialectId: n.id
|
|
620
727
|
};
|
|
621
|
-
},
|
|
622
|
-
let a =
|
|
623
|
-
if (c.length === 0) return
|
|
624
|
-
let { store: u, dispose: d, dialectId: f } = await
|
|
728
|
+
}, lt = async (r) => {
|
|
729
|
+
let a = H(r), o = l(r, "--id"), s = l(r, "--lifecycle"), c = await t(await V(a, i), B);
|
|
730
|
+
if (c.length === 0) return B.info("no seeds found", { root: a }), 0;
|
|
731
|
+
let { store: u, dispose: d, dialectId: f } = await ct(r);
|
|
625
732
|
try {
|
|
626
|
-
return o ? +((await e(c, o, u,
|
|
733
|
+
return o ? +((await e(c, o, u, B)).status === "failed") : +((await n((s ? c.filter((e) => e.definition.lifecycle === s) : c.filter((e) => e.definition.lifecycle === "boot")).map((e) => ({
|
|
627
734
|
...e,
|
|
628
735
|
definition: {
|
|
629
736
|
...e.definition,
|
|
630
737
|
lifecycle: "boot"
|
|
631
738
|
}
|
|
632
|
-
})), u,
|
|
739
|
+
})), u, B, { force: !0 })).filter((e) => e.status === "failed").length > 0);
|
|
633
740
|
} finally {
|
|
634
741
|
await d().catch((e) => {
|
|
635
|
-
|
|
742
|
+
B.warn("voltro db seed: store dispose failed", { error: String(e) });
|
|
636
743
|
});
|
|
637
744
|
}
|
|
638
|
-
},
|
|
639
|
-
let t = await
|
|
745
|
+
}, ut = (e) => typeof e == "object" && !!e && "tableName" in e && "fields" in e && typeof e.tableName == "string", dt = async (e) => {
|
|
746
|
+
let t = await V(e, s), n = [];
|
|
640
747
|
for (let r of t) {
|
|
641
748
|
let t;
|
|
642
749
|
try {
|
|
643
750
|
t = await import(h(r).href);
|
|
644
751
|
} catch (t) {
|
|
645
|
-
|
|
752
|
+
B.warn("db: failed to import schema file", {
|
|
646
753
|
file: p(e, r),
|
|
647
754
|
error: String(t)
|
|
648
755
|
});
|
|
649
756
|
continue;
|
|
650
757
|
}
|
|
651
|
-
for (let e of Object.values(t))
|
|
758
|
+
for (let e of Object.values(t)) ut(e) && n.push(e);
|
|
652
759
|
}
|
|
653
760
|
return n;
|
|
654
|
-
},
|
|
761
|
+
}, ft = (e) => {
|
|
655
762
|
let t = new Map(e.map((e) => [e.tableName, e]));
|
|
656
763
|
t.has("actors") || t.set("actors", te);
|
|
657
764
|
let n = t.get("actors"), r = t.get("tenants");
|
|
658
|
-
return
|
|
765
|
+
return ie({
|
|
659
766
|
actors: () => n,
|
|
660
767
|
...r === void 0 ? {} : { tenants: () => r }
|
|
661
768
|
}), [...t.values()];
|
|
662
|
-
},
|
|
769
|
+
}, pt = (e) => {
|
|
663
770
|
let t = /* @__PURE__ */ new Map();
|
|
664
771
|
for (let n of e) {
|
|
665
772
|
let e = n.tableName;
|
|
666
773
|
e !== void 0 && !t.has(e) && t.set(e, n);
|
|
667
774
|
}
|
|
668
775
|
return [...t.values()];
|
|
669
|
-
},
|
|
670
|
-
let t =
|
|
776
|
+
}, mt = (e) => {
|
|
777
|
+
let t = ue(process.env.VOLTRO_DESTRUCTIVE_OK);
|
|
671
778
|
if (t === void 0 || !e.operations.filter((e) => e.blocked).every((e) => e.classification === "lossy")) return {
|
|
672
779
|
plan: e,
|
|
673
780
|
acknowledged: !1
|
|
674
781
|
};
|
|
675
|
-
let n =
|
|
782
|
+
let n = be(e, t);
|
|
676
783
|
return {
|
|
677
784
|
plan: n,
|
|
678
785
|
acknowledged: !n.operations.some((e) => e.blocked)
|
|
679
786
|
};
|
|
680
787
|
}, J = async (e, t) => g.runPromise(t.pipe(g.provide(e))), Y = async (e) => {
|
|
681
788
|
try {
|
|
682
|
-
return await J(e,
|
|
789
|
+
return await J(e, x.SqlClient.pipe(g.flatMap(pe)));
|
|
683
790
|
} catch (e) {
|
|
684
791
|
let t = String(e?.message ?? e);
|
|
685
|
-
if (
|
|
686
|
-
let n = /* @__PURE__ */ Error(`${
|
|
792
|
+
if (rt(e) || /connection error/i.test(t)) {
|
|
793
|
+
let n = /* @__PURE__ */ Error(`${it}\n\nOriginal error: ${t}`);
|
|
687
794
|
throw n.cause = e, n;
|
|
688
795
|
}
|
|
689
796
|
throw e;
|
|
690
797
|
}
|
|
691
|
-
},
|
|
798
|
+
}, ht = {
|
|
692
799
|
safe: "\x1B[32m✓\x1B[0m",
|
|
693
800
|
"needs-default": "\x1B[32m⊕\x1B[0m",
|
|
694
801
|
"needs-backfill": "\x1B[33m⊕\x1B[0m",
|
|
@@ -696,7 +803,7 @@ var k = class extends Error {
|
|
|
696
803
|
lossy: "\x1B[31m✗\x1B[0m",
|
|
697
804
|
"online-required": "\x1B[36m◷\x1B[0m",
|
|
698
805
|
"multi-step": "\x1B[35m⇉\x1B[0m"
|
|
699
|
-
},
|
|
806
|
+
}, gt = (e) => {
|
|
700
807
|
let t = e.op, n;
|
|
701
808
|
switch (t.kind) {
|
|
702
809
|
case "create-table":
|
|
@@ -760,9 +867,9 @@ var k = class extends Error {
|
|
|
760
867
|
n = `ALTER TABLE ${t.table} DROP CHECK ${t.column}`;
|
|
761
868
|
break;
|
|
762
869
|
}
|
|
763
|
-
let r =
|
|
870
|
+
let r = ht[e.classification], i = e.reason ? ` [2m# ${e.reason}[0m` : "", a = e.blocked ? `\n [31m! ${e.blocked.fix}[0m` : "";
|
|
764
871
|
return ` ${r} ${n}${i}${a}`;
|
|
765
|
-
},
|
|
872
|
+
}, _t = async (e, t) => {
|
|
766
873
|
let n = e.operations.filter((e) => e.classification === "needs-default" && e.op.kind === "add-column");
|
|
767
874
|
if (n.length === 0) return;
|
|
768
875
|
let r = [];
|
|
@@ -771,7 +878,7 @@ var k = class extends Error {
|
|
|
771
878
|
if (i === void 0 || a === void 0) continue;
|
|
772
879
|
let o = 0;
|
|
773
880
|
try {
|
|
774
|
-
let e = await J(t,
|
|
881
|
+
let e = await J(t, x.SqlClient.pipe(g.flatMap((e) => e.unsafe(`SELECT COUNT(*) AS n FROM ${re(i, W())}`))));
|
|
775
882
|
o = Number(Object.values(e[0] ?? { n: 0 })[0] ?? 0);
|
|
776
883
|
} catch {
|
|
777
884
|
continue;
|
|
@@ -785,13 +892,13 @@ var k = class extends Error {
|
|
|
785
892
|
}
|
|
786
893
|
}, X = async (e, t) => {
|
|
787
894
|
if (W() !== "postgres") return;
|
|
788
|
-
let n =
|
|
895
|
+
let n = oe, r;
|
|
789
896
|
try {
|
|
790
|
-
r = await J(t,
|
|
897
|
+
r = await J(t, x.SqlClient.pipe(g.flatMap((t) => de(t, e, n))));
|
|
791
898
|
} catch {
|
|
792
899
|
return;
|
|
793
900
|
}
|
|
794
|
-
let i =
|
|
901
|
+
let i = he({
|
|
795
902
|
tables: e,
|
|
796
903
|
dialect: "postgres",
|
|
797
904
|
missing: r.missing,
|
|
@@ -799,10 +906,10 @@ var k = class extends Error {
|
|
|
799
906
|
channel: n
|
|
800
907
|
});
|
|
801
908
|
if (i.length === 0) return;
|
|
802
|
-
|
|
909
|
+
B.info(`db apply: installing change triggers on ${r.missing.length} table(s)` + (r.stale.length > 0 ? `, removing ${r.stale.length} stale one(s)` : ""));
|
|
803
910
|
let a = 0;
|
|
804
|
-
for (let e of i) await J(t,
|
|
805
|
-
|
|
911
|
+
for (let e of i) await J(t, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(e)))), a += 1;
|
|
912
|
+
B.info(`db apply: change triggers converged (${a} statement(s))`, {
|
|
806
913
|
installed: r.missing.length,
|
|
807
914
|
removed: r.stale.length
|
|
808
915
|
});
|
|
@@ -812,40 +919,40 @@ var k = class extends Error {
|
|
|
812
919
|
console.log(" (schema is up to date)\n");
|
|
813
920
|
return;
|
|
814
921
|
}
|
|
815
|
-
for (let t of e.operations) console.log(
|
|
816
|
-
console.log(), console.log(` safe: ${t.safe} needs-default: ${t.needsDefault} needs-backfill: ${t.needsBackfill}`), console.log(` rename: ${t.needsRenameAnnotation} lossy: ${t.lossy} blocked: ${t.blocked}`), console.log(` fingerprint: ${e.fromFingerprint.slice(0, 16)} → ${e.toFingerprint.slice(0, 16)}\n`),
|
|
817
|
-
},
|
|
818
|
-
let t =
|
|
922
|
+
for (let t of e.operations) console.log(gt(t));
|
|
923
|
+
console.log(), console.log(` safe: ${t.safe} needs-default: ${t.needsDefault} needs-backfill: ${t.needsBackfill}`), console.log(` rename: ${t.needsRenameAnnotation} lossy: ${t.lossy} blocked: ${t.blocked}`), console.log(` fingerprint: ${e.fromFingerprint.slice(0, 16)} → ${e.toFingerprint.slice(0, 16)}\n`), vt(e);
|
|
924
|
+
}, vt = (e) => {
|
|
925
|
+
let t = _e(e.operations);
|
|
819
926
|
if (t.length !== 0) {
|
|
820
927
|
console.log(` ⚠ ${t.length} operation(s) UNSAFE under a rolling deploy`), console.log(" (old + new instances overlap → old code breaks against the new schema):");
|
|
821
928
|
for (let e of t) console.log(` • ${e.op.op.kind}: ${e.reason}`), console.log(` → ${e.remedy}`);
|
|
822
929
|
console.log(" Deploy with a maintenance window / scale-to-zero, or split into"), console.log(" expand → deploy → contract. See docs: deployment/production-hardening.\n");
|
|
823
930
|
}
|
|
824
|
-
},
|
|
825
|
-
let n =
|
|
931
|
+
}, yt = (e, t) => {
|
|
932
|
+
let n = ce({
|
|
826
933
|
enabled: process.env.VOLTRO_ROLLING_DEPLOY === "1",
|
|
827
934
|
forced: t.includes("--force"),
|
|
828
|
-
unsafeCount:
|
|
935
|
+
unsafeCount: _e(e.operations).length
|
|
829
936
|
});
|
|
830
|
-
return n.refuse ? (
|
|
831
|
-
},
|
|
937
|
+
return n.refuse ? (B.error(`db apply: ${n.message}`), !0) : (n.warnForced && B.warn(`db apply: ${n.message}`), !1);
|
|
938
|
+
}, bt = (e) => {
|
|
832
939
|
let t = l(e, "--online-after") ?? process.env.VOLTRO_ONLINE_THRESHOLD;
|
|
833
940
|
if (t === void 0) return;
|
|
834
941
|
let n = Number(t);
|
|
835
942
|
if (!Number.isInteger(n) || n < 0) throw Error(`--online-after / VOLTRO_ONLINE_THRESHOLD must be a non-negative integer, got "${t}"`);
|
|
836
943
|
return n;
|
|
837
944
|
}, Q = async (e) => {
|
|
838
|
-
let t = await
|
|
945
|
+
let t = await dt(e);
|
|
839
946
|
if (t.length === 0) return;
|
|
840
947
|
let n = await o({ root: e });
|
|
841
|
-
return
|
|
948
|
+
return ft(pt([
|
|
842
949
|
...t,
|
|
843
|
-
...
|
|
950
|
+
...S,
|
|
844
951
|
...n
|
|
845
952
|
]));
|
|
846
|
-
},
|
|
847
|
-
let t =
|
|
848
|
-
if (r === void 0) return
|
|
953
|
+
}, xt = async (e) => {
|
|
954
|
+
let t = bt(e), n = H(e), r = await Q(n);
|
|
955
|
+
if (r === void 0) return B.warn("db plan: no schema files found", {
|
|
849
956
|
root: n,
|
|
850
957
|
hint: "looked for *.entity.ts / *.schema.ts / schema.ts"
|
|
851
958
|
}), 1;
|
|
@@ -855,115 +962,115 @@ var k = class extends Error {
|
|
|
855
962
|
a && (o.authorization = `Bearer ${a}`);
|
|
856
963
|
try {
|
|
857
964
|
let e = n.endsWith("/_voltro/inspect/migrations") ? n : `${n.replace(/\/+$/, "")}/_voltro/inspect/migrations`, i = await fetch(e, { headers: o });
|
|
858
|
-
if (!i.ok) return
|
|
965
|
+
if (!i.ok) return B.error(`db plan --against: remote returned ${i.status}`, { url: e }), 1;
|
|
859
966
|
let a = (await i.json()).drift?.liveSnapshot;
|
|
860
|
-
if (!a) return
|
|
861
|
-
let s =
|
|
967
|
+
if (!a) return B.error("db plan --against: remote inspect payload missing drift.liveSnapshot", { url: e }), 1;
|
|
968
|
+
let s = E({
|
|
862
969
|
declared: r,
|
|
863
970
|
live: a,
|
|
864
971
|
dialect: W(),
|
|
865
|
-
...
|
|
972
|
+
...T(),
|
|
866
973
|
...t === void 0 ? {} : { onlineAfter: t }
|
|
867
974
|
});
|
|
868
975
|
return console.log(`\n[--against ${e}] — diff vs remote live schema:\n`), Z(s), s.summary.blocked > 0 ? 2 : 0;
|
|
869
976
|
} catch (e) {
|
|
870
|
-
return
|
|
977
|
+
return B.error(`db plan --against: ${e.message}`), 1;
|
|
871
978
|
}
|
|
872
979
|
}
|
|
873
|
-
let a =
|
|
980
|
+
let a = E({
|
|
874
981
|
declared: r,
|
|
875
982
|
live: await Y(await q()),
|
|
876
983
|
dialect: W(),
|
|
877
|
-
...
|
|
984
|
+
...T(),
|
|
878
985
|
...t === void 0 ? {} : { onlineAfter: t }
|
|
879
986
|
});
|
|
880
987
|
return e.includes("--json") ? (y(1, `${JSON.stringify(a, null, 2)}\n`), a.summary.blocked > 0 ? 2 : 0) : (Z(a), a.summary.blocked > 0 ? 2 : 0);
|
|
881
|
-
},
|
|
988
|
+
}, St = async (e, t, n) => {
|
|
882
989
|
let r;
|
|
883
990
|
try {
|
|
884
991
|
let t = await v.readFile(m(e), "utf8");
|
|
885
992
|
r = JSON.parse(t);
|
|
886
993
|
} catch (t) {
|
|
887
|
-
return
|
|
994
|
+
return B.error(`db apply --plan: cannot read/parse plan file '${e}': ${t.message}`), 1;
|
|
888
995
|
}
|
|
889
|
-
if (!Array.isArray(r?.operations) || typeof r?.fromFingerprint != "string" || typeof r?.toFingerprint != "string") return
|
|
996
|
+
if (!Array.isArray(r?.operations) || typeof r?.fromFingerprint != "string" || typeof r?.toFingerprint != "string") return B.error(`db apply --plan: '${e}' is not a valid plan file (expected the output of \`voltro db plan --json\`).`), 1;
|
|
890
997
|
let i = await Q(n);
|
|
891
|
-
if (i === void 0) return
|
|
998
|
+
if (i === void 0) return B.warn("db apply --plan: no schema files found", { root: n }), 1;
|
|
892
999
|
let a = await q();
|
|
893
|
-
await J(a, w(
|
|
894
|
-
let o = await J(a,
|
|
895
|
-
if (o.length > 0) return
|
|
896
|
-
let s =
|
|
1000
|
+
await J(a, w(S, W()));
|
|
1001
|
+
let o = await J(a, x.SqlClient.pipe(g.flatMap((e) => me(e, n)))).catch(() => []);
|
|
1002
|
+
if (o.length > 0) return B.error(`db apply --plan: refusing — ${o.length} pending file-based migration(s).`), console.error(` ${o.join("\n ")}`), console.error(""), console.error(" These perform the changes a state diff cannot infer, so they change the shape"), console.error(" this plan was computed against. Apply them and regenerate the plan:"), console.error(""), console.error(" voltro db files ."), console.error(" voltro db plan --json > plan.json"), console.error(" voltro db apply --plan plan.json"), 1;
|
|
1003
|
+
let s = E({
|
|
897
1004
|
declared: i,
|
|
898
1005
|
live: await Y(a),
|
|
899
1006
|
dialect: W(),
|
|
900
|
-
...
|
|
1007
|
+
...T()
|
|
901
1008
|
});
|
|
902
|
-
if (s.fromFingerprint !== r.fromFingerprint) return
|
|
903
|
-
if (s.toFingerprint !== r.toFingerprint) return
|
|
904
|
-
let c =
|
|
905
|
-
if (u.summary.blocked > 0 && !c.acknowledged) return Z(u),
|
|
906
|
-
if (c.acknowledged &&
|
|
907
|
-
if (u.operations.length === 0) return
|
|
1009
|
+
if (s.fromFingerprint !== r.fromFingerprint) return B.error("db apply --plan: refusing — the live schema has drifted since this plan was generated."), console.error(` plan.from=${r.fromFingerprint.slice(0, 16)} live=${s.fromFingerprint.slice(0, 16)}`), console.error(" Regenerate against the current live schema: `voltro db plan --json > plan.json`"), 2;
|
|
1010
|
+
if (s.toFingerprint !== r.toFingerprint) return B.error("db apply --plan: refusing — the declared schema differs from what this plan targets."), console.error(` plan.to=${r.toFingerprint.slice(0, 16)} declared=${s.toFingerprint.slice(0, 16)}`), console.error(" The schema files changed since the plan was generated. Regenerate the plan."), 2;
|
|
1011
|
+
let c = mt(s), u = c.plan;
|
|
1012
|
+
if (u.summary.blocked > 0 && !c.acknowledged) return Z(u), B.error(`db apply --plan: refusing — ${s.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (B.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), B.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
|
|
1013
|
+
if (c.acknowledged && B.warn("db apply --plan: proceeding with acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)"), yt(u, t)) return 2;
|
|
1014
|
+
if (u.operations.length === 0) return B.info("db apply --plan: schema is up to date — no DDL to apply (change triggers are reconciled separately below)"), await X(i, a), 0;
|
|
908
1015
|
Z(u);
|
|
909
|
-
let d = (process.env.NODE_ENV ?? "dev").toLowerCase(), f = l(t, "--note"), p = await J(a,
|
|
1016
|
+
let d = (process.env.NODE_ENV ?? "dev").toLowerCase(), f = l(t, "--note"), p = await J(a, x.SqlClient.pipe(g.flatMap((e) => se(e, u, {
|
|
910
1017
|
declared: i,
|
|
911
1018
|
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:plan",
|
|
912
1019
|
environment: d === "production" ? "prod" : d === "staging" ? "staging" : "dev",
|
|
913
1020
|
source: "file",
|
|
914
1021
|
...f ? { notes: f } : {},
|
|
915
|
-
replan: () => g.promise(() => Y(a)).pipe(g.map((e) =>
|
|
1022
|
+
replan: () => g.promise(() => Y(a)).pipe(g.map((e) => E({
|
|
916
1023
|
declared: i,
|
|
917
1024
|
live: e,
|
|
918
1025
|
dialect: W(),
|
|
919
|
-
...
|
|
1026
|
+
...T()
|
|
920
1027
|
})))
|
|
921
1028
|
}))));
|
|
922
|
-
return
|
|
1029
|
+
return B.info(`db apply --plan: applied ${p.appliedOps} op(s) in ${p.durationMs}ms`, {
|
|
923
1030
|
id: p.id,
|
|
924
1031
|
fingerprint: p.fingerprint.slice(0, 16)
|
|
925
1032
|
}), await X(i, a), 0;
|
|
926
|
-
},
|
|
927
|
-
let t =
|
|
928
|
-
if (r) return
|
|
929
|
-
if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return
|
|
1033
|
+
}, Ct = async (e) => {
|
|
1034
|
+
let t = H(e), n = bt(e), r = l(e, "--plan");
|
|
1035
|
+
if (r) return St(r, e, t);
|
|
1036
|
+
if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return B.error("voltro db apply: refusing — auto-apply on prod is not allowed."), console.error("In production, schema changes go through `voltro db plan --json > plan.json`"), console.error("then `voltro db apply --plan plan.json` as an explicit step in your deploy pipeline."), 3;
|
|
930
1037
|
let i = await Q(t);
|
|
931
|
-
if (i === void 0) return
|
|
932
|
-
let a = await q(), o = await
|
|
933
|
-
if ("failed" in o) return
|
|
934
|
-
await J(a, w(
|
|
935
|
-
let s =
|
|
1038
|
+
if (i === void 0) return B.warn("db apply: no schema files found", { root: t }), 1;
|
|
1039
|
+
let a = await q(), o = await Mt(t, a);
|
|
1040
|
+
if ("failed" in o) return B.error("db apply: a file-based migration FAILED — the schema diff was NOT run"), console.error(o.failed), console.error("Fix the migration and re-run. `voltro db files` runs them alone."), 1;
|
|
1041
|
+
await J(a, w(S, W()));
|
|
1042
|
+
let s = E({
|
|
936
1043
|
declared: i,
|
|
937
1044
|
live: await Y(a),
|
|
938
1045
|
dialect: W(),
|
|
939
|
-
...
|
|
1046
|
+
...T(),
|
|
940
1047
|
...n === void 0 ? {} : { onlineAfter: n }
|
|
941
1048
|
});
|
|
942
|
-
Z(s), await
|
|
943
|
-
let c =
|
|
944
|
-
if (u.summary.blocked > 0 && !c.acknowledged) return
|
|
945
|
-
if (c.acknowledged &&
|
|
946
|
-
if (u.operations.length === 0) return
|
|
947
|
-
let d = l(e, "--note"), f = await J(a,
|
|
1049
|
+
Z(s), await _t(s, a);
|
|
1050
|
+
let c = mt(s), u = c.plan;
|
|
1051
|
+
if (u.summary.blocked > 0 && !c.acknowledged) return B.error(`db apply: refusing — ${u.summary.blocked} blocked operation(s). See the fix hints above.`), process.env.VOLTRO_DESTRUCTIVE_OK !== void 0 && (B.error(" VOLTRO_DESTRUCTIVE_OK is set but did not cover this plan — it only relaxes"), B.error(" `lossy` operations, and only the tables it names. Anything else stays refused.")), 2;
|
|
1052
|
+
if (c.acknowledged && B.warn(`db apply: proceeding with ${s.summary.blocked} acknowledged lossy operation(s) (VOLTRO_DESTRUCTIVE_OK)`), yt(u, e)) return 2;
|
|
1053
|
+
if (u.operations.length === 0) return B.info("db apply: schema is up to date — no DDL to apply (change triggers are reconciled separately below)"), await Ot(a), l(e, "--note") !== void 0 && (B.warn("db apply: --note was ignored — an empty plan writes no history row to attach it to."), console.error("To record that the CURRENT live schema is the accepted baseline, use `voltro db drift --accept`.")), await X(i, a), 0;
|
|
1054
|
+
let d = l(e, "--note"), f = await J(a, x.SqlClient.pipe(g.flatMap((e) => se(e, u, {
|
|
948
1055
|
declared: i,
|
|
949
1056
|
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user",
|
|
950
1057
|
environment: (process.env.NODE_ENV ?? "dev").toLowerCase() === "staging" ? "staging" : "dev",
|
|
951
1058
|
source: "auto-diff",
|
|
952
1059
|
...d ? { notes: d } : {},
|
|
953
|
-
replan: () => g.promise(() => Y(a)).pipe(g.map((e) =>
|
|
1060
|
+
replan: () => g.promise(() => Y(a)).pipe(g.map((e) => E({
|
|
954
1061
|
declared: i,
|
|
955
1062
|
live: e,
|
|
956
1063
|
dialect: W(),
|
|
957
|
-
...
|
|
1064
|
+
...T(),
|
|
958
1065
|
...n === void 0 ? {} : { onlineAfter: n }
|
|
959
1066
|
})))
|
|
960
1067
|
}))));
|
|
961
|
-
return
|
|
1068
|
+
return B.info(`db apply: applied ${f.appliedOps} op(s) in ${f.durationMs}ms`, {
|
|
962
1069
|
id: f.id,
|
|
963
1070
|
fingerprint: f.fingerprint.slice(0, 16)
|
|
964
1071
|
}), await X(i, a), 0;
|
|
965
|
-
},
|
|
966
|
-
let t = Math.max(1, Number(l(e, "--limit") ?? "20")), n = await J(await q(),
|
|
1072
|
+
}, wt = async (e) => {
|
|
1073
|
+
let t = Math.max(1, Number(l(e, "--limit") ?? "20")), n = await J(await q(), x.SqlClient.pipe(g.flatMap((e) => e`
|
|
967
1074
|
SELECT ${e("id")}, ${e("fingerprint")}, ${e("operations")},
|
|
968
1075
|
${e("appliedBy")}, ${e("environment")}, ${e("source")},
|
|
969
1076
|
${e("durationMs")}, ${e("notes")}, ${e("appliedAt")}
|
|
@@ -971,25 +1078,25 @@ var k = class extends Error {
|
|
|
971
1078
|
ORDER BY ${e("appliedAt")} DESC
|
|
972
1079
|
LIMIT ${t}
|
|
973
1080
|
`)));
|
|
974
|
-
if (n.length === 0) return
|
|
1081
|
+
if (n.length === 0) return B.info("db plans: no planner-based migrations applied yet"), console.log("Run `voltro db apply` after a schema change to start a plan history."), 0;
|
|
975
1082
|
console.log(`\n${n.length} plan${n.length === 1 ? "" : "s"} (newest first):\n`);
|
|
976
1083
|
for (let e of n) {
|
|
977
1084
|
let t = 0;
|
|
978
1085
|
try {
|
|
979
|
-
t = JSON.parse(
|
|
1086
|
+
t = JSON.parse(kt(e.operations)).length;
|
|
980
1087
|
} catch {}
|
|
981
|
-
console.log(` ${e.id} fp=${
|
|
1088
|
+
console.log(` ${e.id} fp=${D(e.fingerprint)} env=${e.environment} src=${e.source} ${t} op(s) ${e.durationMs}ms ${$(e.appliedAt)} by=${e.appliedBy}`), e.notes && console.log(` [2mnote: ${e.notes}[0m`);
|
|
982
1089
|
}
|
|
983
1090
|
return console.log(), 0;
|
|
984
|
-
},
|
|
1091
|
+
}, Tt = async (e) => {
|
|
985
1092
|
let t = e[0];
|
|
986
1093
|
if (!t) return console.error("voltro db restore-snapshot <plan-id>"), console.error(" Renames soft-dropped columns/tables from a plan back to their original names."), console.error(" Requires the plan to have been applied with VOLTRO_SOFT_DROP=1."), 2;
|
|
987
|
-
let n = await q(), r = await J(n,
|
|
1094
|
+
let n = await q(), r = await J(n, x.SqlClient.pipe(g.flatMap((e) => e`SELECT ${e("id")}, ${e("operations")}
|
|
988
1095
|
FROM ${e("_voltro_migration_plans")} WHERE ${e("id")} = ${t}`)));
|
|
989
1096
|
if (r.length === 0) return console.error(`restore-snapshot: plan '${t}' not found`), 1;
|
|
990
1097
|
let i;
|
|
991
1098
|
try {
|
|
992
|
-
i = JSON.parse(
|
|
1099
|
+
i = JSON.parse(kt(r[0].operations));
|
|
993
1100
|
} catch {
|
|
994
1101
|
return console.error(`restore-snapshot: plan '${t}' has corrupted operations JSON`), 1;
|
|
995
1102
|
}
|
|
@@ -1003,7 +1110,7 @@ var k = class extends Error {
|
|
|
1003
1110
|
if (a.length === 0 && o.length === 0) return console.log(`restore-snapshot: plan '${t}' has no drop-column / drop-table ops to restore`), 0;
|
|
1004
1111
|
let s = 0;
|
|
1005
1112
|
for (let e of a) {
|
|
1006
|
-
let t = await J(n,
|
|
1113
|
+
let t = await J(n, x.SqlClient.pipe(g.flatMap((t) => t`
|
|
1007
1114
|
SELECT column_name FROM information_schema.columns
|
|
1008
1115
|
WHERE ${t.unsafe(K(W()))}
|
|
1009
1116
|
AND table_name = ${e.table}
|
|
@@ -1015,11 +1122,11 @@ var k = class extends Error {
|
|
|
1015
1122
|
continue;
|
|
1016
1123
|
}
|
|
1017
1124
|
let r = t[0].column_name;
|
|
1018
|
-
await J(n,
|
|
1125
|
+
await J(n, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table)} RENAME COLUMN ${G(r)} TO ${G(e.column)}`)))), s++, console.log(` ↩ restored ${e.table}.${e.column} (from ${r})`);
|
|
1019
1126
|
}
|
|
1020
1127
|
let c = 0;
|
|
1021
1128
|
for (let e of o) {
|
|
1022
|
-
let t = await J(n,
|
|
1129
|
+
let t = await J(n, x.SqlClient.pipe(g.flatMap((t) => t`
|
|
1023
1130
|
SELECT table_name FROM information_schema.tables
|
|
1024
1131
|
WHERE ${t.unsafe(K(W()))}
|
|
1025
1132
|
AND table_name LIKE ${e.table + "__dropped_%"}
|
|
@@ -1030,18 +1137,18 @@ var k = class extends Error {
|
|
|
1030
1137
|
continue;
|
|
1031
1138
|
}
|
|
1032
1139
|
let r = t[0].table_name;
|
|
1033
|
-
await J(n,
|
|
1140
|
+
await J(n, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(r)} RENAME TO ${G(e.table)}`)))), c++, console.log(` ↩ restored table ${e.table} (from ${r})`);
|
|
1034
1141
|
}
|
|
1035
1142
|
return console.log(`\n✓ restored ${s} column(s) + ${c} table(s) from plan '${t}'\n`), 0;
|
|
1036
|
-
},
|
|
1143
|
+
}, Et = async (e) => {
|
|
1037
1144
|
let t = l(e, "--before");
|
|
1038
1145
|
if (!t) return console.error("voltro db gc-snapshots --before <date> [--dry-run]"), console.error(" Permanently drops soft-dropped columns/tables (<name>__dropped_<ts>) older than <date>."), console.error(" <date> is ISO — e.g. 2026-01-01 or 2026-01-01T00:00:00Z. NOT reversible."), 2;
|
|
1039
1146
|
let n = t.replace(/[^0-9]/g, "");
|
|
1040
1147
|
if (n.length < 8) return console.error(`gc-snapshots: --before must be at least a full date (YYYY-MM-DD), got "${t}"`), 2;
|
|
1041
|
-
let r = (n + "00000000000000").slice(0, 14), i = e.includes("--dry-run"), a = await q(), o = await J(a,
|
|
1148
|
+
let r = (n + "00000000000000").slice(0, 14), i = e.includes("--dry-run"), a = await q(), o = await J(a, x.SqlClient.pipe(g.flatMap((e) => e`
|
|
1042
1149
|
SELECT table_name, column_name FROM information_schema.columns
|
|
1043
1150
|
WHERE ${e.unsafe(K(W()))} AND column_name LIKE ${"%__dropped_%"}
|
|
1044
|
-
`))), s = await J(a,
|
|
1151
|
+
`))), s = await J(a, x.SqlClient.pipe(g.flatMap((e) => e`
|
|
1045
1152
|
SELECT table_name FROM information_schema.tables
|
|
1046
1153
|
WHERE ${e.unsafe(K(W()))} AND table_name LIKE ${"%__dropped_%"}
|
|
1047
1154
|
`))), c = (e) => {
|
|
@@ -1050,37 +1157,37 @@ var k = class extends Error {
|
|
|
1050
1157
|
}, u = 0;
|
|
1051
1158
|
for (let e of o) {
|
|
1052
1159
|
let t = c(e.column_name);
|
|
1053
|
-
t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop column" : "dropping column"} ${e.table_name}.${e.column_name}`), i || await J(a,
|
|
1160
|
+
t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop column" : "dropping column"} ${e.table_name}.${e.column_name}`), i || await J(a, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`ALTER TABLE ${G(e.table_name)} DROP COLUMN ${G(e.column_name)}`)))), u++);
|
|
1054
1161
|
}
|
|
1055
1162
|
for (let e of s) {
|
|
1056
1163
|
let t = c(e.table_name);
|
|
1057
|
-
t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop table" : "dropping table"} ${e.table_name}`), i || await J(a,
|
|
1164
|
+
t === null || t >= r || (console.log(` ${i ? "[dry-run] would drop table" : "dropping table"} ${e.table_name}`), i || await J(a, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(`DROP TABLE ${G(e.table_name)}`)))), u++);
|
|
1058
1165
|
}
|
|
1059
1166
|
return console.log(`\n${i ? `[dry-run] ${u} snapshot(s) would be dropped` : `✓ dropped ${u} snapshot(s)`} (older than ${r}).\n`), 0;
|
|
1060
|
-
},
|
|
1167
|
+
}, Dt = async (e) => {
|
|
1061
1168
|
let t = l(e, "--before");
|
|
1062
1169
|
if (!t) return console.error("voltro db squash --before <iso-date> [--note \"<text>\"]"), 2;
|
|
1063
1170
|
let n = t;
|
|
1064
1171
|
if (Number.isNaN(new Date(n).getTime())) return console.error(`voltro db squash: --before '${n}' is not a valid ISO date`), 2;
|
|
1065
|
-
let r = l(e, "--note"), i =
|
|
1066
|
-
if (a === void 0) return
|
|
1067
|
-
let o =
|
|
1172
|
+
let r = l(e, "--note"), i = H(e), a = await Q(i);
|
|
1173
|
+
if (a === void 0) return B.warn("db squash: no schema files found", { root: i }), 1;
|
|
1174
|
+
let o = fe(le(a)), s = await q();
|
|
1068
1175
|
try {
|
|
1069
|
-
let e = await J(s,
|
|
1176
|
+
let e = await J(s, ye({
|
|
1070
1177
|
before: n,
|
|
1071
1178
|
fingerprint: o,
|
|
1072
1179
|
appliedBy: process.env.USER ?? "cli",
|
|
1073
1180
|
environment: process.env.NODE_ENV === "production" ? "prod" : "dev",
|
|
1074
1181
|
...r === void 0 ? {} : { note: r }
|
|
1075
1182
|
}));
|
|
1076
|
-
return console.log(`\n✓ squashed ${e.squashedCount} migration plan(s)\n snapshot id: ${e.snapshotId}\n fingerprint: ${
|
|
1183
|
+
return console.log(`\n✓ squashed ${e.squashedCount} migration plan(s)\n snapshot id: ${e.snapshotId}\n fingerprint: ${D(e.snapshotFingerprint)}\n`), 0;
|
|
1077
1184
|
} catch (e) {
|
|
1078
1185
|
return console.error(`squash refused: ${e.message}`), 1;
|
|
1079
1186
|
}
|
|
1080
|
-
},
|
|
1187
|
+
}, Ot = async (e) => {
|
|
1081
1188
|
try {
|
|
1082
|
-
let t =
|
|
1083
|
-
await J(e,
|
|
1189
|
+
let t = fe(await Y(e));
|
|
1190
|
+
await J(e, x.SqlClient.pipe(g.flatMap((e) => e`
|
|
1084
1191
|
UPDATE ${e("_voltro_migration_plans")}
|
|
1085
1192
|
SET ${e("liveFingerprint")} = ${t}
|
|
1086
1193
|
WHERE ${e("id")} = (
|
|
@@ -1092,7 +1199,7 @@ var k = class extends Error {
|
|
|
1092
1199
|
AND ${e("liveFingerprint")} IS NULL
|
|
1093
1200
|
`)));
|
|
1094
1201
|
} catch {}
|
|
1095
|
-
},
|
|
1202
|
+
}, kt = (e) => typeof e == "string" ? e : e == null ? "" : JSON.stringify(e), $ = (e) => e instanceof Date ? e.toISOString() : String(e ?? ""), At = (e) => e === void 0 ? {
|
|
1096
1203
|
accept: !1,
|
|
1097
1204
|
reason: "no-declared-schema"
|
|
1098
1205
|
} : e > 0 ? {
|
|
@@ -1101,38 +1208,38 @@ var k = class extends Error {
|
|
|
1101
1208
|
} : {
|
|
1102
1209
|
accept: !0,
|
|
1103
1210
|
reason: "converged"
|
|
1104
|
-
},
|
|
1105
|
-
let t = e.includes("--accept"), n = await q(), r = await J(n,
|
|
1211
|
+
}, jt = async (e) => {
|
|
1212
|
+
let t = e.includes("--accept"), n = await q(), r = await J(n, x.SqlClient.pipe(g.flatMap((e) => e`
|
|
1106
1213
|
SELECT * FROM ${e("_voltro_migration_plans")}
|
|
1107
1214
|
ORDER BY ${e("appliedAt")} DESC
|
|
1108
1215
|
LIMIT 1
|
|
1109
|
-
`))), i = await Y(n), a =
|
|
1110
|
-
if (r.length === 0) return
|
|
1216
|
+
`))), i = await Y(n), a = fe(i);
|
|
1217
|
+
if (r.length === 0) return B.info("db drift: no _voltro_migration_plans rows yet"), console.log(`live user-schema fingerprint: ${D(a)}`), console.log("No baseline to compare against. Run `voltro db apply` to lock in the current state."), 0;
|
|
1111
1218
|
let o = r[0];
|
|
1112
|
-
if (o.liveFingerprint === null || o.liveFingerprint === void 0) return
|
|
1113
|
-
if (o.liveFingerprint === a) return
|
|
1219
|
+
if (o.liveFingerprint === null || o.liveFingerprint === void 0) return B.warn("db drift: no drift baseline recorded yet — cannot compare"), console.log(), console.log(` last applied: ${o.id} at ${$(o.appliedAt)}`), console.log(` live now: ${D(a)}`), console.log(), console.log("That row was written before the framework recorded a live-schema baseline,"), console.log("and the value it does carry is the DECLARED snapshot's hash — not comparable"), console.log("with a live one. Run `voltro db apply` once to establish the baseline (a no-op"), console.log("apply backfills it too); use `voltro db plan` meanwhile, which compares declared"), console.log("against live directly."), console.log(), console.log("Exit code 3 — NOT 0. \"Did not compare\" is not \"clean\", and a gate on the exit"), console.log("code has to be able to tell them apart."), 3;
|
|
1220
|
+
if (o.liveFingerprint === a) return B.info(`db drift: live matches last applied (${D(a)})`), 0;
|
|
1114
1221
|
let s = await (async () => {
|
|
1115
1222
|
try {
|
|
1116
|
-
let t = await Q(
|
|
1117
|
-
return t === void 0 ? void 0 :
|
|
1223
|
+
let t = await Q(H(e));
|
|
1224
|
+
return t === void 0 ? void 0 : E({
|
|
1118
1225
|
declared: t,
|
|
1119
1226
|
live: i,
|
|
1120
1227
|
dialect: W(),
|
|
1121
|
-
...
|
|
1228
|
+
...T()
|
|
1122
1229
|
}).operations;
|
|
1123
1230
|
} catch {
|
|
1124
1231
|
return;
|
|
1125
1232
|
}
|
|
1126
1233
|
})();
|
|
1127
1234
|
if (t) {
|
|
1128
|
-
let e =
|
|
1129
|
-
if (e.reason === "no-declared-schema") return
|
|
1235
|
+
let e = At(s?.length);
|
|
1236
|
+
if (e.reason === "no-declared-schema") return B.error("db drift --accept: no schema files found — cannot verify the live schema is current."), 1;
|
|
1130
1237
|
if (!e.accept && s !== void 0) {
|
|
1131
|
-
|
|
1238
|
+
B.error(`db drift --accept: refusing — ${s.length} operation(s) still pending against your declared schema.`), console.log();
|
|
1132
1239
|
for (let e of s.slice(0, 10)) console.log(` - ${e.op.kind} ${"table" in e.op ? `[${e.op.table}]` : ""}`);
|
|
1133
1240
|
return s.length > 10 && console.log(` … and ${s.length - 10} more`), console.log(), console.log("`--accept` records the CURRENT live schema as the applied baseline. It is for a"), console.log("change that already landed out of band and left the schema correct — not for one"), console.log("that has not landed. Run `voltro db apply` instead; it writes a real baseline."), 4;
|
|
1134
1241
|
}
|
|
1135
|
-
return await J(n,
|
|
1242
|
+
return await J(n, x.SqlClient.pipe(g.flatMap((e) => e`
|
|
1136
1243
|
UPDATE ${e("_voltro_migration_plans")}
|
|
1137
1244
|
SET ${e("liveFingerprint")} = ${a}
|
|
1138
1245
|
WHERE ${e("id")} = (
|
|
@@ -1141,43 +1248,43 @@ var k = class extends Error {
|
|
|
1141
1248
|
ORDER BY ${e("appliedAt")} DESC LIMIT 1
|
|
1142
1249
|
) AS ${e("newest")}
|
|
1143
1250
|
)
|
|
1144
|
-
`))),
|
|
1251
|
+
`))), B.info(`db drift: accepted the live schema as the baseline (${D(a)})`), console.log(), console.log("`voltro db plan` reports 0 operations against this schema, so the live state already"), console.log("matches your declaration — only the ledger did not know. It does now."), console.log(), console.log("No history entry was invented: the latest row's baseline was updated in place,"), console.log("because no migration ran."), 0;
|
|
1145
1252
|
}
|
|
1146
|
-
return
|
|
1147
|
-
},
|
|
1253
|
+
return B.error("db drift: live schema DIVERGED from last applied state"), console.log(), console.log(` baseline: ${D(o.liveFingerprint)} at ${$(o.appliedAt)} (${o.id})`), console.log(` live now: ${D(a)}`), console.log(), console.log("Something changed the live schema after the last apply. This command can see"), console.log("THAT it changed, not what or who — the fingerprints are hashes, not a diff."), console.log(), s === void 0 ? (console.log("No schema files found here, so the declared side could not be compared."), console.log("Run `voltro db plan` from your project root for the actual difference."), 4) : s.length === 0 ? (console.log("Your DECLARED schema is already satisfied — `voltro db plan` reports 0 operations"), console.log("against this database. So the live schema is not wrong, only unrecorded: something"), console.log("applied a change without going through the planner (a hand-run ALTER, a DBA"), console.log("window, a restored dump), or it touched a table your code does not declare."), console.log(), console.log("If that was deliberate and the schema is right, record it:"), console.log(), console.log(" voltro db drift --accept"), console.log(), console.log("It updates the latest ledger row's baseline to the live schema and invents no"), console.log("history entry. Drift then measures from here."), 4) : (console.log(`\`voltro db plan\` reports ${s.length} operation(s) still pending against your`), console.log("declared schema, so this is not merely an unrecorded change — run it for the diff,"), console.log("then `voltro db apply`, which writes a fresh baseline of its own."), 4);
|
|
1254
|
+
}, Mt = async (e, t) => {
|
|
1148
1255
|
let n = (process.env.NODE_ENV ?? "").toLowerCase() === "production" ? "prod" : (process.env.NODE_ENV ?? "").toLowerCase() === "staging" ? "staging" : "dev";
|
|
1149
1256
|
try {
|
|
1150
|
-
let r = await J(t,
|
|
1257
|
+
let r = await J(t, x.SqlClient.pipe(g.flatMap((t) => ve(t, {
|
|
1151
1258
|
projectRoot: e,
|
|
1152
1259
|
env: n,
|
|
1153
1260
|
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
|
|
1154
1261
|
}))));
|
|
1155
|
-
return r.applied.length > 0 &&
|
|
1262
|
+
return r.applied.length > 0 && B.info(`db apply: applied ${r.applied.length} pending file-based migration(s) first`, { applied: r.applied.map((e) => `${e.id} (${e.durationMs}ms)`) }), { applied: r.applied.length };
|
|
1156
1263
|
} catch (e) {
|
|
1157
1264
|
return { failed: e };
|
|
1158
1265
|
}
|
|
1159
|
-
},
|
|
1160
|
-
let t =
|
|
1161
|
-
await J(n, w(
|
|
1162
|
-
let i = await J(n,
|
|
1266
|
+
}, Nt = async (e) => {
|
|
1267
|
+
let t = H(e), n = await q(), r = (process.env.NODE_ENV ?? "").toLowerCase() === "production" ? "prod" : (process.env.NODE_ENV ?? "").toLowerCase() === "staging" ? "staging" : "dev";
|
|
1268
|
+
await J(n, w(S, W()));
|
|
1269
|
+
let i = await J(n, x.SqlClient.pipe(g.flatMap((e) => ve(e, {
|
|
1163
1270
|
projectRoot: t,
|
|
1164
1271
|
env: r,
|
|
1165
1272
|
appliedBy: process.env.USER ? `cli:${process.env.USER}` : "cli:user"
|
|
1166
1273
|
}))));
|
|
1167
|
-
return i.applied.length === 0 ? (
|
|
1274
|
+
return i.applied.length === 0 ? (B.info(`db files: no pending file-based migrations (${i.skipped.length} already applied)`), 0) : (B.info(`db files: applied ${i.applied.length} migration(s)`, {
|
|
1168
1275
|
applied: i.applied.map((e) => `${e.id} (${e.durationMs}ms)`),
|
|
1169
1276
|
skipped: i.skipped.length
|
|
1170
1277
|
}), 0);
|
|
1171
|
-
},
|
|
1172
|
-
let t =
|
|
1173
|
-
if (!n) return
|
|
1174
|
-
if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return
|
|
1175
|
-
let r = await J(await q(),
|
|
1278
|
+
}, Pt = async (e) => {
|
|
1279
|
+
let t = H(e), n = e.find((e) => !e.startsWith("--"));
|
|
1280
|
+
if (!n) return B.error("voltro db rollback-file <migration-id>"), 2;
|
|
1281
|
+
if ((process.env.NODE_ENV ?? "").toLowerCase() === "production") return B.error("voltro db rollback-file: refusing on prod — schema rollback runs as an explicit deploy step."), 3;
|
|
1282
|
+
let r = await J(await q(), x.SqlClient.pipe(g.flatMap((e) => ge(e, {
|
|
1176
1283
|
projectRoot: t,
|
|
1177
1284
|
id: n
|
|
1178
1285
|
}))));
|
|
1179
|
-
return
|
|
1180
|
-
},
|
|
1286
|
+
return B.info(`db rollback-file: rolled back ${r.id} in ${r.durationMs}ms`), 0;
|
|
1287
|
+
}, Ft = () => {
|
|
1181
1288
|
let e = /* @__PURE__ */ new Date();
|
|
1182
1289
|
return [
|
|
1183
1290
|
e.getUTCFullYear(),
|
|
@@ -1187,7 +1294,7 @@ var k = class extends Error {
|
|
|
1187
1294
|
String(e.getUTCMinutes()).padStart(2, "0"),
|
|
1188
1295
|
String(e.getUTCSeconds()).padStart(2, "0")
|
|
1189
1296
|
].join("");
|
|
1190
|
-
},
|
|
1297
|
+
}, It = (e) => e.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration", Lt = (e, t) => `import { defineMigration, SqlClient } from '@voltro/database'
|
|
1191
1298
|
import { Effect } from 'effect'
|
|
1192
1299
|
|
|
1193
1300
|
// A hand-written migration. Reach for this when the DECLARATIVE differ can't
|
|
@@ -1219,24 +1326,24 @@ export default defineMigration({
|
|
|
1219
1326
|
}),
|
|
1220
1327
|
],
|
|
1221
1328
|
})
|
|
1222
|
-
`,
|
|
1223
|
-
plan:
|
|
1224
|
-
apply:
|
|
1225
|
-
plans:
|
|
1226
|
-
drift:
|
|
1227
|
-
files:
|
|
1228
|
-
"rollback-file":
|
|
1229
|
-
migrate:
|
|
1230
|
-
rollback:
|
|
1231
|
-
status:
|
|
1232
|
-
seed:
|
|
1329
|
+
`, Rt = {
|
|
1330
|
+
plan: xt,
|
|
1331
|
+
apply: Ct,
|
|
1332
|
+
plans: wt,
|
|
1333
|
+
drift: jt,
|
|
1334
|
+
files: Nt,
|
|
1335
|
+
"rollback-file": Pt,
|
|
1336
|
+
migrate: at,
|
|
1337
|
+
rollback: ot,
|
|
1338
|
+
status: st,
|
|
1339
|
+
seed: lt,
|
|
1233
1340
|
generate: async (e) => {
|
|
1234
|
-
let { value: t, rest: n } = c(e, "--root"), { value: r, rest: i } = c(n, "--name"), a = t ? m(t) : process.cwd(), o =
|
|
1235
|
-
return await v.mkdir(l, { recursive: !0 }), await v.writeFile(d,
|
|
1341
|
+
let { value: t, rest: n } = c(e, "--root"), { value: r, rest: i } = c(n, "--name"), a = t ? m(t) : process.cwd(), o = It(r ?? u(i) ?? "migration"), s = `${Ft()}_${o}`, l = f(a, "migrations"), d = f(l, `${s}.migration.ts`);
|
|
1342
|
+
return await v.mkdir(l, { recursive: !0 }), await v.writeFile(d, Lt(s, o), "utf8"), B.info(`created ${p(a, d)}`), console.error("Fill in the apply/undo steps, then run `voltro db migrate` (rollback with `voltro db rollback`)."), console.error("For a plain schema shape change you may not need this — `voltro db apply` diffs declared vs live."), 0;
|
|
1236
1343
|
},
|
|
1237
|
-
squash:
|
|
1238
|
-
"restore-snapshot":
|
|
1239
|
-
"gc-snapshots":
|
|
1344
|
+
squash: Dt,
|
|
1345
|
+
"restore-snapshot": Tt,
|
|
1346
|
+
"gc-snapshots": Et,
|
|
1240
1347
|
adopt: async (e) => {
|
|
1241
1348
|
let t = (t) => {
|
|
1242
1349
|
let n = e.indexOf(`--${t}`);
|
|
@@ -1250,18 +1357,18 @@ export default defineMigration({
|
|
|
1250
1357
|
into: r,
|
|
1251
1358
|
map: o.map,
|
|
1252
1359
|
...o.leaveUnset ? { leaveUnset: o.leaveUnset } : {}
|
|
1253
|
-
}, c = W(), l = await q(), u = await J(l,
|
|
1360
|
+
}, c = W(), l = await q(), u = await J(l, x.SqlClient.pipe(g.flatMap(pe))), d = (e) => u.tables.find((t) => t.name === e)?.columns.map((e) => ({
|
|
1254
1361
|
name: e.name,
|
|
1255
1362
|
nullable: e.nullable,
|
|
1256
1363
|
hasDefault: e.defaultValue !== void 0 && e.defaultValue !== null
|
|
1257
1364
|
})), f = d(n), p = d(r);
|
|
1258
1365
|
if (f === void 0) return console.error(`voltro db adopt: '${n}' does not exist in the database`), 1;
|
|
1259
1366
|
if (p === void 0) return console.error(`voltro db adopt: '${r}' does not exist in the database — boot once so the plugin's table is created`), 1;
|
|
1260
|
-
let h = async (e) => await J(l,
|
|
1261
|
-
for (let e of
|
|
1367
|
+
let h = async (e) => await J(l, x.SqlClient.pipe(g.flatMap((t) => t.unsafe(e)))), _ = async (e) => Number(Object.values((await h(`SELECT COUNT(*) AS n FROM "${e}"`))[0] ?? { n: 0 })[0] ?? 0), v = Ce(s, f, p, await _(r));
|
|
1368
|
+
for (let e of Te(s, v, await _(n))) console.log(e);
|
|
1262
1369
|
if (v.fatal) return 1;
|
|
1263
1370
|
if (!e.includes("--apply")) return console.log(""), console.log(" (dry run — nothing written. Re-run with --apply.)"), 0;
|
|
1264
|
-
let y = await
|
|
1371
|
+
let y = await Ae(s, v, c, "id", void 0, h, { keepSource: e.includes("--keep-source") });
|
|
1265
1372
|
return console.log(""), console.log(` copied ${y.copied}/${y.sourceRows} row(s)`), console.log(` snapshot: ${y.snapshotTable} — restore from here, then drop it yourself`), y.verified ? (console.log(y.sourceDropped ? ` dropped ${n}` : ` ${n} left in place (--keep-source)`), 0) : (console.error(" ✗ counts DIFFER — the source was NOT dropped. Compare the two tables before doing anything else."), 1);
|
|
1266
1373
|
},
|
|
1267
1374
|
"scan-credentials": async (e) => {
|
|
@@ -1269,28 +1376,89 @@ export default defineMigration({
|
|
|
1269
1376
|
for (let t = 0; t < e.length; t += 1) {
|
|
1270
1377
|
if (e[t] !== "--table") continue;
|
|
1271
1378
|
let r = e[t + 1];
|
|
1272
|
-
if (r === void 0) throw new
|
|
1379
|
+
if (r === void 0) throw new O("voltro db scan-credentials: --table needs a value");
|
|
1273
1380
|
let [i, a = "subject"] = r.split(":");
|
|
1274
1381
|
n.push({
|
|
1275
1382
|
table: i,
|
|
1276
1383
|
column: a
|
|
1277
1384
|
});
|
|
1278
1385
|
}
|
|
1279
|
-
let r = [...
|
|
1280
|
-
return console.log(
|
|
1386
|
+
let r = [...je, ...n], i = await J(await q(), x.SqlClient.pipe(g.flatMap((e) => g.promise(() => ze(e, t, r, G)))));
|
|
1387
|
+
return console.log(Be(i, t)), Ve(i);
|
|
1388
|
+
},
|
|
1389
|
+
"encrypt-column": async (e) => {
|
|
1390
|
+
let t = W(), n = e.includes("--dry-run"), r = e.includes("--yes"), i = (t) => {
|
|
1391
|
+
let n = e.indexOf(t);
|
|
1392
|
+
return n >= 0 ? e[n + 1] : void 0;
|
|
1393
|
+
}, a = Math.max(1, Number(i("--batch") ?? 500)), o = i("--key-env") ?? "FIELD_ENCRYPTION_KEY", s = process.env[o];
|
|
1394
|
+
if (!s) throw new O(`voltro db encrypt-column: ${o} is not set. It must be the SAME key your app runs with (\`governancePlugin({ fieldEncryption: { key } })\`) — encrypting with a different one produces rows the app cannot read. Use --key-env to name a different variable.`);
|
|
1395
|
+
let c = ee(s), l = e.filter((e) => !e.startsWith("--") && e.includes(".")).map(Ke);
|
|
1396
|
+
if (l.length === 0) throw new O("voltro db encrypt-column: name at least one <table>.<column>");
|
|
1397
|
+
let u = await q(), d = [];
|
|
1398
|
+
for (let e of l) {
|
|
1399
|
+
let i = G(e.table), s = G(e.column), l = await J(u, x.SqlClient.pipe(g.flatMap((n) => g.promise(async () => {
|
|
1400
|
+
let r = await g.runPromise(n.unsafe(`SELECT character_maximum_length AS n FROM information_schema.columns\n WHERE ${K(t)} AND table_name = '${e.table}' AND column_name = '${e.column}'`));
|
|
1401
|
+
if (r.length === 0) throw new O(`voltro db encrypt-column: ${e.table}.${e.column} does not exist on this database`);
|
|
1402
|
+
let i = Object.values(r[0])[0];
|
|
1403
|
+
return i == null ? void 0 : Number(i);
|
|
1404
|
+
})))), f = await J(u, x.SqlClient.pipe(g.flatMap((e) => g.promise(async () => await g.runPromise(e.unsafe(`SELECT ${G("id")} AS id, ${s} AS v FROM ${i}`)))))), p = 0;
|
|
1405
|
+
for (let e of f) typeof e.v != "string" || C(e.v) || (p = Math.max(p, Qe(e.v)));
|
|
1406
|
+
let m = Je(p, l);
|
|
1407
|
+
if (!m.fits) return console.error(` ✗ ${Ye(e, m)}`), 1;
|
|
1408
|
+
if ($e(f.map((e) => e.v).find((e) => typeof e == "string" && C(e)), c) === !1) return console.error(` ✗ ${et(e, o)}`), 1;
|
|
1409
|
+
let h = f.filter((t) => Ze(t.v, c, e) !== void 0).length;
|
|
1410
|
+
if (!n && !r) return console.error(` ✗ ${e.table}.${e.column}: ${h} row(s) would be rewritten in place. Re-run with --yes, or --dry-run to see the counts without writing.`), 1;
|
|
1411
|
+
let _ = 0, v = 0, y = 0, b = [];
|
|
1412
|
+
for (let t of f) {
|
|
1413
|
+
if (t.v === null || t.v === void 0 || t.v === "") {
|
|
1414
|
+
v += 1;
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (typeof t.v == "string" && C(t.v)) {
|
|
1418
|
+
_ += 1;
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
let n = Ze(t.v, c, e);
|
|
1422
|
+
if (n === void 0) {
|
|
1423
|
+
v += 1;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
b.push({
|
|
1427
|
+
id: t.id,
|
|
1428
|
+
value: n
|
|
1429
|
+
}), y += 1;
|
|
1430
|
+
}
|
|
1431
|
+
if (!n && b.length > 0) for (let t = 0; t < b.length; t += a) {
|
|
1432
|
+
let n = b.slice(t, t + a);
|
|
1433
|
+
await J(u, x.SqlClient.pipe(g.flatMap((t) => g.promise(async () => {
|
|
1434
|
+
for (let r of n) await g.runPromise(t`UPDATE ${t(e.table)} SET ${t(e.column)} = ${r.value} WHERE ${t("id")} = ${r.id}`);
|
|
1435
|
+
})))), B.info(`encrypt-column ${e.table}.${e.column}: ${Math.min(t + n.length, b.length)}/${b.length}`);
|
|
1436
|
+
}
|
|
1437
|
+
d.push({
|
|
1438
|
+
table: e.table,
|
|
1439
|
+
column: e.column,
|
|
1440
|
+
scanned: f.length,
|
|
1441
|
+
alreadyEncrypted: _,
|
|
1442
|
+
empty: v,
|
|
1443
|
+
encrypted: y
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
console.log(`field encryption · dialect ${t}${n ? " · DRY RUN, nothing written" : ""}`), console.log("");
|
|
1447
|
+
for (let e of d) console.log(Xe(e, n));
|
|
1448
|
+
return n || console.log(tt(l)), 0;
|
|
1281
1449
|
}
|
|
1282
|
-
},
|
|
1450
|
+
}, zt = async (e) => {
|
|
1283
1451
|
let [t, ...n] = e;
|
|
1284
1452
|
if (!t) {
|
|
1285
1453
|
console.error("voltro db <subcommand> [args]"), console.error("subcommands:");
|
|
1286
|
-
for (let e of Object.keys(
|
|
1454
|
+
for (let e of Object.keys(Rt)) console.error(` ${e}`);
|
|
1287
1455
|
return 1;
|
|
1288
1456
|
}
|
|
1289
|
-
let r =
|
|
1457
|
+
let r = Rt[t];
|
|
1290
1458
|
if (!r) return console.error(`voltro db: unknown subcommand '${t}'`), 2;
|
|
1291
|
-
(process.env.DB_DIRECT_URL !== void 0 || process.env.DB_MIGRATE_URL !== void 0) &&
|
|
1459
|
+
(process.env.DB_DIRECT_URL !== void 0 || process.env.DB_MIGRATE_URL !== void 0) && B.info("using a DIRECT (non-pooler) connection for the db-command path (DB_DIRECT_URL/DB_MIGRATE_URL set)");
|
|
1292
1460
|
let i = (e) => {
|
|
1293
|
-
|
|
1461
|
+
rt(e) ? B.error(it) : console.error(e), process.exit(1);
|
|
1294
1462
|
};
|
|
1295
1463
|
process.on("uncaughtException", i);
|
|
1296
1464
|
try {
|
|
@@ -1300,4 +1468,4 @@ export default defineMigration({
|
|
|
1300
1468
|
}
|
|
1301
1469
|
};
|
|
1302
1470
|
//#endregion
|
|
1303
|
-
export {
|
|
1471
|
+
export { ft as a, O as c, At as i, xe as l, U as n, rt as o, dt as r, zt as s, it as t };
|