@voltro/cli 0.22.0 → 0.23.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 +383 -0
- package/dist/apiBuild-CD-4JLLA.js +2 -0
- package/dist/{apiBuild-2zRxj2hJ.js → apiBuild-JQtIhZPy.js} +2 -2
- package/dist/bin.js +3 -3
- package/dist/{commands-DGaeTn2f.js → commands-Bsu9Buln.js} +1764 -1492
- package/dist/dbCommand-CSguuUQn.js +2 -0
- package/dist/{dbCommand-DVASmZj2.js → dbCommand-GI7-BVZt.js} +440 -259
- package/dist/{dev-BEYBSGJ7.js → dev-DRxF_qOB.js} +1 -1
- package/dist/{dev-DeHVUxQx.js → dev-jM07kq_D.js} +1479 -1479
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-4AVPn6IS.js → inspectMetrics-dcC7gnxu.js} +603 -560
- package/dist/{serveCommand-DEVTtnLy.js → serveCommand-Cl8sicD5.js} +230 -229
- package/dist/serveEntry.js +2 -2
- package/dist/{start-BjGUIIvj.js → start-C_4VUDT8.js} +327 -325
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/_manifest.json +1 -1
- package/templates/agent-docs/caching.md +21 -5
- package/templates/agent-docs/cli.md +44 -0
- package/templates/agent-docs/data.md +23 -5
- package/templates/agent-docs/database/migrations.md +94 -0
- package/templates/agent-docs/plugins.md +42 -0
- package/templates/agent-docs/routing.md +23 -1
- package/templates/agent-docs/templates/apibackends.md +2 -2
- package/templates/agent-docs/whats-new.md +203 -136
- 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-storage/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-search/package.json +8 -8
- package/templates/apps/api-versioning/app.config.ts +6 -4
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- 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-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/dist/apiBuild-sqNmWmY5.js +0 -2
- package/dist/dbCommand-CO3eSAZR.js +0 -2
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { Z as e } from "./inspectMetrics-dcC7gnxu.js";
|
|
2
|
+
import { t } from "./start-C_4VUDT8.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|
|
@@ -62,22 +62,22 @@
|
|
|
62
62
|
"@effect/platform-node": "^0.108.0",
|
|
63
63
|
"@effect/sql": "^0.52.0",
|
|
64
64
|
"@effect/workflow": "^0.19.0",
|
|
65
|
-
"@voltro/ai": "0.
|
|
66
|
-
"@voltro/cache": "0.
|
|
67
|
-
"@voltro/data-transfer": "0.
|
|
68
|
-
"@voltro/database": "0.
|
|
69
|
-
"@voltro/env": "0.
|
|
70
|
-
"@voltro/kv": "0.
|
|
71
|
-
"@voltro/logger": "0.
|
|
72
|
-
"@voltro/plugin-auth": "0.
|
|
73
|
-
"@voltro/plugin-broadcast": "0.
|
|
74
|
-
"@voltro/plugin-mail": "0.
|
|
75
|
-
"@voltro/plugin-storage": "0.
|
|
76
|
-
"@voltro/plugin-webhooks": "0.
|
|
77
|
-
"@voltro/protocol": "0.
|
|
78
|
-
"@voltro/runtime": "0.
|
|
79
|
-
"@voltro/serverless": "0.
|
|
80
|
-
"@voltro/workflow": "0.
|
|
65
|
+
"@voltro/ai": "0.23.0",
|
|
66
|
+
"@voltro/cache": "0.23.0",
|
|
67
|
+
"@voltro/data-transfer": "0.23.0",
|
|
68
|
+
"@voltro/database": "0.23.0",
|
|
69
|
+
"@voltro/env": "0.23.0",
|
|
70
|
+
"@voltro/kv": "0.23.0",
|
|
71
|
+
"@voltro/logger": "0.23.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.23.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.23.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.23.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.23.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.23.0",
|
|
77
|
+
"@voltro/protocol": "0.23.0",
|
|
78
|
+
"@voltro/runtime": "0.23.0",
|
|
79
|
+
"@voltro/serverless": "0.23.0",
|
|
80
|
+
"@voltro/workflow": "0.23.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -595,7 +595,7 @@ each plugin's own README.
|
|
|
595
595
|
|
|
596
596
|
| Topic | Open | Summary |
|
|
597
597
|
|---|---|---|
|
|
598
|
-
| **What's new in 0.
|
|
598
|
+
| **What's new in 0.23.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. |
|
|
599
599
|
| 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. |
|
|
600
600
|
| 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. |
|
|
601
601
|
| 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.23.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. |
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"group": "database.migrations",
|
|
90
90
|
"description": "Voltro's planner-based migration system — diff your declared schema against the live DB, classify each change, refuse-to-apply anything risky without explicit intent. Dev auto-applies, prod refuses.",
|
|
91
91
|
"path": "agent-docs/database/migrations.md",
|
|
92
|
-
"files":
|
|
92
|
+
"files": 17
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"id": "database/misc",
|
|
@@ -286,12 +286,13 @@ The matching `.<primitive>.server.ts` is **unchanged** — caching is a descript
|
|
|
286
286
|
|
|
287
287
|
## `scope` is required — and it's a security decision
|
|
288
288
|
|
|
289
|
-
`scope` has **no default**, because guessing wrong leaks one user's rows to another. The rubric is one question:
|
|
289
|
+
`scope` has **no default**, because guessing wrong leaks one user's rows to another. The rubric is one question, with **three** answers:
|
|
290
290
|
|
|
291
|
-
> **Does the resolved query depend on the caller?**
|
|
291
|
+
> **Does the resolved query depend on the caller — and on what about them?**
|
|
292
292
|
|
|
293
|
-
- **
|
|
294
|
-
- **
|
|
293
|
+
- **On the PERSON** — `where authorId = me`, anything row-scoped → **`scope: 'subject'`**. The cache key includes the caller's subject id, so two subjects can never share an entry.
|
|
294
|
+
- **On their ORG only** — an org-wide figure every colleague sees identically → **`scope: 'tenant'`**. One entry per `tenantId`, never shared across orgs.
|
|
295
|
+
- **On neither** — the same rows for everyone (reference / lookup data) → **`scope: 'global'`**. One entry shared across all callers.
|
|
295
296
|
|
|
296
297
|
```ts
|
|
297
298
|
// reference data — identical for everyone → global
|
|
@@ -302,9 +303,24 @@ export const listCountries = defineQuery({
|
|
|
302
303
|
output: Country,
|
|
303
304
|
cache: { ttl: '1h', scope: 'global' },
|
|
304
305
|
})
|
|
306
|
+
|
|
307
|
+
// an org-wide statistic — same for all 18 colleagues, never across orgs
|
|
308
|
+
export const last12Months = defineQuery({
|
|
309
|
+
name: 'globalStatistics.last12Months',
|
|
310
|
+
source: ['invoices', 'employees'],
|
|
311
|
+
input: Schema.Struct({}),
|
|
312
|
+
output: Stats,
|
|
313
|
+
cache: { ttl: '5m', scope: 'tenant' },
|
|
314
|
+
})
|
|
305
315
|
```
|
|
306
316
|
|
|
307
|
-
|
|
317
|
+
`'tenant'` exists because the other two were the only options and neither fit an org-wide figure: `'subject'` recomputes it per person — eighteen identical computations of the same nine-table statistic for an eighteen-person org — and `'global'` shares one entry across tenant boundaries, which for data derived from `subject.tenantId` is not a cache but a leak.
|
|
318
|
+
|
|
319
|
+
A caller with no `tenantId` (an anonymous or system subject) **bypasses** a `'tenant'` cache rather than sharing a null-keyed entry.
|
|
320
|
+
|
|
321
|
+
**Never put `scope: 'global'` on a subject- or tenant-filtered query.** Tenant tables are auto-scoped by the runtime, so a `global` cache over one would serve tenant A's rows to tenant B. **The boot audit checks this**: a `'global'` scope over a `tenant()`-scoped table is reported by `voltro dev` and refused under `VOLTRO_SERVER_ONLY=strict`. It stays silent for `'global'` on reference data — the case the option exists for — and for a query with no declared `source`, where it has nothing to reason about.
|
|
322
|
+
|
|
323
|
+
**`scope: 'tenant'` is not a replacement for modelling.** For a rollup, an aggregate with `tenantId` as an indexed column puts the tenant boundary in the *data* rather than in a cache key, which is better. `'tenant'` is for the other case: a query that must be FRESH and is merely expensive, where an aggregate's refresh interval is the wrong instrument.
|
|
308
324
|
|
|
309
325
|
## How auto-invalidation works
|
|
310
326
|
|
|
@@ -1069,6 +1069,21 @@ source counts as a guard, so `requireTeamAccess()` is recognised without any
|
|
|
1069
1069
|
configuration. Without that the scan would report every call site of your own
|
|
1070
1070
|
guards, which is the failure mode that makes a check ignorable.
|
|
1071
1071
|
|
|
1072
|
+
It reads your **whole source tree** for those names, not just the
|
|
1073
|
+
convention-named files — guards live in `lib/access.ts`, not in `*.mutation.ts`.
|
|
1074
|
+
The line above the counts tells you what it found, and it is worth reading before
|
|
1075
|
+
you trust the numbers:
|
|
1076
|
+
|
|
1077
|
+
```txt
|
|
1078
|
+
guard vocabulary: 17 from your source (requireTeamAccess, assertInquiryAccess, …)
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
If it instead says `framework names only — no exported require*/assert* found in
|
|
1082
|
+
this app` while you know you export some, the counts below it are not meaningful:
|
|
1083
|
+
every call site of your own guards is being reported as unguarded. Check that
|
|
1084
|
+
they are `export`ed and that the name starts with `require` / `assert` followed
|
|
1085
|
+
by a capital.
|
|
1086
|
+
|
|
1072
1087
|
**An inline ownership check is informational.** `row.userId !== subject.id → new
|
|
1073
1088
|
AccessDeniedError({})` is correct code — it is listed so you can see where the
|
|
1074
1089
|
rule lives in a handler rather than on a descriptor, and it never fails the run.
|
|
@@ -2606,6 +2621,35 @@ voltro update --codemods-only --from 0.5.0 --to 0.6.0
|
|
|
2606
2621
|
|
|
2607
2622
|
Every `@voltro/*` entry in `dependencies` and `devDependencies` — in every workspace member, see above — with the range style preserved (`^0.3.0` stays caret, `~0.3.0` stays tilde) unless you pass `--exact`. Non-registry specs (`workspace:*`, `catalog:`, `link:`, …) are left untouched — they're already resolved by your monorepo or catalog.
|
|
2608
2623
|
|
|
2624
|
+
### And the peer dependencies the framework requires
|
|
2625
|
+
|
|
2626
|
+
`@effect/*` are **peer** dependencies, so your app declares them directly. When a
|
|
2627
|
+
release moves its peer range, bumping only `@voltro/*` leaves you installed
|
|
2628
|
+
against the old ones:
|
|
2629
|
+
|
|
2630
|
+
```txt
|
|
2631
|
+
Aligning peer dependencies the framework requires:
|
|
2632
|
+
@effect/rpc ^0.75.1 → ^0.76.0 (apps/api/package.json)
|
|
2633
|
+
@effect/platform ^0.96.2 → ^0.97.0 (apps/api/package.json)
|
|
2634
|
+
```
|
|
2635
|
+
|
|
2636
|
+
`update` reads those requirements off the freshly installed `@voltro/*` packages
|
|
2637
|
+
and re-installs if anything moved. Without it your package manager only *warns*,
|
|
2638
|
+
and the app compiles and boots on a graph the framework was never tested against
|
|
2639
|
+
— which is the failure mode with no symptom until there is one.
|
|
2640
|
+
|
|
2641
|
+
It is deliberately conservative:
|
|
2642
|
+
|
|
2643
|
+
- **Only peers you already declare.** One resolved transitively is not `update`'s
|
|
2644
|
+
to add.
|
|
2645
|
+
- **Only when your range is genuinely lower.** Pinned ahead, or pinned exactly at
|
|
2646
|
+
the floor (`0.76.0` vs `^0.76.0`), is left alone — that is a choice.
|
|
2647
|
+
- **Only ranges it can judge** (`^`, `~`, `>=`, exact). A union (`^1 || ^2`), a
|
|
2648
|
+
bounded range, `workspace:` / `catalog:` — untouched.
|
|
2649
|
+
|
|
2650
|
+
If two framework packages disagree about one peer, it says so and changes
|
|
2651
|
+
nothing: that is our bug, not yours to absorb silently.
|
|
2652
|
+
|
|
2609
2653
|
## When the install cannot run on this host
|
|
2610
2654
|
|
|
2611
2655
|
Some projects install in a container with their own store, from an offline
|
|
@@ -953,7 +953,11 @@ try {
|
|
|
953
953
|
|
|
954
954
|
**Trace id for debugging.** An error caught from `useMutation` / `useAction` carries a **non-enumerable `__voltroTraceId`** — the bridge to the server logs for that exact call:
|
|
955
955
|
|
|
956
|
+
```ts
|
|
957
|
+
const traceId = (err as { __voltroTraceId?: string }).__voltroTraceId
|
|
958
|
+
// → `voltro logs --trace <traceId>` to see the server-side span
|
|
956
959
|
```
|
|
960
|
+
|
|
957
961
|
`errorTag` lives in `@voltro/protocol` rather than in the client for a reason
|
|
958
962
|
worth knowing before you decide where your own error handling goes: **`_tag` is a
|
|
959
963
|
wire concept, and protocol owns the wire.** So a shared error handler in a package
|
|
@@ -961,10 +965,6 @@ that has no business depending on `@voltro/client` — a UI kit, an i18n layer
|
|
|
961
965
|
can read a tag without taking that dependency. `instanceof` is the thing that
|
|
962
966
|
does NOT survive the wire: what arrives in the browser was decoded from JSON and
|
|
963
967
|
never constructed, so match on the tag, not on the class.
|
|
964
|
-
ts
|
|
965
|
-
const traceId = (err as { __voltroTraceId?: string }).__voltroTraceId
|
|
966
|
-
// → `voltro logs --trace <traceId>` to see the server-side span
|
|
967
|
-
```
|
|
968
968
|
|
|
969
969
|
**Exhaustive matching with the generated `matchError`.** Codegen emits a per-app `matchError` (plus `AppError` / `AppErrorTag`) into `rpcGroup.generated.ts`, derived by reference from every descriptor's `error:` schema + your plugins' cross-cutting errors — so there's no hand-maintained tag list to drift out of date (a dead/renamed tag is a compile error):
|
|
970
970
|
|
|
@@ -1009,6 +1009,24 @@ nobody wrote a client call for it, it is on the wire — the same reasoning as
|
|
|
1009
1009
|
whatever authority its caller has. This removes the wire surface, not the need to
|
|
1010
1010
|
check who is asking; `voltro doctor`'s authz scan still covers it.
|
|
1011
1011
|
|
|
1012
|
+
**It cannot be combined with `publicApi` or `exposeAsTool`.** Those add a REST
|
|
1013
|
+
route and an agent tool respectively — opt-ins to a *different* surface — so a
|
|
1014
|
+
procedure carrying both would be unreachable from your own client and reachable
|
|
1015
|
+
from the internet. That combination throws where it is declared:
|
|
1016
|
+
|
|
1017
|
+
```text
|
|
1018
|
+
auditLog.createFromAction: `internal: true` cannot be combined with `publicApi`.
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
Neither silent resolution would be right: dropping the REST route breaks a live
|
|
1022
|
+
endpoint invisibly, and keeping it defeats the flag. Drop `internal: true` if the
|
|
1023
|
+
wider surface is intended, or remove the annotation if it is not.
|
|
1024
|
+
|
|
1025
|
+
The flag also removes the procedure from `voltro dev`'s inspect invoker, so the
|
|
1026
|
+
devtools "invoke" panel will not list it. That is deliberate — an internal
|
|
1027
|
+
procedure is the one most likely to carry no guard, since "only server code calls
|
|
1028
|
+
this" is the reason people write them.
|
|
1029
|
+
|
|
1012
1030
|
## When Not To Use A Mutation
|
|
1013
1031
|
|
|
1014
1032
|
- **External I/O.** Use an action or workflow.
|
|
@@ -3522,7 +3540,7 @@ plugin's `tables` option, then read a timeline or restore a snapshot:
|
|
|
3522
3540
|
import { versioningPlugin, rowHistory, restoreAsOf } from '@voltro/plugin-versioning'
|
|
3523
3541
|
|
|
3524
3542
|
// Register in your app's plugin list:
|
|
3525
|
-
versioningPlugin({
|
|
3543
|
+
versioningPlugin({})
|
|
3526
3544
|
|
|
3527
3545
|
const timeline = await rowHistory(ctx.store, 'blogPost_published', postId, tenantId)
|
|
3528
3546
|
await restoreAsOf(ctx.store, 'blogPost_published', postId, tenantId, someEarlierDate)
|
|
@@ -4,6 +4,69 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<!-- source: en/database/migrations/adopt.md -->
|
|
10
|
+
## Adopting a table into a plugin's
|
|
11
|
+
|
|
12
|
+
_voltro db adopt — move an app's grown table into a plugin-owned one, with a snapshot, a count verify and the drop last._
|
|
13
|
+
|
|
14
|
+
An app that did not start on a green field already has a table for half the plugins it installs. `voltro db adopt` is the way **into** the plugin's table, so using the plugin does not mean running a second source of truth beside it.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
voltro db adopt --from ai_flows --into _voltro_ai_flows --map ./ai-flows.map.ts
|
|
18
|
+
voltro db adopt --from ai_flows --into _voltro_ai_flows --map ./ai-flows.map.ts --apply
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Dry run unless you pass `--apply`.** The interesting failure here is irreversible and the interesting output is the refusal, so nothing is written until you say so. A refused plan prints no steps at all, rather than a preview of something that will not happen.
|
|
22
|
+
|
|
23
|
+
## The map file is yours
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// ai-flows.map.ts
|
|
27
|
+
export default {
|
|
28
|
+
map: {
|
|
29
|
+
name: 'name',
|
|
30
|
+
mode: { expr: `CASE WHEN "allowDeviation" THEN 'agentic' ELSE 'deterministic' END` },
|
|
31
|
+
costMicroUsd: { expr: '"totalCostCents" * 10000' },
|
|
32
|
+
},
|
|
33
|
+
leaveUnset: ['id'],
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Read `target: source` — fill the plugin's column FROM this expression of mine, which is the direction the SQL runs. A string is a source column; `{ expr }` is raw SQL against the source row, for the unit conversions and merged fields no tool can infer. Those are domain knowledge, and a tool inventing them silently corrupts data.
|
|
38
|
+
|
|
39
|
+
`leaveUnset` is how "the target's own default fills this" stops looking like "I forgot it".
|
|
40
|
+
|
|
41
|
+
## What it refuses
|
|
42
|
+
|
|
43
|
+
- **a NOT NULL target column nobody maps to** — the alternative is a silent zero that reads as real data forever after;
|
|
44
|
+
- **a target table that already holds rows** — adopt MOVES rows into a table, it does not merge into one somebody else already wrote;
|
|
45
|
+
- **a typo on either side of the map.**
|
|
46
|
+
|
|
47
|
+
A source column nobody carries across is reported but not fatal: dropping a dead column is deliberate often enough, and "I forgot this" and "I decided" look identical in a map file.
|
|
48
|
+
|
|
49
|
+
## The order, and why the drop is last
|
|
50
|
+
|
|
51
|
+
1. **snapshot** — `<table>__adopt_snapshot`, a real table in the same database, so restoring is a statement rather than an operational procedure at 2am. It keeps the columns the adopt left behind.
|
|
52
|
+
2. **copy**
|
|
53
|
+
3. **verify by count** — this catches the one failure that is otherwise invisible: a `WHERE` inside a raw expression silently dropping rows.
|
|
54
|
+
4. **drop the source** — last, and only if the counts match.
|
|
55
|
+
|
|
56
|
+
On a mismatch **both tables stay** and the command says so. The snapshot is never removed after a failed verify — it exists for exactly the run that goes wrong. `--keep-source` copies and verifies without dropping at all.
|
|
57
|
+
|
|
58
|
+
## Ids, if the typeid prefixes differ
|
|
59
|
+
|
|
60
|
+
The dry run says so before anything runs, because discovering it after the copy is discovering it too late: every row gets a new id, so every reference to the old table has to be rewritten — **including ids embedded in JSON columns**.
|
|
61
|
+
|
|
62
|
+
Rewriting them is **not** automatic. Those ids live in your columns and inside your JSON, and only you know where. The translation table is what the command owes you; the rewrite is what you owe yourself. Doing it automatically is the one place here where being wrong would be silent.
|
|
63
|
+
|
|
64
|
+
## After the move
|
|
65
|
+
|
|
66
|
+
Your rows are now in a table whose shape the framework evolves — and nothing special happens to them. They migrate exactly like every other row, through the same declarative differ. A narrowing can fail on your data, loudly, the same way it would on anybody's.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
7
70
|
---
|
|
8
71
|
|
|
9
72
|
<!-- source: en/database/migrations/index.md -->
|
|
@@ -1556,6 +1619,37 @@ no-ops out.
|
|
|
1556
1619
|
|
|
1557
1620
|
The cloud dashboard surfaces per-env state with a multi-env tab in the [cloud UI](./cloud-ui.md).
|
|
1558
1621
|
|
|
1622
|
+
## File-based migrations in this pipeline
|
|
1623
|
+
|
|
1624
|
+
`voltro db apply` runs pending `migrations/*.ts` **first**, then diffs — the same order the boot path uses.
|
|
1625
|
+
|
|
1626
|
+
`voltro db apply --plan plan.json` does **not** run them. It **refuses** when any are pending:
|
|
1627
|
+
|
|
1628
|
+
```txt
|
|
1629
|
+
db apply --plan: refusing — 2 pending file-based migration(s).
|
|
1630
|
+
20260714_090000_split_full_name
|
|
1631
|
+
20260721_143000_backfill_slug
|
|
1632
|
+
|
|
1633
|
+
These perform the changes a state diff cannot infer, so they change the shape
|
|
1634
|
+
this plan was computed against. Apply them and regenerate the plan:
|
|
1635
|
+
|
|
1636
|
+
voltro db files .
|
|
1637
|
+
voltro db plan --json > plan.json
|
|
1638
|
+
voltro db apply --plan plan.json
|
|
1639
|
+
```
|
|
1640
|
+
|
|
1641
|
+
That is not caution for its own sake. A saved plan was computed and reviewed against an earlier state; a file migration performs exactly the kind of change (a table split, a cross-table data move) that makes the plan stale. Running the migrations first would trip the fingerprint guard immediately afterwards and leave a half-applied deploy; running them after would apply a plan reviewed against a state that no longer exists.
|
|
1642
|
+
|
|
1643
|
+
**So a pipeline that uses the saved-plan form needs `voltro db files` as its own step**, before the plan is generated:
|
|
1644
|
+
|
|
1645
|
+
```bash
|
|
1646
|
+
voltro db files . # authored data steps
|
|
1647
|
+
voltro db plan --json > plan.json # diff, now against the corrected shape
|
|
1648
|
+
voltro db apply --plan plan.json # reviewed, fingerprint-guarded
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
If you use plain `voltro db apply` instead, the first step is already included.
|
|
1652
|
+
|
|
1559
1653
|
|
|
1560
1654
|
|
|
1561
1655
|
---
|
|
@@ -132,6 +132,48 @@ Order matters: the framework composes outer→inner, so the rate-limit intercept
|
|
|
132
132
|
| `onInstall` / `onActivate` / `onDeactivate` / `onUninstall` | Lifecycle hooks at first-install, boot, shutdown, and removal. |
|
|
133
133
|
| Schema mixins (`defineMixin`) | The OTHER plugin shape — `audit()`, `tenant()`, `softDelete()` — declared in `@voltro/database`, not via the runtime contract. |
|
|
134
134
|
|
|
135
|
+
## Pointing YOUR table at a plugin's row
|
|
136
|
+
|
|
137
|
+
Every table-carrying plugin exports its table handles, so a column in your schema
|
|
138
|
+
can reference one exactly like it references your own:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { aiFlowsTable } from '@voltro/plugin-ai-flows'
|
|
142
|
+
import { id, reference, table, text } from '@voltro/database'
|
|
143
|
+
|
|
144
|
+
export const flowFavourites = table('flow_favourites', {
|
|
145
|
+
id: id({ prefix: 'fav' }),
|
|
146
|
+
employeeId: reference(() => employees, { onDelete: 'cascade' }),
|
|
147
|
+
// A real foreign key across the plugin boundary. Deleting the flow removes
|
|
148
|
+
// the favourite; the DATABASE enforces it, so no cleanup subscriber exists to
|
|
149
|
+
// forget.
|
|
150
|
+
flowId: reference(() => aiFlowsTable, { onDelete: 'cascade' }),
|
|
151
|
+
note: text().nullable(),
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This is not a special primitive — it is `reference()`, with the same
|
|
156
|
+
`onDelete` semantics and the same index defaults. `plugin-storage`'s `assetRef()`
|
|
157
|
+
has always been exactly this under the hood: a `reference(() => _voltroStorageRefsTable,
|
|
158
|
+
{ onDelete: 'setNull' })`.
|
|
159
|
+
|
|
160
|
+
**Referencing the table as a VALUE rather than its name as a string is what makes
|
|
161
|
+
this safe across a plugin's own migrations.** When ten plugin tables moved into
|
|
162
|
+
the `_voltro_` namespace in 0.22.0, a `reference(() => table)` followed the rename
|
|
163
|
+
(catalog-only, the constraint travels with the table); a hand-written
|
|
164
|
+
`text()` column holding ids would not have told you anything had changed.
|
|
165
|
+
|
|
166
|
+
**`fk: false`-style decoupling is still available** — declare a plain `text()`
|
|
167
|
+
column instead. Choose it when you deliberately want the app schema independent
|
|
168
|
+
of the plugin's, and accept that nothing then enforces the link. What you should
|
|
169
|
+
NOT do is reach for it by default: an unenforced id column plus a hand-written
|
|
170
|
+
`defineSubscriber` that cleans up on delete is referential integrity re-implemented
|
|
171
|
+
per app, and it is silently wrong the first time somebody forgets it.
|
|
172
|
+
|
|
173
|
+
**`orphanPolicy` is not part of this.** It is migration metadata — how existing
|
|
174
|
+
orphan rows are cleaned up *before* the FK constraint is added — and has no
|
|
175
|
+
runtime semantics. Runtime behaviour comes from `onDelete`.
|
|
176
|
+
|
|
135
177
|
## When NOT to write a plugin
|
|
136
178
|
|
|
137
179
|
- **One-off side effect** — just call it from the mutation directly.
|
|
@@ -961,6 +961,7 @@ React alone.
|
|
|
961
961
|
export const loader = async (ctx: {
|
|
962
962
|
readonly params: Readonly<Record<string, string>> // URL params from [name] segments
|
|
963
963
|
readonly pathname: string // matched path (no query string)
|
|
964
|
+
readonly search: string // raw query string incl. `?`, or '' — filled on every path
|
|
964
965
|
readonly signal: AbortSignal // Aborts if the client disconnects mid-render
|
|
965
966
|
readonly headers?: Readonly<Record<string, string>> // Request headers (SSR/ISR only — empty for SSG/client)
|
|
966
967
|
// Call the backend rpc directly — present ONLY when the loader runs
|
|
@@ -970,7 +971,28 @@ export const loader = async (ctx: {
|
|
|
970
971
|
}) => Promise<unknown>
|
|
971
972
|
```
|
|
972
973
|
|
|
973
|
-
The loader context carries `pathname`, not a `request` object.
|
|
974
|
+
The loader context carries `pathname` and `search`, not a `request` object.
|
|
975
|
+
|
|
976
|
+
`pathname` is deliberately query-free — a loader keyed on `?tab=2` would cache badly. `search` carries the raw query string (with its leading `?`, or `''`), filled identically on client navigation, `voltro dev` SSR and `voltro start` SSR. Parse it with `new URLSearchParams(ctx.search)`.
|
|
977
|
+
|
|
978
|
+
**Reach for `search` when the loader makes a decision, not when it fetches data.** The case it exists for is a redirect target that depends on a parameter:
|
|
979
|
+
|
|
980
|
+
```ts
|
|
981
|
+
import { RedirectError } from '@voltro/web'
|
|
982
|
+
|
|
983
|
+
export const loader = async (ctx) => {
|
|
984
|
+
const player = await ctx.query('players.byCode', { code: ctx.params['playerCode'] })
|
|
985
|
+
if (!player) {
|
|
986
|
+
// Preserve kiosk mode across the redirect — otherwise a kiosk terminal
|
|
987
|
+
// drops back to normal mode after every failed scan.
|
|
988
|
+
const mode = new URLSearchParams(ctx.search).get('mode')
|
|
989
|
+
throw new RedirectError(`/?error=${ctx.params['playerCode']}${mode ? `&mode=${mode}` : ''}`)
|
|
990
|
+
}
|
|
991
|
+
return { player }
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
Do not reconstruct this from `window.location.search`: that exists only on the client-navigation path, so a fresh SSR request loses the value — which is the bug the field was added to remove.
|
|
974
996
|
|
|
975
997
|
Use `signal` for any fetch that could outlive the request — pass it to `fetch(url, { signal })` so cancelled requests don't waste CPU.
|
|
976
998
|
|
|
@@ -2850,7 +2850,7 @@ voltro create-project acme --api=api-versioning
|
|
|
2850
2850
|
// app.config.ts
|
|
2851
2851
|
import { versioningPlugin } from '@voltro/plugin-versioning'
|
|
2852
2852
|
export default { type: 'api', name: 'AcmeVer', store: 'memory',
|
|
2853
|
-
plugins: [versioningPlugin({
|
|
2853
|
+
plugins: [versioningPlugin({})] }
|
|
2854
2854
|
```
|
|
2855
2855
|
|
|
2856
2856
|
The snapshotting is automatic. The point is to **read history through handlers** — the template ships two actions:
|
|
@@ -2898,7 +2898,7 @@ The memory history store is single-process. With a SQL store, history persists i
|
|
|
2898
2898
|
|
|
2899
2899
|
## Anti-patterns
|
|
2900
2900
|
|
|
2901
|
-
- **Listing `versioningPlugin(
|
|
2901
|
+
- **Listing `versioningPlugin()` and stopping there.** That records history but never shows it. The value is in the READS — wire `rowHistory` / `rowAsOf` into a handler (or the dashboard) like this template does.
|
|
2902
2902
|
- **Versioning every table.** Snapshots cost storage proportional to write volume. Pick the tables whose past values you actually need.
|
|
2903
2903
|
|
|
2904
2904
|
|