@voltro/cli 0.45.0 → 0.46.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 +176 -0
- package/dist/{apiBuild-CDQBNRDx.js → apiBuild-BKdd0I1Z.js} +2 -2
- package/dist/apiBuild-P2gewrfQ.js +2 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.js +1 -1
- package/dist/{build-yvsLG77U.js → build-co6yadYz.js} +7 -7
- package/dist/{checkCommand-Boxov8lr.js → checkCommand-B-eiW-G_.js} +3 -3
- package/dist/{checkCommand-lrzwOzV1.js → checkCommand-CXAXTjZL.js} +1 -1
- package/dist/{codegenCommand-CQOLytL-.js → codegenCommand-CB6_3sX0.js} +15 -15
- package/dist/{codemodRunner-5ZhDH3rB.js → codemodRunner-DSZjgyCL.js} +8 -0
- package/dist/{commands-CEkVoFM5.js → commands-C-hxgAGr.js} +23 -23
- package/dist/{dashboardCommand-0ahyp-zA.js → dashboardCommand-CEVO0GWF.js} +3 -3
- package/dist/{dataCommand-D4IlZcih.js → dataCommand-k0ezbQWa.js} +271 -230
- package/dist/{dbCommand-B237qI-j.js → dbCommand-BwkyNxn-.js} +110 -110
- package/dist/dbCommand-C2XK63Dd.js +2 -0
- package/dist/{dev-DAM5CnAa.js → dev-8hTEnN6s.js} +2080 -2057
- package/dist/{dev-P4HyeY9W.js → dev-DyoYst4j.js} +1 -1
- package/dist/devActivity.d.ts +8 -0
- package/dist/{doctorCommand-DW2cwAvC.js → doctorCommand-B6D7HH1-.js} +14 -14
- package/dist/doctorCommand-Btl2Jz1a.js +2 -0
- package/dist/{dormancyCommand-BqQz7lbM.js → dormancyCommand-CrPAwOgo.js} +1 -1
- package/dist/{embeddingsCommand-DJyArpU4.js → embeddingsCommand-DsSxsn_b.js} +1 -1
- package/dist/{envCommand-CviRZx1L.js → envCommand-B_NPVGUf.js} +2 -2
- package/dist/{evalCommand-6RUfPen4.js → evalCommand-Db01Kbng.js} +24 -24
- package/dist/{evolveCommand-qdlP-R9G.js → evolveCommand-DUuaCIyn.js} +2 -2
- package/dist/{frameworkTableAssembly-oMPBKqlE.js → frameworkTableAssembly-BPlPfU07.js} +82 -78
- package/dist/frameworkTableAssembly-Bob3vj9R.js +2 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -1
- package/dist/{infoCommand-CNWPSUcW.js → infoCommand-CuNl9cbh.js} +1 -1
- package/dist/inspectState.d.ts +8 -0
- package/dist/interruptedReplace-C1ZI84xi.js +27 -0
- package/dist/interruptedReplace-DNJ7MKph.js +2 -0
- package/dist/{migrate-DN47p2PG.js → migrate-CM1qwalu.js} +27 -27
- package/dist/{runtimeTrace-BDZSt9W_.js → runtimeTrace-BRNM06vK.js} +1 -1
- package/dist/{sdkgen-sNaD3oZB.js → sdkgen-CRCw_XcR.js} +1 -1
- package/dist/{seedRunner-IdHEprqf.js → seedRunner-Dgsiwk_e.js} +59 -59
- package/dist/serveCommand-BNHMVXyp.js +2 -0
- package/dist/serveCommand-CHOr74WM.js +1928 -0
- package/dist/serveEntry.d.ts +8 -0
- package/dist/serveEntry.js +1 -1
- package/dist/{start-Ds3xyn8R.js → start-7HcSIKEp.js} +1 -1
- package/dist/{start-BAlGcjF5.js → start-DvlYDyAr.js} +1 -1
- package/dist/startEntry.d.ts +8 -0
- package/dist/startEntry.js +1 -1
- package/dist/startup.d.ts +8 -0
- package/dist/{updateCommand-rCnhaj84.js → updateCommand-BlyXavoG.js} +1 -1
- package/dist/updateCommand-CT5AvVg7.js +2 -0
- package/dist/{webDev-101csg6Q.js → webDev-B4IEMB0n.js} +6 -6
- package/dist/webDev-B79ji8M5.js +2 -0
- package/dist/{webhooksCommand-C0iZZfaI.js → webhooksCommand-BRrG_sGf.js} +1 -1
- package/package.json +23 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +138 -2
- package/templates/agent-docs/data.md +70 -4
- package/templates/agent-docs/whats-new.md +127 -54
- 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 +9 -9
- 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 +10 -10
- 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 +4 -4
- package/dist/apiBuild-yJCqJYQ8.js +0 -2
- package/dist/dbCommand-BvQeKQOb.js +0 -2
- package/dist/doctorCommand-2K1dIzYk.js +0 -2
- package/dist/frameworkTableAssembly-DgylYh9_.js +0 -2
- package/dist/serveCommand-BBhT2U94.js +0 -1885
- package/dist/serveCommand-DY2jLy0o.js +0 -2
- package/dist/updateCommand-BH6EGuCc.js +0 -2
- package/dist/webDev-BLKBO34K.js +0 -2
package/dist/serveEntry.d.ts
CHANGED
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as e } from "./loadEnv-D9nEOClM.js";
|
|
2
2
|
import { r as t } from "./appModuleLoader-C9r9mxZt.js";
|
|
3
3
|
import { i as n } from "./dialectDriver-czCHYpeH.js";
|
|
4
|
-
import { t as r } from "./serveCommand-
|
|
4
|
+
import { t as r } from "./serveCommand-CHOr74WM.js";
|
|
5
5
|
export { e as loadDotEnv, t as registerAppModules, n as registerDriver, r as runServe };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as e, t } from "./nodeEnvironment-cGFAj1J8.js";
|
|
2
2
|
import { r as n } from "./appPort-B_HpJ_ck.js";
|
|
3
|
-
import { A as r, C as i, D as a, E as o, F as s, H as c, I as l, J as ee, L as u, M as d, N as f, O as te, P as p, R as m, S as h, T as ne, V as g, W as re, X as _, Y as v, Z as y, a as b, b as x, d as S, f as C, ht as w, i as ie, j as T, k as E, n as D, nt as O, q as k, s as ae, tt as A, vt as j, x as oe, y as M, z as se } from "./webDev-
|
|
3
|
+
import { A as r, C as i, D as a, E as o, F as s, H as c, I as l, J as ee, L as u, M as d, N as f, O as te, P as p, R as m, S as h, T as ne, V as g, W as re, X as _, Y as v, Z as y, a as b, b as x, d as S, f as C, ht as w, i as ie, j as T, k as E, n as D, nt as O, q as k, s as ae, tt as A, vt as j, x as oe, y as M, z as se } from "./webDev-B4IEMB0n.js";
|
|
4
4
|
import { o as N, t as P } from "./connectionConfig-Bk9IC7D0.js";
|
|
5
5
|
import { A as F, D as I, a as ce, h as le, j as ue, k as de } from "./inspect-B7U7Cl_Z.js";
|
|
6
6
|
import { a as fe, c as L, d as pe, f as R, l as me, p as he, u as ge } from "./renderModeScan-D7J1B7Kw.js";
|
package/dist/startEntry.d.ts
CHANGED
package/dist/startEntry.js
CHANGED
package/dist/startup.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "./codemodRunner-
|
|
1
|
+
import { n as e, r as t, t as n } from "./codemodRunner-DSZjgyCL.js";
|
|
2
2
|
import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
|
|
3
3
|
import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
|
|
4
4
|
import { totalmem as p } from "node:os";
|
|
@@ -14,8 +14,8 @@ import { existsSync as j, promises as M, readFileSync as N, readdirSync as P, st
|
|
|
14
14
|
import { fileURLToPath as te, pathToFileURL as L } from "node:url";
|
|
15
15
|
import { addSink as R, createLogger as z, printReadyBlock as ne } from "@voltro/logger";
|
|
16
16
|
import { createRequire as B } from "node:module";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { currentTraceContext as V, resolveSecret as re } from "@voltro/runtime";
|
|
18
|
+
import { homedir as H } from "node:os";
|
|
19
19
|
import { FRAMEWORK_ENV_CATALOG as U, appEnvEntries as ie, checkEnvNaming as W, formatEnvErrors as ae, installEnvSnapshot as G, isEnvContract as oe, pluginEnvEntries as se, renderDotEnvExample as ce, resolveEnv as le } from "@voltro/env";
|
|
20
20
|
import { __installPublicEnv as ue } from "@voltro/env/public";
|
|
21
21
|
import { mkdir as de, writeFile as fe } from "node:fs/promises";
|
|
@@ -68,7 +68,7 @@ __installPublicEnv(publicEnvValues)
|
|
|
68
68
|
}, Ce = async (e) => {
|
|
69
69
|
let t = oe(e.rawEnv) ? e.rawEnv : void 0, n = await le({
|
|
70
70
|
contract: t,
|
|
71
|
-
resolveSecret:
|
|
71
|
+
resolveSecret: re
|
|
72
72
|
}), r = [...n.errors, ...W(t, e.appType)];
|
|
73
73
|
if (r.length > 0) throw Error(ae(r));
|
|
74
74
|
G(n.values), e.appType === "web" && ue(n.publicValues);
|
|
@@ -780,7 +780,7 @@ __installPublicEnv(publicEnvValues)
|
|
|
780
780
|
if (zt(t[i], a), !r) {
|
|
781
781
|
r = !0;
|
|
782
782
|
try {
|
|
783
|
-
let t = a.length > 0 ? It(a[0]) : "", r = t.length > 500 ? t.slice(0, 500) + "…" : t, o =
|
|
783
|
+
let t = a.length > 0 ? It(a[0]) : "", r = t.length > 500 ? t.slice(0, 500) + "…" : t, o = V();
|
|
784
784
|
e.emit("log", {
|
|
785
785
|
level: i,
|
|
786
786
|
message: r,
|
|
@@ -818,7 +818,7 @@ __installPublicEnv(publicEnvValues)
|
|
|
818
818
|
argsCount: 1,
|
|
819
819
|
...t.scope === void 0 ? {} : { scope: t.scope }
|
|
820
820
|
});
|
|
821
|
-
let a =
|
|
821
|
+
let a = V(), o = a !== void 0 && t.fields.traceId === void 0 ? {
|
|
822
822
|
...t.fields,
|
|
823
823
|
traceId: a.traceId
|
|
824
824
|
} : t.fields;
|
|
@@ -1190,7 +1190,7 @@ import {
|
|
|
1190
1190
|
a || (a = !0, i.destroy(), r(e));
|
|
1191
1191
|
};
|
|
1192
1192
|
i.once("connect", () => o(!0)), i.once("error", () => o(!1)), setTimeout(() => o(!1), n);
|
|
1193
|
-
}), Dn = () => !!(process.env.VOLTRO_DASHBOARD === "off" || process.argv.includes("--disable-dashboard")), On = () => process.env[Tn] === "1", kn = () => process.stdout.isTTY === !0, An = O(
|
|
1193
|
+
}), Dn = () => !!(process.env.VOLTRO_DASHBOARD === "off" || process.argv.includes("--disable-dashboard")), On = () => process.env[Tn] === "1", kn = () => process.stdout.isTTY === !0, An = O(H(), ".voltro", "dashboard-launch.lock"), jn = 1e4, Mn = async () => {
|
|
1194
1194
|
try {
|
|
1195
1195
|
await M.mkdir(D(An), { recursive: !0 });
|
|
1196
1196
|
let e = await M.open(An, "wx");
|
|
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
|
|
|
222
222
|
...t === void 0 ? {} : { payload: t }
|
|
223
223
|
};
|
|
224
224
|
}, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
|
|
225
|
-
let { walk: t, loadDiscovered: n } = await import("./dev-
|
|
225
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-DyoYst4j.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
|
|
226
226
|
return r((await n(await t(e))).events.map((e) => ({
|
|
227
227
|
file: e.file,
|
|
228
228
|
descriptor: e.descriptor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -646,6 +646,12 @@
|
|
|
646
646
|
"title": "`source:` is typechecked against the app’s tables, from the next `voltro dev`",
|
|
647
647
|
"kind": "manual"
|
|
648
648
|
},
|
|
649
|
+
{
|
|
650
|
+
"version": "0.46.0",
|
|
651
|
+
"id": "0.46.0/01_typed-aggregate-source",
|
|
652
|
+
"title": "an aggregate’s `incremental.source` is typechecked against the app’s tables",
|
|
653
|
+
"kind": "manual"
|
|
654
|
+
},
|
|
649
655
|
{
|
|
650
656
|
"version": "0.5.0",
|
|
651
657
|
"id": "0.5.0/01_one-terminal-exactly-one",
|
|
@@ -733,22 +739,22 @@
|
|
|
733
739
|
"@effect/platform-node": "^0.108.0",
|
|
734
740
|
"@effect/sql": "^0.52.0",
|
|
735
741
|
"@effect/workflow": "^0.19.0",
|
|
736
|
-
"@voltro/ai": "0.
|
|
737
|
-
"@voltro/cache": "0.
|
|
738
|
-
"@voltro/data-transfer": "0.
|
|
739
|
-
"@voltro/database": "0.
|
|
740
|
-
"@voltro/env": "0.
|
|
741
|
-
"@voltro/kv": "0.
|
|
742
|
-
"@voltro/logger": "0.
|
|
743
|
-
"@voltro/plugin-auth": "0.
|
|
744
|
-
"@voltro/plugin-broadcast": "0.
|
|
745
|
-
"@voltro/plugin-mail": "0.
|
|
746
|
-
"@voltro/plugin-storage": "0.
|
|
747
|
-
"@voltro/plugin-webhooks": "0.
|
|
748
|
-
"@voltro/protocol": "0.
|
|
749
|
-
"@voltro/runtime": "0.
|
|
750
|
-
"@voltro/serverless": "0.
|
|
751
|
-
"@voltro/workflow": "0.
|
|
742
|
+
"@voltro/ai": "0.46.0",
|
|
743
|
+
"@voltro/cache": "0.46.0",
|
|
744
|
+
"@voltro/data-transfer": "0.46.0",
|
|
745
|
+
"@voltro/database": "0.46.0",
|
|
746
|
+
"@voltro/env": "0.46.0",
|
|
747
|
+
"@voltro/kv": "0.46.0",
|
|
748
|
+
"@voltro/logger": "0.46.0",
|
|
749
|
+
"@voltro/plugin-auth": "0.46.0",
|
|
750
|
+
"@voltro/plugin-broadcast": "0.46.0",
|
|
751
|
+
"@voltro/plugin-mail": "0.46.0",
|
|
752
|
+
"@voltro/plugin-storage": "0.46.0",
|
|
753
|
+
"@voltro/plugin-webhooks": "0.46.0",
|
|
754
|
+
"@voltro/protocol": "0.46.0",
|
|
755
|
+
"@voltro/runtime": "0.46.0",
|
|
756
|
+
"@voltro/serverless": "0.46.0",
|
|
757
|
+
"@voltro/workflow": "0.46.0",
|
|
752
758
|
"chokidar": "^5.0.0",
|
|
753
759
|
"ioredis": "^5.11.1",
|
|
754
760
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -707,7 +707,7 @@ each plugin's own README.
|
|
|
707
707
|
|
|
708
708
|
| Topic | Open | Summary |
|
|
709
709
|
|---|---|---|
|
|
710
|
-
| **What's new in 0.
|
|
710
|
+
| **What's new in 0.46.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
711
711
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
712
712
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
713
713
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.46.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -2610,11 +2610,25 @@ Rows are written as newline-delimited JSON — streamable, resumable by line, an
|
|
|
2610
2610
|
|
|
2611
2611
|
| Flag | Selects |
|
|
2612
2612
|
|---|---|
|
|
2613
|
-
| *(none)* | Every table (the default). |
|
|
2613
|
+
| *(none)* | Every table the app has DATA in (the default) — see the note below on the handful that describe a deployment rather than filling it. |
|
|
2614
2614
|
| `--tenant <id>` | Every `tenant()`-scoped table filtered to that tenant, **plus** the FK closure in BOTH directions: (1) the transitive **FK-parent** closure of those rows — closure-pulled shared tables (a global `users` / reference table) are **row-subset to the ids the tenant's rows actually reference**, never exported in full; and (2) the **child** closure — rows that *reference* the tenant's rows (the comments on the tenant's projects) come along too, each scoped to the ids that actually point into the tenant set. The child walk is anchored on the `tenant()` tables, so a row that references **only a shared parent** (a global `users` another tenant also references) is **not** pulled — that would be a cross-tenant leak. A `--tenant` bundle therefore carries the tenant's parents AND children and **no other tenant's rows** — that's what makes it safe as a GDPR / per-tenant takeout. |
|
|
2615
2615
|
| `--tables a,b` | An explicit set (you own referential integrity; the importer's deferred-FK resolution covers load-order dangles, see below). |
|
|
2616
2616
|
| `--exclude a,b` | Everything EXCEPT these, expanded against the live table list. Direct target only. Cannot be combined with `--tables` or `--tenant`; a name that does not exist is refused, because an exclusion that excludes nothing leaves the run looking like it worked. |
|
|
2617
2617
|
|
|
2618
|
+
#### What `all` deliberately leaves out
|
|
2619
|
+
|
|
2620
|
+
A few framework tables hold rows **about a deployment** rather than an app's data — its migration ledger, its file-migration and seed records, its CDC offsets, its schedule claims and wakeups, its workflow watermarks / pending starts / admissions / pauses. They are dropped from `all`, skipped on import, and never emptied by a `replace`, and the run says which ones and why.
|
|
2621
|
+
|
|
2622
|
+
The reason is worth one paragraph, because it cost a real environment ninety minutes. A `scope: all` bundle carried `_voltro_migration_plans`, `replace` wrote it, and the target's next boot refused:
|
|
2623
|
+
|
|
2624
|
+
```
|
|
2625
|
+
auto-migrate: SCHEMA FINGERPRINT MISMATCH — declared=6e2c61081a9ed80c live=28af9a54414f22f1
|
|
2626
|
+
```
|
|
2627
|
+
|
|
2628
|
+
The refusal was right. That fingerprint is computed over the **declared table set**, which legitimately differs per environment — `NODE_ENV=production` declares `_voltro_traces` and `_voltro_undo_log`, a `development` run does not. So the imported row was not out of date, it was **foreign**: it stated a schema decision made somewhere else. Two of the others would have made the target *act* — a pending workflow start runs a workflow somebody queued elsewhere, a pause silently stops one here.
|
|
2629
|
+
|
|
2630
|
+
`all` is the only scope filtered. **Name one of these in `--tables` and you get it** — an explicit name is an expectation, and this command refuses to drop those silently.
|
|
2631
|
+
|
|
2618
2632
|
**Every table needs a single-column primary key.** The export is keyset-paginated, so it orders by one column and resumes from the last value on the next page. That column comes from the real primary key — a declared `id()` where there is one, otherwise the table's actual PK, whatever it is called.
|
|
2619
2633
|
|
|
2620
2634
|
A table with a **composite** primary key, or none at all, is **refused by name** rather than exported. Ordering by one column of a composite key splits equal values across page boundaries, which drops or duplicates rows into a bundle that reports success — and a short backup is discovered at the restore. Leave such a table out with `--exclude`.
|
|
@@ -2753,6 +2767,128 @@ returns **`409` schema drift** with the fingerprints + diff; `--force` sends
|
|
|
2753
2767
|
|
|
2754
2768
|
The pre-flight only runs when the importer has a target schema (the CLI introspects it; the API endpoint uses the instance's declared schema). Importing into a fresh/empty database with no comparable schema simply skips the check.
|
|
2755
2769
|
|
|
2770
|
+
### `replace` writes down what it is about to destroy
|
|
2771
|
+
|
|
2772
|
+
Before the first delete, a `replace` exports the target's **current** rows for exactly the tables it is going to empty, as an ordinary bundle beside yours:
|
|
2773
|
+
|
|
2774
|
+
```
|
|
2775
|
+
rollback capture: 240172 row(s) across 75 table(s) → ./out.rollback-2026-08-21T09-10-11-000Z
|
|
2776
|
+
If this run does not finish, restore with: voltro data import ./out.rollback-… --mode replace
|
|
2777
|
+
```
|
|
2778
|
+
|
|
2779
|
+
It is on disk **before** anything is destroyed, so it does not depend on a transaction, or on the process being alive to roll one back. That distinction is the whole reason it exists: a deployment lost 240 172 rows to a `replace` whose api pod disappeared nine minutes in, and recovered from an export they had taken twenty minutes earlier out of habit. This is that habit, made into the tool's behaviour.
|
|
2780
|
+
|
|
2781
|
+
**It is fail-closed.** A capture that cannot be taken stops the import before it starts, and the target is untouched. A safety net you believe in and do not have is worse than none — the belief is what stops you taking your own export.
|
|
2782
|
+
|
|
2783
|
+
`--no-rollback` turns it off, and `--rollback-dir <path>` puts it somewhere else. It is only taken for `replace`: `upsert` and `append` do not destroy, so there is no moment where the old state has silently become unreachable.
|
|
2784
|
+
|
|
2785
|
+
**Over `--target api` it goes to the instance's object storage**, because there the process that would roll a transaction back IS the instance — a capture in the pod's filesystem would go away with the failure it exists for. Name a key:
|
|
2786
|
+
|
|
2787
|
+
```bash
|
|
2788
|
+
voltro data import ./out --target api --api-url <url> --token $SECRET --mode replace --rollback-key backups/before.vbundle
|
|
2789
|
+
```
|
|
2790
|
+
|
|
2791
|
+
The instance writes the capture there **before the first delete**, and refuses the run (409) if it has no storage configured — asking for a capture and being served without one is the answer that removes your own precaution while looking like agreement. A `replace` that names no key still runs, and says what it did not keep.
|
|
2792
|
+
|
|
2793
|
+
### `replace` loads somewhere else first
|
|
2794
|
+
|
|
2795
|
+
The target keeps its rows until the load stands. `replace` creates a staging
|
|
2796
|
+
table per table, loads the bundle into those, and then swaps the CONTENT across
|
|
2797
|
+
in one short transaction of server-side SQL. It says so when it does:
|
|
2798
|
+
|
|
2799
|
+
```
|
|
2800
|
+
staging 114 table(s) before the swap — the target keeps its rows until the load stands.
|
|
2801
|
+
The destructive step is one server-side transaction at the end, not the whole load.
|
|
2802
|
+
```
|
|
2803
|
+
|
|
2804
|
+
The difference is what a dead process costs. Loading straight into the target
|
|
2805
|
+
holds the destructive transaction open for the whole load — minutes, for a large
|
|
2806
|
+
bundle — and the target only survives because the database rolls that
|
|
2807
|
+
transaction back. With staging, a process that dies during the load leaves the
|
|
2808
|
+
target exactly as it was, because nothing has been deleted yet.
|
|
2809
|
+
|
|
2810
|
+
Not every run can take it, and a run that cannot **says why** rather than quietly
|
|
2811
|
+
taking the slower path:
|
|
2812
|
+
|
|
2813
|
+
- a store the framework cannot send DDL to (the in-memory store).
|
|
2814
|
+
- a **write recorder** on any table in the set — `versioningPlugin({ timing:
|
|
2815
|
+
'in-transaction' })` and friends. A recorder is keyed by table name, so a
|
|
2816
|
+
staged write would find none and the recorder would silently not run. Its
|
|
2817
|
+
promise is "if the change committed, the entry is there", so the run keeps the
|
|
2818
|
+
path that can keep it.
|
|
2819
|
+
|
|
2820
|
+
**`--no-atomic` stages too, and that is where it changes the most.** The flag
|
|
2821
|
+
exists for resumability on a large bundle, and it used to be the mode with the
|
|
2822
|
+
worst failure: the target emptied and partially refilled, in neither state.
|
|
2823
|
+
Staged, the ledger keeps its exact meaning — a recorded table is one fully
|
|
2824
|
+
loaded, it just lands in staging — while the target stays untouched until the
|
|
2825
|
+
swap. Resumable and all-or-nothing at once, which the two could not be before.
|
|
2826
|
+
|
|
2827
|
+
A resumed run continues from what it already staged rather than reloading it,
|
|
2828
|
+
and a run whose swap has not happened keeps its staged rows and says so:
|
|
2829
|
+
|
|
2830
|
+
```
|
|
2831
|
+
the staged rows are KEPT so a re-run can continue from them rather than reload.
|
|
2832
|
+
If you are not going to re-run this bundle, drop them: voltro data clear-staging --yes
|
|
2833
|
+
```
|
|
2834
|
+
|
|
2835
|
+
**A failed swap names every offending row, not the first.** Staging carries no
|
|
2836
|
+
foreign keys — a staged row whose parent has not been staged yet must not be
|
|
2837
|
+
refused — so a dangling reference surfaces at the swap, where the database
|
|
2838
|
+
reports one constraint. The importer then asks staging the same question and
|
|
2839
|
+
lists every row that fails it:
|
|
2840
|
+
|
|
2841
|
+
```
|
|
2842
|
+
the swap could not run: 2 row(s) in the bundle reference a row the bundle does not carry.
|
|
2843
|
+
tasks.t_41: ownerId = "u_9" — no such row in users
|
|
2844
|
+
tasks.t_88: ownerId = "u_12" — no such row in users
|
|
2845
|
+
The target is UNCHANGED — the swap runs in one transaction and none of it committed.
|
|
2846
|
+
```
|
|
2847
|
+
|
|
2848
|
+
Staging tables from a run that died mid-load are collected by the next
|
|
2849
|
+
`replace` over the same tables. One over a DIFFERENT set leaves them, and
|
|
2850
|
+
nothing else removes them:
|
|
2851
|
+
|
|
2852
|
+
```bash
|
|
2853
|
+
voltro data clear-staging --yes
|
|
2854
|
+
```
|
|
2855
|
+
|
|
2856
|
+
Deliberately a command and not a boot sweep: a booting process cannot tell a
|
|
2857
|
+
leftover from a staging table another replica is loading into right now, and
|
|
2858
|
+
deleting the second would destroy an import in flight.
|
|
2859
|
+
|
|
2860
|
+
A **cycle** in the bundle's foreign keys is detected before the load, not after
|
|
2861
|
+
it. The swap inserts parents first, so two tables referencing each other cannot
|
|
2862
|
+
both be satisfied by a bulk copy on postgres, sqlite or SQL Server — `SET
|
|
2863
|
+
CONSTRAINTS ALL DEFERRED` does not help, because postgres only defers a
|
|
2864
|
+
constraint declared `DEFERRABLE`. Such a run says so and takes the row-by-row
|
|
2865
|
+
path, whose deferred-FK pass exists for exactly that shape. A table referencing
|
|
2866
|
+
ITSELF is not a cycle: one statement carries the whole table.
|
|
2867
|
+
|
|
2868
|
+
### An interrupted `replace` cannot be silent
|
|
2869
|
+
|
|
2870
|
+
The capture only helps if somebody knows to reach for it. A half-replaced database is indistinguishable from an empty one **from the inside** — every table exists, every constraint holds, every query returns nothing without erroring — so a run that emptied a target and disappeared can be served over for hours before anyone asks the right question.
|
|
2871
|
+
|
|
2872
|
+
So a `replace` writes one row before the first delete and removes it after the last insert. Finding it at boot is a **refusal**, not a warning:
|
|
2873
|
+
|
|
2874
|
+
```
|
|
2875
|
+
refusing to start: a destructive import did not finish.
|
|
2876
|
+
- a `replace` over api began emptying 114 table(s) 12 minute(s) ago and never reported finishing.
|
|
2877
|
+
The target's previous rows were captured first:
|
|
2878
|
+
./out.rollback-2026-08-21T09-10-11-000Z
|
|
2879
|
+
Restore them with: voltro data import ./out.rollback-… --mode replace
|
|
2880
|
+
```
|
|
2881
|
+
|
|
2882
|
+
The row lives in the same transaction as the emptying, so it is present **exactly when the emptying is**: a run that rolls back cleanly takes the marker with it, and a boot over a database nothing happened to is not refused. A completed `replace` clears its own marker and any older one — so the recovery import both restores the data and silences the alarm, in one command.
|
|
2883
|
+
|
|
2884
|
+
Nothing expires. A half-replaced database does not become whole with time, so clearing it is a decision:
|
|
2885
|
+
|
|
2886
|
+
```bash
|
|
2887
|
+
voltro data clear-replace-marker --yes
|
|
2888
|
+
```
|
|
2889
|
+
|
|
2890
|
+
The devtools import panel takes the same two precautions as the command line — a capture before the first delete, and the marker — because a button is easier to press than a command is to type.
|
|
2891
|
+
|
|
2756
2892
|
### Deferred-FK resolution
|
|
2757
2893
|
|
|
2758
2894
|
A row whose write fails on a foreign-key constraint — a forward reference from a `live`-consistency export, a genuine FK **cycle** between tables (which the exporter orders by breaking the closing edge), or an intra-table self-reference to a later row — does **not** fail the import. It is held and resolved after every table has streamed:
|
|
@@ -2803,7 +2939,7 @@ Unlike the rpc wire, this string DOES include the driver's own words. The audien
|
|
|
2803
2939
|
|---|---|---|---|
|
|
2804
2940
|
| `upsert` (default) | INSERT-or-UPDATE by primary key | sync / idempotent re-import | overwrites per row |
|
|
2805
2941
|
| `append` | INSERT only | additive data (event log, new seed) | `--on-conflict skip` (default) or `fail` |
|
|
2806
|
-
| `replace` |
|
|
2942
|
+
| `replace` | capture the target, empty it, then INSERT | full refresh — target ends up **exactly** the bundle | — |
|
|
2807
2943
|
|
|
2808
2944
|
`replace` **refuses a partial bundle** (a subset / tenant / table scope): emptying would delete rows the bundle never carried. Re-export with full scope, or use `upsert`. (`append --on-conflict fail` throws on a duplicate primary key only where the store enforces the constraint — every SQL dialect does; the in-memory dev store overwrites.)
|
|
2809
2945
|
|
|
@@ -397,10 +397,59 @@ changes. **That is an authorization staleness, not a cosmetic one:** revoke
|
|
|
397
397
|
someone's membership and their open subscription keeps serving rows they may no
|
|
398
398
|
longer see, until something else happens to invalidate it.
|
|
399
399
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
Reported by a team whose own invariant caught it after five computed queries
|
|
401
|
+
under-declared their `source`; the fix was array sources.
|
|
402
|
+
|
|
403
|
+
### `voltro dev` now says when a query reads a table it did not declare
|
|
404
|
+
|
|
405
|
+
That paragraph used to end "nothing warns about this at runtime". It does now.
|
|
406
|
+
|
|
407
|
+
While `voltro dev` is running, every read a query makes is attributed to it and
|
|
408
|
+
compared against its own `source:`. Read a table you did not declare and the
|
|
409
|
+
terminal says so, once:
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
source: tasks.list: read `task_sub_tasks` without declaring it in `source:`.
|
|
413
|
+
A write to that table will not re-run this query, so an open view keeps
|
|
414
|
+
showing what it showed before. The write itself is fine, which is why nothing
|
|
415
|
+
else reports this.
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
It is deliberately narrow, and knowing where its edges are is the difference
|
|
419
|
+
between acting on it and learning to skim it:
|
|
420
|
+
|
|
421
|
+
- **It reports what it SAW.** A branch that did not run contributes nothing, so
|
|
422
|
+
it never claims your `source:` is otherwise complete — only that a table it
|
|
423
|
+
watched you read is missing from it.
|
|
424
|
+
- **Once per query per boot.** A per-request warning on a hot list would be its
|
|
425
|
+
own outage.
|
|
426
|
+
- **A query with no `source:` at all is left alone.** It has made no claim; the
|
|
427
|
+
finding is about an incomplete list, not a missing one.
|
|
428
|
+
- **An eager-loaded relation COUNTS**, and it is the case worth knowing about.
|
|
429
|
+
`.with({ subTasks: true })` issues no second read — the whole spec folds into
|
|
430
|
+
one round trip — so the loaded table never appears as a read of its own. The
|
|
431
|
+
recorder resolves it through the relation registry instead, target and (for a
|
|
432
|
+
many-to-many) junction alike. A write to the junction changes membership,
|
|
433
|
+
which is exactly the change a user makes.
|
|
434
|
+
- **A table read only to NARROW a result is not counted** — a parent reached
|
|
435
|
+
through `inSubquery(...)`, or a read the framework made to resolve your row
|
|
436
|
+
filter. Those decide which rows come back rather than contributing rows, and
|
|
437
|
+
putting every one of them in `source:` would re-run every list on every
|
|
438
|
+
membership write.
|
|
439
|
+
|
|
440
|
+
That last rule is a judgement the recorder makes for the common case and
|
|
441
|
+
deliberately does not make for yours. The section above is the case where you
|
|
442
|
+
want a restricting read in `source:` anyway — an authorization read whose
|
|
443
|
+
staleness you care about. The recorder will not nag you into it and will not
|
|
444
|
+
argue when you add it.
|
|
445
|
+
|
|
446
|
+
**Dev only.** `voltro serve` installs none of it — it costs a wrapper per read,
|
|
447
|
+
and a production log is not where this gets read. `VOLTRO_SOURCE_RECORDER=off`
|
|
448
|
+
turns it off in dev.
|
|
449
|
+
|
|
450
|
+
If your own helper resolves access somewhere the framework does not call it, wrap
|
|
451
|
+
it in `restrictingReads` (from `@voltro/runtime`) and its reads stop counting —
|
|
452
|
+
inside or outside a recording session, so it is safe to leave in place.
|
|
404
453
|
|
|
405
454
|
## `output` is the serializer — `timestampMs`
|
|
406
455
|
|
|
@@ -3168,6 +3217,23 @@ When a shape can't be incremental, leave `incremental` off and use a refresh
|
|
|
3168
3217
|
policy — the engine recomputes. Incremental is an optimisation for hot, bounded
|
|
3169
3218
|
aggregates, not a different result.
|
|
3170
3219
|
|
|
3220
|
+
### `source` is a table name, and it is checked
|
|
3221
|
+
|
|
3222
|
+
`incremental.source` is typed against your app's own tables — the same
|
|
3223
|
+
`TableName` a query's `source:` uses — so a misspelled or renamed-away name is a
|
|
3224
|
+
compile error from your next `voltro dev`.
|
|
3225
|
+
|
|
3226
|
+
That check exists because the failure has no other symptom. A source matching no
|
|
3227
|
+
table does not error: the runner subscribes to something nothing writes, no delta
|
|
3228
|
+
ever arrives, and the aggregate quietly stops tracking its input while every read
|
|
3229
|
+
of it still succeeds and still returns a number.
|
|
3230
|
+
|
|
3231
|
+
The boot says it too. An aggregate whose CDC source resolves to no live table is
|
|
3232
|
+
named in the same stale-`source:` warning queries and streams appear in, on both
|
|
3233
|
+
boot paths — a name can be correct at the type level and still be a table this
|
|
3234
|
+
deployment does not have. An aggregate with no `incremental` block declares no
|
|
3235
|
+
source and is not audited.
|
|
3236
|
+
|
|
3171
3237
|
## Boot strategy
|
|
3172
3238
|
|
|
3173
3239
|
Each aggregate declares `bootRefresh` (default `'persistent'`). All three modes are wired in the aggregate runner (`attachAggregates`):
|