@voltro/cli 0.53.0 → 0.54.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 +195 -0
- package/dist/{agentsMd-0l980yhL.js → agentsMd-BaLC10Na.js} +110 -82
- package/dist/agentsMd-DCY1RSs8.js +2 -0
- package/dist/apiBuild-CeUN55uk.js +2 -0
- package/dist/{apiBuild-CaPfoWku.js → apiBuild-DTWp0S_q.js} +2 -2
- package/dist/bin.js +1 -1
- package/dist/{build-D-OnvNMf.js → build-D4ygSbnV.js} +114 -114
- package/dist/{checkCommand-C5elt0tW.js → checkCommand-Dg1G7Gwd.js} +6 -6
- package/dist/{checkCommand-D2ZduVlh.js → checkCommand-L7DTlpIF.js} +1 -1
- package/dist/{cloudCmd-QUXh-b5w.js → cloudCmd-DzKcSYuy.js} +1 -1
- package/dist/{codegen-FEk8AZHb.js → codegen-DSLM8Su9.js} +2 -2
- package/dist/codegen-DjgxEOnD.js +2 -0
- package/dist/codegenCommand-CG_Vx4lc.js +41 -0
- package/dist/{codemodRunner-BjtB2lq6.js → codemodRunner-Cd4xkC6u.js} +9 -9
- package/dist/{commands-DyxAmhP0.js → commands-6Kzi92Np.js} +96 -73
- package/dist/{dashboardCommand-BdKTyT13.js → dashboardCommand-Cq1PWvI1.js} +3 -3
- package/dist/{dataCommand-Bab9X7s8.js → dataCommand-DYzW8vkv.js} +3 -3
- package/dist/{dbCommand-06O2finM.js → dbCommand-B4NWZtGL.js} +3 -3
- package/dist/dbCommand-CSFWs9ev.js +2 -0
- package/dist/{dev-C6LGF4iY.js → dev-CmuvUKRq.js} +2236 -2219
- package/dist/{dev-GjJWAYo2.js → dev-cKUiZZsB.js} +1 -1
- package/dist/{doctorCommand-etMkflRc.js → doctorCommand-DCiFVMtZ.js} +21 -21
- package/dist/doctorCommand-J3qu4E0Y.js +2 -0
- package/dist/{dormancyCommand-UwZ1AZzB.js → dormancyCommand-w1TrmgYP.js} +1 -1
- package/dist/{embeddingsCommand-C70zWHwo.js → embeddingsCommand-CMgPyRTr.js} +1 -1
- package/dist/{envCommand-dSyKvRkM.js → envCommand-Cyynmcfa.js} +15 -15
- package/dist/{evolveCommand-CG0_ebO5.js → evolveCommand-BwvQ8dVH.js} +2 -2
- package/dist/fileConventions-l-RIXbx8.js +36 -0
- package/dist/{fileTaxonomy-B7uxipWS.js → fileTaxonomy-CbyMQYx_.js} +37 -37
- package/dist/{frameworkTableAssembly-DKx3ba3S.js → frameworkTableAssembly-D7LJuALW.js} +5 -5
- package/dist/frameworkTableAssembly-IPD1pUnZ.js +2 -0
- package/dist/index.js +2 -2
- package/dist/{infoCommand-_53iOc_j.js → infoCommand-DlYlUPqs.js} +1 -1
- package/dist/{metaCommands-Cn2oboG4.js → metaCommands-x7RCi2AF.js} +2 -2
- package/dist/{migrate-Cko9rswM.js → migrate-BK_Bbx-_.js} +2 -2
- package/dist/mobileCommand-D9O6iq3D.js +428 -0
- package/dist/mobileCommand-DAum7tsG.js +2 -0
- package/dist/{pageConvention-C938S8oC.js → pageConvention-CMpfDN6r.js} +1 -1
- package/dist/{privacyCommand-DWTQMC6R.js → privacyCommand-BCa2OoZG.js} +2 -2
- package/dist/{probeCommand-DkGGLknv.js → probeCommand-_C0YU207.js} +1 -1
- package/dist/{projectScaffold-EzlErR4E.js → projectScaffold-CJfP-xbT.js} +1 -1
- package/dist/{projectScaffold-B4dmTlwT.js → projectScaffold-CSN0OzBV.js} +2 -2
- package/dist/renderModeScan-43yQ2opo.js +147 -0
- package/dist/{renderProfile-CskIgAfn.js → renderProfile-DvrhVJHa.js} +2 -2
- package/dist/{runtimeTrace-c0APJz7E.js → runtimeTrace-CGWx1Q6l.js} +1 -1
- package/dist/{sdkgen-BiQCgIEr.js → sdkgen-CDGHQUFj.js} +1 -1
- package/dist/serveCommand-BiPe8BJm.js +2 -0
- package/dist/{serveCommand-CueKQgzl.js → serveCommand-Bje09q1v.js} +708 -708
- package/dist/serveEntry.js +1 -1
- package/dist/start-B0bnJgxI.js +3 -0
- package/dist/{start-ekPan8BT.js → start-Clz-1BHB.js} +511 -504
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-xlSL-IWk.js → staticCommand-ey0kYmOT.js} +1 -1
- package/dist/{subcommandNames-DpYs3DXr.js → subcommandNames-CDzfEtKV.js} +3 -3
- package/dist/{templates-BR-fb4SP.js → templates-BTWZkJJT.js} +41 -9
- package/dist/{test-BWPQcRoB.js → test-D_kW4KMj.js} +1 -1
- package/dist/updateCommand-CIoVDKnj.js +2 -0
- package/dist/{updateCommand-C_8I8Rzo.js → updateCommand-CRJlAOaM.js} +1 -1
- package/dist/{webDev-oczpugbx.js → webDev-DSI9SOhs.js} +1127 -1090
- package/dist/{webDev-C7jWJ5dX.js → webDev-DlvZO30c.js} +1 -1
- package/dist/{webhooksCommand-4SVPDjKg.js → webhooksCommand-BvzXNHji.js} +1 -1
- package/dist/workspaceDeps-RKEkX92S.js +45 -0
- package/package.json +19 -19
- package/templates/AGENTS.core.md +2 -0
- package/templates/AGENTS.md +4 -2
- package/templates/agent-docs/_index.md +2 -2
- package/templates/agent-docs/_manifest.json +1 -1
- package/templates/agent-docs/ai.md +4 -4
- package/templates/agent-docs/authentication.md +72 -0
- package/templates/agent-docs/cli.md +4 -1
- package/templates/agent-docs/data.md +61 -12
- package/templates/agent-docs/database/advancedqueries.md +1 -1
- package/templates/agent-docs/database/migrations.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +64 -2
- package/templates/agent-docs/internationalization.md +2 -0
- package/templates/agent-docs/introduction.md +25 -0
- package/templates/agent-docs/local-first-mobile.md +139 -41
- package/templates/agent-docs/observability.md +4 -2
- package/templates/agent-docs/plugins/atlassian.md +2 -2
- package/templates/agent-docs/plugins/audit.md +2 -2
- package/templates/agent-docs/plugins/billing.md +1 -1
- package/templates/agent-docs/plugins/cdc-out.md +8 -3
- package/templates/agent-docs/plugins/comments.md +22 -0
- package/templates/agent-docs/plugins/presence.md +32 -3
- package/templates/agent-docs/plugins/prometheus.md +2 -0
- package/templates/agent-docs/plugins/queue.md +47 -4
- package/templates/agent-docs/plugins.md +6 -6
- package/templates/agent-docs/reference.md +20 -1
- package/templates/agent-docs/routing.md +63 -9
- package/templates/agent-docs/scheduling.md +1 -1
- package/templates/agent-docs/schema-driven-ui.md +12 -1
- package/templates/agent-docs/templates/appshells.md +36 -4
- package/templates/agent-docs/whats-new.md +109 -135
- package/templates/apps/api-ai/package.json +6 -6
- 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 +9 -9
- package/templates/apps/api-collab/README.md +3 -3
- package/templates/apps/api-collab/app.config.ts +1 -1
- package/templates/apps/api-collab/database/schema.ts +12 -8
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +1 -1
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-collab/template.json +1 -1
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +10 -2
- 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-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +11 -10
- 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-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +7 -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/README.md +43 -24
- package/templates/apps/frontend-collab/app.config.ts +3 -3
- package/templates/apps/frontend-collab/package.json +14 -10
- package/templates/apps/frontend-collab/src/locales/de.ts +1 -2
- package/templates/apps/frontend-collab/src/locales/en.ts +1 -2
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +72 -76
- package/templates/apps/frontend-collab/src/pages/page.tsx +45 -22
- package/templates/apps/frontend-collab/template.json +2 -2
- 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 +8 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/README.md +48 -0
- package/templates/apps/frontend-landing/app.config.ts +28 -0
- package/templates/apps/frontend-landing/package.json +7 -6
- package/templates/apps/frontend-landing/src/assets/hero.jpg +0 -0
- package/templates/apps/frontend-landing/src/fonts/Geist-Variable.woff2 +0 -0
- package/templates/apps/frontend-landing/src/fonts/LICENSE-Geist.txt +92 -0
- package/templates/apps/frontend-landing/src/globals.css +15 -0
- package/templates/apps/frontend-landing/src/globals.d.ts +17 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +3 -2
- package/templates/apps/frontend-landing/src/locales/en.ts +5 -2
- package/templates/apps/frontend-landing/src/pages/page.test.tsx +79 -0
- package/templates/apps/frontend-landing/src/pages/page.tsx +26 -3
- package/templates/apps/frontend-landing/template.json +2 -2
- 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 +8 -7
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +12 -11
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +29 -17
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +16 -0
- package/dist/agentsMd-SDDSkyl4.js +0 -2
- package/dist/apiBuild-DHtLXYx9.js +0 -2
- package/dist/codegen-BWpt3VgF.js +0 -2
- package/dist/codegenCommand-BOiWQ5hz.js +0 -137
- package/dist/dbCommand-B1EXBC6f.js +0 -2
- package/dist/doctorCommand-B0hX0tdz.js +0 -2
- package/dist/fileConventions-DASGEmj-.js +0 -35
- package/dist/frameworkTableAssembly-C_7Z-rMs.js +0 -2
- package/dist/renderModeScan-CUbOeOAg.js +0 -122
- package/dist/serveCommand-DsnrVN3U.js +0 -2
- package/dist/start-BJzZLbt8.js +0 -3
- package/dist/updateCommand-Bqql_rsQ.js +0 -2
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { p as e, x as t, y as n } from "./webDev-
|
|
1
|
+
import { p as e, x as t, y as n } from "./webDev-DSI9SOhs.js";
|
|
2
2
|
export { e as loadConfig, n as tryRunWebServe, t as walkPagesTree };
|
|
@@ -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-cKUiZZsB.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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { dirname as e, join as t } from "node:path";
|
|
2
|
+
import { promises as n } from "node:fs";
|
|
3
|
+
//#region src/workspaceDeps.ts
|
|
4
|
+
var r = async (r, i) => {
|
|
5
|
+
let a = r;
|
|
6
|
+
for (;;) {
|
|
7
|
+
let r = t(a, "node_modules", i);
|
|
8
|
+
try {
|
|
9
|
+
if ((await n.stat(r)).isDirectory()) {
|
|
10
|
+
let e = await n.realpath(r);
|
|
11
|
+
return e.includes("/node_modules/") ? null : e;
|
|
12
|
+
}
|
|
13
|
+
} catch {}
|
|
14
|
+
let o = e(a);
|
|
15
|
+
if (o === a) return null;
|
|
16
|
+
a = o;
|
|
17
|
+
}
|
|
18
|
+
}, i = async (e) => {
|
|
19
|
+
let i;
|
|
20
|
+
try {
|
|
21
|
+
let r = await n.readFile(t(e, "package.json"), "utf8"), a = JSON.parse(r);
|
|
22
|
+
i = Object.keys({
|
|
23
|
+
...a.dependencies,
|
|
24
|
+
...a.devDependencies
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return (await Promise.all(i.map(async (i) => {
|
|
30
|
+
let a = await r(e, i);
|
|
31
|
+
if (a === null) return null;
|
|
32
|
+
let o = t(a, "src");
|
|
33
|
+
try {
|
|
34
|
+
if (!(await n.stat(o)).isDirectory()) return null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
pkg: i,
|
|
40
|
+
dir: o
|
|
41
|
+
};
|
|
42
|
+
}))).filter((e) => e !== null);
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { i as n, r as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.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",
|
|
@@ -836,24 +836,24 @@
|
|
|
836
836
|
"@effect/platform-node": "^0.108.0",
|
|
837
837
|
"@effect/sql": "^0.52.0",
|
|
838
838
|
"@effect/workflow": "^0.19.0",
|
|
839
|
-
"@voltro/ai": "0.
|
|
840
|
-
"@voltro/cache": "0.
|
|
841
|
-
"@voltro/client": "0.
|
|
842
|
-
"@voltro/content": "0.
|
|
843
|
-
"@voltro/data-transfer": "0.
|
|
844
|
-
"@voltro/database": "0.
|
|
845
|
-
"@voltro/env": "0.
|
|
846
|
-
"@voltro/kv": "0.
|
|
847
|
-
"@voltro/logger": "0.
|
|
848
|
-
"@voltro/plugin-auth": "0.
|
|
849
|
-
"@voltro/plugin-broadcast": "0.
|
|
850
|
-
"@voltro/plugin-mail": "0.
|
|
851
|
-
"@voltro/plugin-storage": "0.
|
|
852
|
-
"@voltro/plugin-webhooks": "0.
|
|
853
|
-
"@voltro/protocol": "0.
|
|
854
|
-
"@voltro/runtime": "0.
|
|
855
|
-
"@voltro/serverless": "0.
|
|
856
|
-
"@voltro/workflow": "0.
|
|
839
|
+
"@voltro/ai": "0.54.0",
|
|
840
|
+
"@voltro/cache": "0.54.0",
|
|
841
|
+
"@voltro/client": "0.54.0",
|
|
842
|
+
"@voltro/content": "0.54.0",
|
|
843
|
+
"@voltro/data-transfer": "0.54.0",
|
|
844
|
+
"@voltro/database": "0.54.0",
|
|
845
|
+
"@voltro/env": "0.54.0",
|
|
846
|
+
"@voltro/kv": "0.54.0",
|
|
847
|
+
"@voltro/logger": "0.54.0",
|
|
848
|
+
"@voltro/plugin-auth": "0.54.0",
|
|
849
|
+
"@voltro/plugin-broadcast": "0.54.0",
|
|
850
|
+
"@voltro/plugin-mail": "0.54.0",
|
|
851
|
+
"@voltro/plugin-storage": "0.54.0",
|
|
852
|
+
"@voltro/plugin-webhooks": "0.54.0",
|
|
853
|
+
"@voltro/protocol": "0.54.0",
|
|
854
|
+
"@voltro/runtime": "0.54.0",
|
|
855
|
+
"@voltro/serverless": "0.54.0",
|
|
856
|
+
"@voltro/workflow": "0.54.0",
|
|
857
857
|
"chokidar": "^5.0.0",
|
|
858
858
|
"ioredis": "^5.11.1",
|
|
859
859
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -368,6 +368,8 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
|
|
|
368
368
|
| `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
|
|
369
369
|
| `*.tool.tsx` | tool an agent can call |
|
|
370
370
|
| `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
|
|
371
|
+
| `*.collection.ts` | content collection (`defineCollection`) — schema-typed markdown/JSON |
|
|
372
|
+
| `content/<name>/**` | that collection's files (markdown + frontmatter, or `.json`) |
|
|
371
373
|
| `page.tsx` (under `src/pages/`) | the route its DIRECTORY serves + `page.test.tsx` |
|
|
372
374
|
| `*.component.tsx` | exactly ONE component (+ types) |
|
|
373
375
|
| `*.component.ui.tsx` | presentational: one component, READS only — never writes |
|
package/templates/AGENTS.md
CHANGED
|
@@ -368,6 +368,8 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
|
|
|
368
368
|
| `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
|
|
369
369
|
| `*.tool.tsx` | tool an agent can call |
|
|
370
370
|
| `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
|
|
371
|
+
| `*.collection.ts` | content collection (`defineCollection`) — schema-typed markdown/JSON |
|
|
372
|
+
| `content/<name>/**` | that collection's files (markdown + frontmatter, or `.json`) |
|
|
371
373
|
| `page.tsx` (under `src/pages/`) | the route its DIRECTORY serves + `page.test.tsx` |
|
|
372
374
|
| `*.component.tsx` | exactly ONE component (+ types) |
|
|
373
375
|
| `*.component.ui.tsx` | presentational: one component, READS only — never writes |
|
|
@@ -728,7 +730,7 @@ each plugin's own README.
|
|
|
728
730
|
|
|
729
731
|
| Topic | Open | Summary |
|
|
730
732
|
|---|---|---|
|
|
731
|
-
| **What's new in 0.
|
|
733
|
+
| **What's new in 0.54.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. |
|
|
732
734
|
| 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. |
|
|
733
735
|
| 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. |
|
|
734
736
|
| 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. |
|
|
@@ -786,7 +788,7 @@ each plugin's own README.
|
|
|
786
788
|
| auth-workos | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-workos.md` (or `node_modules/@voltro/plugin-auth-workos/README.md`) | WorkOS AuthStrategy — verifies WorkOS AuthKit / SSO JWTs via JWKS (no API key) and maps org_id → tenantId. |
|
|
787
789
|
| billing | `node_modules/@voltro/cli/templates/agent-docs/plugins/billing.md` (or `node_modules/@voltro/plugin-billing/README.md`) | Subscriptions, plans, entitlements, and usage metering over a pluggable provider (Stripe + mock). Money is integer minor units. |
|
|
788
790
|
| broadcast | `node_modules/@voltro/cli/templates/agent-docs/plugins/broadcast.md` (or `node_modules/@voltro/plugin-broadcast/README.md`) | Cross-replica reactivity over a pub/sub bus (Redis / NATS) for non-postgres dialects — closes the single-instance gap so a write on one pod surfaces on another. |
|
|
789
|
-
| cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
|
|
791
|
+
| cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, Kafka, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
|
|
790
792
|
| clickhouse | `node_modules/@voltro/cli/templates/agent-docs/plugins/clickhouse.md` (or `node_modules/@voltro/plugin-clickhouse/README.md`) | Production-grade OLAP AnalyticsSink over ClickHouse — self-hosted or ClickHouse Cloud — for billions of events with millisecond aggregates. |
|
|
791
793
|
| comments | `node_modules/@voltro/cli/templates/agent-docs/plugins/comments.md` (or `node_modules/@voltro/plugin-comments/README.md`) | Comment threads on any app entity — replies, resolve/reopen, @-mentions with notifications, reactions, unread counters — live over the reactive engine, with an ejectable thread UI. |
|
|
792
794
|
| datadog | `node_modules/@voltro/cli/templates/agent-docs/plugins/datadog.md` (or `node_modules/@voltro/plugin-datadog/README.md`) | Agentless Datadog metrics exporter — pushes the unified Metrics-API to Datadog's /api/v2/series HTTP intake. |
|
|
@@ -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.54.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. |
|
|
@@ -67,7 +67,7 @@ each plugin's own README.
|
|
|
67
67
|
| auth-workos | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-workos.md` (or `node_modules/@voltro/plugin-auth-workos/README.md`) | WorkOS AuthStrategy — verifies WorkOS AuthKit / SSO JWTs via JWKS (no API key) and maps org_id → tenantId. |
|
|
68
68
|
| billing | `node_modules/@voltro/cli/templates/agent-docs/plugins/billing.md` (or `node_modules/@voltro/plugin-billing/README.md`) | Subscriptions, plans, entitlements, and usage metering over a pluggable provider (Stripe + mock). Money is integer minor units. |
|
|
69
69
|
| broadcast | `node_modules/@voltro/cli/templates/agent-docs/plugins/broadcast.md` (or `node_modules/@voltro/plugin-broadcast/README.md`) | Cross-replica reactivity over a pub/sub bus (Redis / NATS) for non-postgres dialects — closes the single-instance gap so a write on one pod surfaces on another. |
|
|
70
|
-
| cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
|
|
70
|
+
| cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, Kafka, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
|
|
71
71
|
| clickhouse | `node_modules/@voltro/cli/templates/agent-docs/plugins/clickhouse.md` (or `node_modules/@voltro/plugin-clickhouse/README.md`) | Production-grade OLAP AnalyticsSink over ClickHouse — self-hosted or ClickHouse Cloud — for billions of events with millisecond aggregates. |
|
|
72
72
|
| comments | `node_modules/@voltro/cli/templates/agent-docs/plugins/comments.md` (or `node_modules/@voltro/plugin-comments/README.md`) | Comment threads on any app entity — replies, resolve/reopen, @-mentions with notifications, reactions, unread counters — live over the reactive engine, with an ejectable thread UI. |
|
|
73
73
|
| datadog | `node_modules/@voltro/cli/templates/agent-docs/plugins/datadog.md` (or `node_modules/@voltro/plugin-datadog/README.md`) | Agentless Datadog metrics exporter — pushes the unified Metrics-API to Datadog's /api/v2/series HTTP intake. |
|
|
@@ -460,7 +460,7 @@
|
|
|
460
460
|
{
|
|
461
461
|
"slug": "cdc-out",
|
|
462
462
|
"title": "CDC-out (reverse-ETL)",
|
|
463
|
-
"description": "Declaratively mirror table changes outward to external sinks (webhook, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered.",
|
|
463
|
+
"description": "Declaratively mirror table changes outward to external sinks (webhook, Kafka, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered.",
|
|
464
464
|
"pkg": "@voltro/plugin-cdc-out",
|
|
465
465
|
"doc": "plugins/cdc-out.md",
|
|
466
466
|
"module": "agent-docs/plugins/cdc-out.md"
|
|
@@ -116,7 +116,7 @@ Env vars `providerFromEnv()` reads:
|
|
|
116
116
|
| `AI_PROVIDER` | `mock` | `mock` \| `anthropic` \| `openai` \| `gateway`. |
|
|
117
117
|
| `AI_MODEL` | per provider (see below) | Override the model. Defaults: `claude-opus-4-8` (anthropic), `gpt-5.5` (openai), `mock` (mock). **REQUIRED for `gateway`** — a `creator/model` id; boot throws if unset. |
|
|
118
118
|
|
|
119
|
-
Provider API keys are read by the underlying `@ai-sdk/*` packages from their standard env vars — `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AI_GATEWAY_API_KEY`. The framework doesn't read a separate `AI_API_KEY`. (To give a SINGLE agent its own key from code instead of env, see [Per-config key + base URL](#per-config-key
|
|
119
|
+
Provider API keys are read by the underlying `@ai-sdk/*` packages from their standard env vars — `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AI_GATEWAY_API_KEY`. The framework doesn't read a separate `AI_API_KEY`. (To give a SINGLE agent its own key from code instead of env, see [Per-config key + base URL](#per-config-key-base-url) below.)
|
|
120
120
|
|
|
121
121
|
## Anthropic
|
|
122
122
|
|
|
@@ -141,7 +141,7 @@ AI_MODEL=gpt-5.5
|
|
|
141
141
|
OPENAI_API_KEY=sk-…
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
Backed by `@ai-sdk/openai`. `AI_MODEL` is a plain OpenAI model id (`gpt-5.5`, `gpt-4o-mini`, …). For a self-hosted / Azure-style / proxy endpoint, set `baseURL` on a `ProviderConfig` (see [Per-config key + base URL](#per-config-key
|
|
144
|
+
Backed by `@ai-sdk/openai`. `AI_MODEL` is a plain OpenAI model id (`gpt-5.5`, `gpt-4o-mini`, …). For a self-hosted / Azure-style / proxy endpoint, set `baseURL` on a `ProviderConfig` (see [Per-config key + base URL](#per-config-key-base-url)) rather than an env var.
|
|
145
145
|
|
|
146
146
|
## Vercel AI Gateway
|
|
147
147
|
|
|
@@ -239,7 +239,7 @@ export default (input: { prompt: string }) =>
|
|
|
239
239
|
})
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
`GenerateTextOptions` is `{ prompt, system?, provider?, fallbacks?, maxTokens? }`. There is no `messages`/`effort` shape — the prompt is a single string the SDK wraps as the user turn; `system` steers it. `fallbacks` is a [provider fallback chain](#fallback-chain
|
|
242
|
+
`GenerateTextOptions` is `{ prompt, system?, provider?, fallbacks?, maxTokens? }`. There is no `messages`/`effort` shape — the prompt is a single string the SDK wraps as the user turn; `system` steers it. `fallbacks` is a [provider fallback chain](#fallback-chain-survive-a-provider-outage).
|
|
243
243
|
|
|
244
244
|
Structured output:
|
|
245
245
|
|
|
@@ -276,7 +276,7 @@ type ProviderConfig =
|
|
|
276
276
|
| { name: 'gateway'; model: `${string}/${string}`; apiKey?: string; baseURL?: string } // creator/model
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
The mock-only `mockText` / `script` can't appear on a real provider (the type rejects it), the gateway's `model` is a `creator/model`-typed string (a bare `'gpt-5.5'` is a compile error, not a boot crash), and the per-provider model-id types (`AnthropicModel` / `OpenAIModel`) are **open unions** — known ids autocomplete, but any string the provider ships tomorrow still type-checks. Use `provider` for per-request model selection (e.g. a cheaper model on a fallback path), or to pin a specific key/endpoint (see [Per-config key + base URL](#per-config-key
|
|
279
|
+
The mock-only `mockText` / `script` can't appear on a real provider (the type rejects it), the gateway's `model` is a `creator/model`-typed string (a bare `'gpt-5.5'` is a compile error, not a boot crash), and the per-provider model-id types (`AnthropicModel` / `OpenAIModel`) are **open unions** — known ids autocomplete, but any string the provider ships tomorrow still type-checks. Use `provider` for per-request model selection (e.g. a cheaper model on a fallback path), or to pin a specific key/endpoint (see [Per-config key + base URL](#per-config-key-base-url)).
|
|
280
280
|
|
|
281
281
|
For runtime provider switching across a whole layer, bind an `AiServiceImpl` to the `AiService` Context tag at boot and read it with `yield* AiService` — `defaultAiService` (backed by `providerFromEnv`) is the default.
|
|
282
282
|
|
|
@@ -2602,6 +2602,78 @@ A membership that ends mid-subscription therefore stops serving rows — the
|
|
|
2602
2602
|
caller's open ticket list drops the rows they can no longer see, without a
|
|
2603
2603
|
refresh and without the subscription having to be torn down.
|
|
2604
2604
|
|
|
2605
|
+
**On every transport, and that list is complete: the WebSocket, an [SSE
|
|
2606
|
+
stream](/docs/data/rest-routes#live-updates-over-http-stream-sse), and a
|
|
2607
|
+
[gRPC](/docs/data/grpc) server-streaming rpc.** All three open their
|
|
2608
|
+
subscription through the same code and resolve the filter per delivery, so
|
|
2609
|
+
"stays open for hours" never becomes a way to hold a stale predicate. The same
|
|
2610
|
+
delivery also re-checks the query's `guards:`; a filter resolution that FAILS
|
|
2611
|
+
revokes the subscription on all three rather than falling back to an unfiltered
|
|
2612
|
+
or empty read.
|
|
2613
|
+
|
|
2614
|
+
## Declare which tables it narrows — `tables:`
|
|
2615
|
+
|
|
2616
|
+
Optional, one line, and it buys back a feature the filter otherwise switches
|
|
2617
|
+
off for the whole app:
|
|
2618
|
+
|
|
2619
|
+
```ts
|
|
2620
|
+
setRowFilter({
|
|
2621
|
+
load,
|
|
2622
|
+
predicate,
|
|
2623
|
+
tables: ['bookmarks', 'recentSearches', 'todoSchedules', 'todoTags'],
|
|
2624
|
+
})
|
|
2625
|
+
```
|
|
2626
|
+
|
|
2627
|
+
**What it buys.** [Delta-resume](/docs/data/wire-protocol#reconnect-delta-resume)
|
|
2628
|
+
is excluded for a subscription whose row set is re-resolved per delivery —
|
|
2629
|
+
replaying deltas could serve rows the subject has since lost. Without a
|
|
2630
|
+
declaration the framework cannot tell which tables your predicate may reach, so
|
|
2631
|
+
it excludes them all: one registration disables cheap reconnects for every
|
|
2632
|
+
subscription in the process. A deployment measured a filter over 4 tables
|
|
2633
|
+
costing it on all 173 of their query descriptors, 55 of whose source tables the
|
|
2634
|
+
filter never touches. With the declaration, only subscriptions on the listed
|
|
2635
|
+
tables are excluded.
|
|
2636
|
+
|
|
2637
|
+
**Why a declaration and not a probe.** Resolving the scope at subscribe time
|
|
2638
|
+
and treating "returns `undefined` for this table" as safe is cheaper and
|
|
2639
|
+
unsound: your predicate is a function of freshly loaded context, so a table it
|
|
2640
|
+
does not narrow now may be narrowed on the next delivery — which is the entire
|
|
2641
|
+
reason the filter is re-resolved per delivery. A static list is a promise about
|
|
2642
|
+
every future resolution.
|
|
2643
|
+
|
|
2644
|
+
**It is verified, not trusted.** Returning a predicate for a table outside the
|
|
2645
|
+
list raises `RowFilterDeclarationViolated` at the read that did it — the
|
|
2646
|
+
request fails and a subscription is revoked. A declaration nobody checks is a
|
|
2647
|
+
comment, and this one is load-bearing: the resume grant is issued on its
|
|
2648
|
+
strength. Omit `tables:` entirely and nothing is verified and nothing resumes —
|
|
2649
|
+
the conservative default.
|
|
2650
|
+
|
|
2651
|
+
## Eager loads are refused, not silently unfiltered
|
|
2652
|
+
|
|
2653
|
+
A relation pulled in with `.with(...)` is resolved **below** the seam that
|
|
2654
|
+
applies the filter: the stores expand the eager tree themselves (the memory
|
|
2655
|
+
store recurses through its own raw read; the SQL stores fold the relation into
|
|
2656
|
+
one join). So a filtered table reached through a relation would come back
|
|
2657
|
+
unfiltered.
|
|
2658
|
+
|
|
2659
|
+
Rather than serve those rows, the read **refuses**, naming the table and both
|
|
2660
|
+
ways out:
|
|
2661
|
+
|
|
2662
|
+
```
|
|
2663
|
+
row filter: 'readers' is reached through an eager load on 'notes', and an eager
|
|
2664
|
+
relation is resolved BELOW the row filter — its rows would come back
|
|
2665
|
+
unfiltered. Refusing the read rather than serving it.
|
|
2666
|
+
→ read 'readers' as its own query (it is filtered there), or
|
|
2667
|
+
→ drop 'readers' from this .with(...) if the relation does not need
|
|
2668
|
+
row-level narrowing.
|
|
2669
|
+
```
|
|
2670
|
+
|
|
2671
|
+
Only relations reaching a table your filter actually narrows are affected —
|
|
2672
|
+
every other eager load is untouched. Applying the filter inside eager
|
|
2673
|
+
compilation is the real fix and it is a per-dialect change; until then this is
|
|
2674
|
+
a refusal rather than a leak, for the same reason the module refuses to
|
|
2675
|
+
fail open.
|
|
2676
|
+
|
|
2605
2677
|
## Row filters vs. guards
|
|
2606
2678
|
|
|
2607
2679
|
They answer different questions, and a complete policy usually wants both:
|
|
@@ -32,6 +32,7 @@ The dispatcher routes `voltro <command> [args]` to the matching subcommand and p
|
|
|
32
32
|
| Integrate | [`webhooks`](/docs/plugins/webhooks#voltro-webhooks-consumer-the-package-your-subscribers-install) (`consumer` / `events`) — generate the ZERO-dependency Standard-Webhooks verification package your subscribers install, from your own declared events (`--out` / `--name`); list the events a subscriber can register for (`--json`) |
|
|
33
33
|
| [Inspect & debug](/docs/cli/inspect) | `inspect`, `logs`, `traces`, `workflows`, `cluster`, `check` |
|
|
34
34
|
| [Health & surface](/docs/cli/build-and-start) | [`doctor`](/docs/cli/build-and-start) — serve preflight + the hand-roll detector (names the shipped primitive at the spot you're rebuilding it); [`capabilities`](/docs/cli/build-and-start) (`--json`) — the export surface read from your installed `@voltro/*`, so it can be verified instead of recalled; `info` (`--json`) — CLI / node / package-manager / dialect + every installed `@voltro/*` version, flagging lockstep skew (exits 1 on skew) |
|
|
35
|
+
| Mobile | `mobile` (`codegen` / `links`) — the Expo app's build-time steps: the typed rpc client + deep-link table, and the `apple-app-site-association` / `assetlinks.json` a universal link needs. `voltro codegen` inside a mobile app runs the same generators. |
|
|
35
36
|
| Harness | `test`, `e2e` |
|
|
36
37
|
| Cloud | `cloud` (`login` / `whoami` / `projects` / `env` / `import`); `login` is a top-level alias of `cloud login` |
|
|
37
38
|
| Secrets | `secret` (`generate [purpose]` — the right var+format per secret; `generate` alone → a generic secret; `list`) |
|
|
@@ -143,6 +144,8 @@ config value always winning:
|
|
|
143
144
|
| `VOLTRO_HSTS` | `Strict-Transport-Security` value. `off` drops just this one. |
|
|
144
145
|
| `VOLTRO_MAX_RPC_BODY_BYTES` | Cap on the buffered `POST /rpc` JSON body (default 8 MiB) — an oversized body is refused `413` and never buffered past the cap. File uploads ride plugin routes with their own limits. |
|
|
145
146
|
| `VOLTRO_MAX_BODY_BYTES` | Cap on every OTHER body read — plugin HTTP routes, REST routes, incoming webhooks (default 8 MiB, matching the rpc cap). The config-file spelling is `http.maxBodyBytes` in `app.config.ts`; per-route overrides (`defineRestRoute({ maxBodyBytes })`, a webhook handler's `maxBodyBytes`) win over both. Oversize is `413` for `Content-Length` and chunked alike. |
|
|
147
|
+
| `VOLTRO_CRDT_COMPACT_MAX_BYTES` | Size above which a merged `crdtText()` / `crdtDoc()` blob is soft-compacted (default 512 KiB, `0` disables). The config-file spelling is `crdt.compactMaxBytes` in `app.config.ts`; this variable wins over it. See [local-first](/docs/local-first/overview#rich-text-crdtdoc-usecrdtdoc-usecrdteditor). |
|
|
148
|
+
| `VOLTRO_GRPC_DRAIN_MS` | How long the [gRPC surface](/docs/data/grpc) lets open calls finish on shutdown before force-closing them (default 5000, `0` forces immediately). The config-file spelling is `grpc.drainMs`; this variable wins over it. Keep it below your orchestrator's termination grace. |
|
|
146
149
|
|
|
147
150
|
Response compression for the buffered non-rpc surfaces (and `voltro start`'s
|
|
148
151
|
HTML) is configured in the same `http:` block — `http.compression.{enabled,minBytes}`
|
|
@@ -2031,7 +2034,7 @@ package's README.
|
|
|
2031
2034
|
|
|
2032
2035
|
_voltro migrate — apply the declared schema through the declarative differ (an alias of voltro db apply)._
|
|
2033
2036
|
|
|
2034
|
-
`voltro migrate` applies your declared schema (`*.entity.ts` / `*.schema.ts` / `schema.ts`) to the configured database. It is an **alias of [`voltro db apply`](#the-declarative-workflow)**: it diffs the declared schema against the live database and emits the ALTERs, so a changed column or a new index actually lands.
|
|
2037
|
+
`voltro migrate` applies your declared schema (`*.entity.ts` / `*.schema.ts` / `schema.ts`) to the configured database. It is an **alias of [`voltro db apply`](#the-declarative-diff-workflow-voltro-db)**: it diffs the declared schema against the live database and emits the ALTERs, so a changed column or a new index actually lands.
|
|
2035
2038
|
|
|
2036
2039
|
> Before 0.11.4 this command was a create-only apply (`CREATE TABLE IF NOT EXISTS`, no diffing), which meant a column or type change reported success having applied **nothing**. If you need that bootstrap-only behaviour for a brand-new database, it is now `voltro migrate --create-only`.
|
|
2037
2040
|
|
|
@@ -1717,7 +1717,7 @@ created by every migration and diffed on every boot.
|
|
|
1717
1717
|
|
|
1718
1718
|
The other tempting option is to point `source:` at a name that resolves to
|
|
1719
1719
|
nothing. That is worse than the empty table: the [stale-`source` boot
|
|
1720
|
-
warning](#fan-out
|
|
1720
|
+
warning](#fan-out-how-many-subscribers-may-one-change-wake) is the only signal
|
|
1721
1721
|
for a subscription that has gone permanently quiet, and an exemption for a name
|
|
1722
1722
|
you invented disables it for the one case it was built for.
|
|
1723
1723
|
|
|
@@ -1847,7 +1847,7 @@ Two consequences worth knowing:
|
|
|
1847
1847
|
- **Not free per subscriber.** A publish wakes every subscriber of that channel
|
|
1848
1848
|
and re-runs each one's executor; the channel is one routing key, so
|
|
1849
1849
|
subscribers looking at different slices of the state are woken too. Publish on
|
|
1850
|
-
a real change, not on a timer — see [Fan-out](#fan-out
|
|
1850
|
+
a real change, not on a timer — see [Fan-out](#fan-out-how-many-subscribers-may-one-change-wake).
|
|
1851
1851
|
|
|
1852
1852
|
## Query Executor
|
|
1853
1853
|
|
|
@@ -2067,7 +2067,7 @@ materialised revision and the stream continues on the same revision line, so a
|
|
|
2067
2067
|
short offline gap costs a handful of patches instead of every row. Outside the
|
|
2068
2068
|
window, for computed queries, for row-filtered apps, or whenever anything is in
|
|
2069
2069
|
doubt, the query answers with a fresh snapshot — the delta-resume wire contract
|
|
2070
|
-
lives in [the wire protocol](/docs/data/wire-protocol#reconnect
|
|
2070
|
+
lives in [the wire protocol](/docs/data/wire-protocol#reconnect-delta-resume).
|
|
2071
2071
|
|
|
2072
2072
|
**What is on screen while that happens is your last-known-good data, not a
|
|
2073
2073
|
skeleton.** The replacement cache is seeded from the one it retires, so `data`
|
|
@@ -2172,6 +2172,15 @@ subscriber on every delivery, on purpose — a role revoked or a share withdrawn
|
|
|
2172
2172
|
to end the stream on the very NEXT delivery, not whenever a cache happens to
|
|
2173
2173
|
expire — and each of them can be a database round-trip.
|
|
2174
2174
|
|
|
2175
|
+
**On every transport.** A live query can leave the server three ways — the
|
|
2176
|
+
WebSocket the browser client uses, an [SSE
|
|
2177
|
+
stream](/docs/data/rest-routes#live-updates-over-http-stream-sse), and a
|
|
2178
|
+
[gRPC](/docs/data/grpc) server-streaming rpc — and all three resolve per
|
|
2179
|
+
delivery through the same code: guards re-checked before each frame, row
|
|
2180
|
+
visibility re-derived from the unfiltered base descriptor for each frame, and a
|
|
2181
|
+
revoked scope ending the stream. The transport decides how the frame is
|
|
2182
|
+
framed, never what the subject may see.
|
|
2183
|
+
|
|
2175
2184
|
So deliveries run **concurrently, up to a bound**. The default is 8 in flight.
|
|
2176
2185
|
Measured with 50 subscribers behind a 5 ms guard: 517 ms to serve all of them
|
|
2177
2186
|
serially, 72 ms at 8 lanes.
|
|
@@ -3205,7 +3214,9 @@ es.addEventListener('delta', (e) => applyDelta(JSON.parse(e.data)))
|
|
|
3205
3214
|
|
|
3206
3215
|
Each event's `_tag` becomes the SSE `event:` name, so a client listens per kind instead of switching on a payload field. The framing handles the details that bite otherwise: embedded newlines are split across `data:` lines (a raw `\n` would truncate the event), a `retry:` hint is sent, and a keep-alive comment goes out every 15s so proxies don't drop an idle stream.
|
|
3207
3216
|
|
|
3208
|
-
Same guarantees as the WebSocket path, because it is the same code: the declarative `guards:`, the row filter and tenant scoping all run before anything is emitted, and the client's disconnect tears the subscription down (including a disconnect *during* setup). A guard denial arrives as one `error` event rather than an HTTP status — by then the response headers are already sent.
|
|
3217
|
+
Same guarantees as the WebSocket path, because it is the same code: the declarative `guards:`, the [row filter](/docs/authentication/row-level-security) and tenant scoping all run before anything is emitted, and the client's disconnect tears the subscription down (including a disconnect *during* setup). A guard denial arrives as one `error` event rather than an HTTP status — by then the response headers are already sent.
|
|
3218
|
+
|
|
3219
|
+
And they run before **every** event, not only before the first one: the guards are re-checked and the subject's row visibility is re-resolved from the unfiltered base descriptor per delivery, so a scope revoked while the `EventSource` is open ends the stream on the next event, and a membership that ends stops carrying those rows in the next `delta`. An open SSE stream is not a cheaper read path than a fresh `GET`.
|
|
3209
3220
|
|
|
3210
3221
|
`stream: 'sse'` on a mutation or action is ignored: there is nothing to subscribe to.
|
|
3211
3222
|
|
|
@@ -4999,10 +5010,22 @@ A resume is declined — always with a fresh snapshot — when:
|
|
|
4999
5010
|
- the resuming caller is a different subject or tenant (a login, logout or
|
|
5000
5011
|
tenant switch between disconnect and resume) — the retained history is keyed
|
|
5001
5012
|
by subject AND tenant, so a changed identity simply never finds it;
|
|
5002
|
-
- the
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5013
|
+
- the query is a **computed** query — it re-runs a handler, so there is no
|
|
5014
|
+
delta chain to replay;
|
|
5015
|
+
- a registered row filter (`setRowFilter`) can narrow THIS subscription's
|
|
5016
|
+
source table, or the query declares an eager `.with(...)`. A row-filtered
|
|
5017
|
+
subscription's visible row set exists only per delivery, so replaying it
|
|
5018
|
+
could serve rows the subject has since lost.
|
|
5019
|
+
|
|
5020
|
+
**This is per table, not per app.** A filter that declares
|
|
5021
|
+
`tables: [...]` (see [row-level security](/docs/authentication/row-level-security))
|
|
5022
|
+
keeps delta-resume on every subscription whose source is not in that set —
|
|
5023
|
+
the common case, since most filters narrow a handful of tables. Without the
|
|
5024
|
+
declaration the framework cannot know which tables the predicate may reach
|
|
5025
|
+
and excludes them all, which is what a deployment measured as one filter over
|
|
5026
|
+
4 tables costing the feature on all 173 of their queries. Eager loads are
|
|
5027
|
+
excluded wholesale because a relation resolves below the seam that narrows.
|
|
5028
|
+
They reconnect with a fresh
|
|
5006
5029
|
snapshot, exactly as before.
|
|
5007
5030
|
|
|
5008
5031
|
**Author a live-subscribed getter to return, not throw.** A subscription is a
|
|
@@ -6101,6 +6124,8 @@ export default {
|
|
|
6101
6124
|
port: 50051,
|
|
6102
6125
|
procedures: ['orders.get', 'orders.list', 'orders.create'],
|
|
6103
6126
|
// tls: { certPath, keyPath, caPath? } — plaintext without it (dev / mesh).
|
|
6127
|
+
// drainMs: 5000, // shutdown drain budget — see below
|
|
6128
|
+
// maxMessageBytes, maxMetadataBytes — grpc-js frame limits
|
|
6104
6129
|
},
|
|
6105
6130
|
}
|
|
6106
6131
|
```
|
|
@@ -6112,6 +6137,21 @@ works out of the box). The gRPC packages ship as script-free optional
|
|
|
6112
6137
|
dependencies of `@voltro/cli`; a configured `grpc:` block with them missing
|
|
6113
6138
|
refuses the boot by name.
|
|
6114
6139
|
|
|
6140
|
+
## Shutdown drains, then forces — `drainMs`
|
|
6141
|
+
|
|
6142
|
+
On SIGTERM the surface flips its health status to `NOT_SERVING` (so a load
|
|
6143
|
+
balancer stops sending it work) and gives open calls **`drainMs`** to finish
|
|
6144
|
+
before force-closing them. Default `5000`; `0` forces immediately; the env
|
|
6145
|
+
override is `VOLTRO_GRPC_DRAIN_MS`.
|
|
6146
|
+
|
|
6147
|
+
Pick it from two numbers only you have. Keep it **below** your orchestrator's
|
|
6148
|
+
termination grace (`terminationGracePeriodSeconds`, `docker stop -t`) — past
|
|
6149
|
+
that point SIGKILL arrives and the drain never completes, so a larger budget
|
|
6150
|
+
buys nothing. Keep it **above** your longest legitimately in-flight unary
|
|
6151
|
+
call, or every rolling deploy force-closes work that would have finished. When
|
|
6152
|
+
the budget is exceeded the surface says so in a warning naming the budget,
|
|
6153
|
+
rather than leaking the port into the next boot.
|
|
6154
|
+
|
|
6115
6155
|
## Field numbers are managed — `grpc.manifest.json`
|
|
6116
6156
|
|
|
6117
6157
|
Field numbers are the proto wire identity, so they may never depend on
|
|
@@ -6169,10 +6209,19 @@ sleeping action whose post-sleep write never lands).
|
|
|
6169
6209
|
A `query` becomes a **server-streaming** rpc: each frame is the CURRENT full
|
|
6170
6210
|
snapshot, re-pushed live when the query's `source:` changes — subscribe,
|
|
6171
6211
|
mutate from anywhere, and the open stream receives the new frame with no
|
|
6172
|
-
re-request.
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6212
|
+
re-request.
|
|
6213
|
+
|
|
6214
|
+
**Authorization is re-derived per FRAME, not frozen at open.** Before every
|
|
6215
|
+
delivery the framework re-runs the query's `guards:` and re-resolves the
|
|
6216
|
+
subject's [row-level visibility](/docs/authentication/row-level-security)
|
|
6217
|
+
from the unfiltered base descriptor. A revoked scope ends the stream with the
|
|
6218
|
+
mapped status; a membership that ends mid-stream stops carrying those rows in
|
|
6219
|
+
the next frame, with the stream itself untouched. This is the same code the
|
|
6220
|
+
WebSocket and SSE transports run — an open gRPC stream is not a cheaper read
|
|
6221
|
+
path than a fresh call.
|
|
6222
|
+
|
|
6223
|
+
Slow consumers are handled through grpc-js write backpressure — frames
|
|
6224
|
+
coalesce to the latest snapshot rather than buffering unboundedly.
|
|
6176
6225
|
|
|
6177
6226
|
## Declared limits (v1)
|
|
6178
6227
|
|
|
@@ -627,7 +627,7 @@ await ctx.store.update('notes', id, {
|
|
|
627
627
|
|
|
628
628
|
Two layers of validation apply:
|
|
629
629
|
|
|
630
|
-
1. **JSON validity** — that the stored bytes are well-formed JSON — is enforced automatically by the database on every dialect (see [Storage + validation per dialect](#storage
|
|
630
|
+
1. **JSON validity** — that the stored bytes are well-formed JSON — is enforced automatically by the database on every dialect (see [Storage + validation per dialect](#storage-validation-per-dialect)). You don't declare anything.
|
|
631
631
|
2. **JSON *shape*** — that the value matches your expected structure — is up to you: enforce it at the table level with `table().validate(Schema)`:
|
|
632
632
|
|
|
633
633
|
```ts
|
|
@@ -700,7 +700,7 @@ A handler that a `voltro dev` session or a test actually ran is reported with wh
|
|
|
700
700
|
|
|
701
701
|
### What the codemod does per kind
|
|
702
702
|
|
|
703
|
-
- **`rename-column`** gets a real `transform`: it renames the field in the `*.entity.ts` AND chains **`.renamedFrom('old')`** (so the differ plans a catalog RENAME, not the lossy drop+create described [above](#
|
|
703
|
+
- **`rename-column`** gets a real `transform`: it renames the field in the `*.entity.ts` AND chains **`.renamedFrom('old')`** (so the differ plans a catalog RENAME, not the lossy drop+create described [above](#renamedfrom-oldname)), then annotates the handler sites the blast radius found.
|
|
704
704
|
- **`retype-column` / `split-column` / `drop-column` / `rename-table`** are reshaping changes with no single mechanical rewrite, so they get a **`manual`** codemod: a generated, numbered checklist of the edits + the annotation to add, printed for you to apply.
|
|
705
705
|
|
|
706
706
|
`voltro evolve` produces the plan; it does not apply the schema change. **`voltro check` is the gate on the result**, and `voltro db apply` lands it — after `--write`, review the annotated handlers, then run those two.
|
|
@@ -391,7 +391,7 @@ What the framework hides for you vs what's worth knowing. Per-dialect pages dril
|
|
|
391
391
|
| `RETURNING *` on DELETE | yes | no | yes (10.0+) | OUTPUT DELETED.* | yes | yes |
|
|
392
392
|
| Parameterized `LIMIT ?` | yes | no — integer-literal inlined | yes | no — integer-literal inlined | yes | yes |
|
|
393
393
|
| `LIMIT N OFFSET N` syntax | yes | yes | yes | no — `OFFSET … ROWS FETCH NEXT … ROWS ONLY` | yes | yes |
|
|
394
|
-
| DEFAULT on TEXT columns | yes | **NO** —
|
|
394
|
+
| DEFAULT on TEXT columns | yes | **NO** — framework emits VARCHAR(255) | yes — framework still emits VARCHAR(255) (engine parity) | yes — framework emits NVARCHAR(450) (indexable) | yes | yes |
|
|
395
395
|
| Native JSON column type | JSONB | JSON | JSON | NVARCHAR(MAX) | TEXT | TEXT |
|
|
396
396
|
| JSON columns returned as objects | yes | yes | yes | **no — strings** — framework auto-parses | **no — strings** — framework auto-parses | **no — strings** — framework auto-parses |
|
|
397
397
|
| Booleans | proper booleans | 0/1 (TINYINT) | 0/1 | BIT (proper bool) | 0/1 (INTEGER) | 0/1 (INTEGER) |
|
|
@@ -796,11 +796,48 @@ The framework's DDL emitter detects the case and switches to `VARCHAR(255)`:
|
|
|
796
796
|
```typescript
|
|
797
797
|
text().default('json') // → VARCHAR(255) DEFAULT 'json'
|
|
798
798
|
text().oneOf(['a', 'b', 'c']).default('a') // → VARCHAR(255) DEFAULT 'a' CHECK (col IN ('a','b','c'))
|
|
799
|
-
text().nullable() // →
|
|
799
|
+
text().nullable() // → LONGTEXT (unchanged — no default to trip up)
|
|
800
800
|
```
|
|
801
801
|
|
|
802
802
|
VARCHAR(255) is the framework's heuristic — enough for typical enum-like values, short status strings, format identifiers. If you need longer defaulted text, declare the column as `text().nullable()` + handle the missing-default case in application code, OR drop down to `unsafe()`.
|
|
803
803
|
|
|
804
|
+
### Adding a default to an existing column
|
|
805
|
+
|
|
806
|
+
The rule holds for a migration too, not only for `CREATE TABLE`. Adding
|
|
807
|
+
`.default(…)` to a `text()` column that already exists **reshapes** the column
|
|
808
|
+
rather than setting a default on it:
|
|
809
|
+
|
|
810
|
+
```sql
|
|
811
|
+
ALTER TABLE tickets MODIFY COLUMN `status` VARCHAR(255) NOT NULL DEFAULT 'active'
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
That is deliberate, and it is what makes the change appliable at all: a plain
|
|
815
|
+
`ALTER TABLE … ALTER COLUMN status SET DEFAULT 'active'` is answered by MySQL
|
|
816
|
+
with `BLOB, TEXT, GEOMETRY or JSON column 'status' can't have a default value`,
|
|
817
|
+
so the migration would stop half-applied. Reshaping means the column has the same
|
|
818
|
+
type whether the default was declared before or after the table existed.
|
|
819
|
+
|
|
820
|
+
Two consequences worth knowing before you run it:
|
|
821
|
+
|
|
822
|
+
- **It is a narrowing.** If a row already holds more than 255 characters, the
|
|
823
|
+
ALTER fails (`Data too long for column 'status'`) and the migration stops
|
|
824
|
+
before it. Check first, and pick the width yourself with
|
|
825
|
+
`text().maxLength(n).default(…)` if 255 is too small:
|
|
826
|
+
|
|
827
|
+
```sql
|
|
828
|
+
SELECT COUNT(*) FROM tickets WHERE CHAR_LENGTH(status) > 255
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
- **Removing a default does not reshape back.** `DROP DEFAULT` is legal on any
|
|
832
|
+
mysql type, and widening a `VARCHAR(255)` back to `LONGTEXT` would fail for an
|
|
833
|
+
indexed column — so the column keeps its bounded type. Declare
|
|
834
|
+
`text().maxLength(255)` if you want that to be visible in the schema.
|
|
835
|
+
|
|
836
|
+
SQL Server does the same thing for its own reason (an `NVARCHAR(MAX)` column
|
|
837
|
+
cannot be indexed, so a defaulted text column is `NVARCHAR(450)`). On postgres a
|
|
838
|
+
`TEXT` column takes a `DEFAULT` directly, and SQLite rebuilds the table to the
|
|
839
|
+
declared shape — neither reshapes anything.
|
|
840
|
+
|
|
804
841
|
## JSON columns
|
|
805
842
|
|
|
806
843
|
`json()` columns emit `JSON` (mysql's native binary JSON type since 5.7+). The driver auto-parses on read; same shape as postgres. No coercion overhead.
|
|
@@ -1187,6 +1224,31 @@ When the caller didn't supply an `orderBy` but did set `skip` (uncommon but lega
|
|
|
1187
1224
|
|
|
1188
1225
|
The compiler inlines integer literals for TOP/OFFSET/FETCH NEXT values rather than parameter binding. Same rationale as MySQL — tedious has bind-as-INT issues with large or unexpected-typed numeric params.
|
|
1189
1226
|
|
|
1227
|
+
## Text columns with a DEFAULT — NVARCHAR(450)
|
|
1228
|
+
|
|
1229
|
+
A plain `text()` column is `NVARCHAR(MAX)`, which SQL Server cannot index. A text
|
|
1230
|
+
column that carries a literal default or a closed value set is therefore emitted
|
|
1231
|
+
bounded, at `NVARCHAR(450)` — under the 900-byte single-column key limit, so it
|
|
1232
|
+
stays indexable:
|
|
1233
|
+
|
|
1234
|
+
```typescript
|
|
1235
|
+
text().default('open') // → NVARCHAR(450) + a DEFAULT constraint
|
|
1236
|
+
text().oneOf(['open', 'closed']) // → NVARCHAR(450) + a CHECK constraint
|
|
1237
|
+
text().nullable() // → NVARCHAR(MAX)
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
This holds for migrations as well as for `CREATE TABLE`: adding `.default(…)` to
|
|
1241
|
+
an existing text column retypes it to `NVARCHAR(450)` and then adds the default
|
|
1242
|
+
constraint, so the column has the same type whether the default was declared
|
|
1243
|
+
before or after the table existed. It is a narrowing — if a row already holds
|
|
1244
|
+
more than 450 characters, the `ALTER COLUMN` fails ("String or binary data would
|
|
1245
|
+
be truncated") and the migration stops there. Check first, and use
|
|
1246
|
+
`text().maxLength(n).default(…)` to choose a different width:
|
|
1247
|
+
|
|
1248
|
+
```sql
|
|
1249
|
+
SELECT COUNT(*) FROM tickets WHERE LEN(status) > 450
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1190
1252
|
## JSON columns — NVARCHAR(MAX) + auto-parse
|
|
1191
1253
|
|
|
1192
1254
|
`json()` columns emit `NVARCHAR(MAX)` in DDL — mssql has no native JSON type pre-2025. Validation goes through `ISJSON(col) = 1` CHECK constraints; serialization is application-side.
|
|
@@ -78,6 +78,8 @@ Server-side, the active locale is determined by, in priority order:
|
|
|
78
78
|
2. **`Accept-Language` header** — the browser/OS preference, q-weighted and sorted per RFC 4647.
|
|
79
79
|
3. **`defaultLocale`** — last-resort fallback.
|
|
80
80
|
|
|
81
|
+
**One resolver decides, and everything the server renders for that request reads its answer** — the page render and its `<I18nProvider>`, the `<html lang>` attribute, the ISR cache key (so a language switch cannot re-serve the previous locale's cached HTML), and the validation errors an [`<AutoForm>` renders on the no-JavaScript form-POST path](/docs/ui/forms-and-tables#forms-without-javascript). That last one is worth naming because a server has no `<html lang>` to read yet at the time it validates; deriving the locale a second way there would answer `en` for every request.
|
|
82
|
+
|
|
81
83
|
The resolved locale is **guaranteed** to be one of the codes in `locales`. Any unsupported value (a cookie pointing at a code you no longer ship, a browser asking for `xx-YY`) falls through to the next signal. RFC 4647 lookup strips subtags one segment at a time — `de-CH-1996` → `de-CH` → `de` — so a `de` catalog serves a `de-CH` browser.
|
|
82
84
|
|
|
83
85
|
The client **adopts what the server resolved**, reading it from the `<html lang>` attribute the server render sets, then falling back to the cookie and the default. `Accept-Language` is never read in the browser: `navigator.languages` can diverge from what the server saw.
|