@voltro/cli 0.22.1 → 0.24.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 +427 -0
- package/bin/voltro.mjs +39 -4
- package/dist/{apiBuild-DiWxVz-M.js → apiBuild-B2m4XK_8.js} +45 -23
- package/dist/apiBuild-UEM3QBke.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-BxRaIOBG.js → commands-CaIhTsC2.js} +2944 -2557
- package/dist/{dbCommand-DVASmZj2.js → dbCommand-CC61CsAc.js} +474 -263
- package/dist/dbCommand-Cr__4ATv.js +2 -0
- package/dist/{dev-Dd3EZzj5.js → dev-DPkQVUTP.js} +1 -1
- package/dist/{dev-BRiPgbKw.js → dev-NCuEhNxs.js} +1593 -1575
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-9ZSuDeqD.js → inspectMetrics-dcC7gnxu.js} +566 -552
- package/dist/{serveCommand-DXJOARkO.js → serveCommand-C4gzDZzc.js} +425 -411
- package/dist/serveEntry.js +2 -2
- package/dist/{start-CsIjcOi-.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/authentication.md +21 -0
- package/templates/agent-docs/caching.md +21 -5
- package/templates/agent-docs/cli.md +61 -0
- package/templates/agent-docs/data.md +208 -170
- package/templates/agent-docs/database/migrations.md +123 -0
- package/templates/agent-docs/database/seedsdialects.md +12 -0
- package/templates/agent-docs/deployment.md +66 -1
- 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 +281 -32
- package/templates/agent-docs/workflows.md +30 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/package.json +8 -7
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-versioning/app.config.ts +6 -4
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/changelog/package.json +7 -6
- package/templates/apps/edge-functions/package.json +3 -2
- package/templates/apps/frontend-admin/package.json +9 -8
- package/templates/apps/frontend-app/package.json +9 -8
- package/templates/apps/frontend-blank/package.json +8 -7
- package/templates/apps/frontend-contact/package.json +8 -7
- package/templates/apps/frontend-contact/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/package.json +8 -7
- package/templates/apps/frontend-docs/package.json +8 -7
- package/templates/apps/frontend-docs/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/package.json +7 -6
- package/templates/apps/frontend-landing/package.json +8 -7
- package/templates/apps/frontend-landing/src/globals.d.ts +6 -0
- package/templates/apps/frontend-spa/package.json +8 -7
- package/templates/apps/frontend-spa/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr/package.json +8 -7
- package/templates/apps/frontend-ssr/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/package.json +9 -8
- package/templates/apps/frontend-static-blog/package.json +7 -6
- package/templates/apps/frontend-static-blog/src/globals.d.ts +6 -0
- package/dist/apiBuild-COyPDf3R.js +0 -2
- package/dist/dbCommand-CO3eSAZR.js +0 -2
|
@@ -918,6 +918,44 @@ under `NODE_ENV=production` (it refuses unless both fingerprints still match the
|
|
|
918
918
|
reviewed plan). A Job runs **once per release** vs an `initContainer`'s once per
|
|
919
919
|
replica, so it's the better fit for a multi-replica rollout.
|
|
920
920
|
|
|
921
|
+
### Expand/contract — the migration that's safe while old pods still serve
|
|
922
|
+
|
|
923
|
+
The pre-deploy Job applies the schema **before the new pods roll** — so during a
|
|
924
|
+
rolling update, old pods (old code) run against the already-migrated schema for
|
|
925
|
+
the length of the rollout. A migration that DROPS or RENAMES a column, NARROWS a
|
|
926
|
+
type, or ADDS a constraint breaks those old pods mid-rollout: they 500 reading a
|
|
927
|
+
column that's gone, or their writes are rejected by the new constraint. The
|
|
928
|
+
migration "succeeded" and the app served errors anyway.
|
|
929
|
+
|
|
930
|
+
`voltro db plan` flags these — the operations unsafe under a rolling deploy are
|
|
931
|
+
listed with a `⚠`, separately from the data-safety (lossy / blocked) gate, since
|
|
932
|
+
the two are orthogonal: a `dropped()` column is blessed for data loss and *still*
|
|
933
|
+
breaks an old reader.
|
|
934
|
+
|
|
935
|
+
```text
|
|
936
|
+
⚠ 1 operation(s) UNSAFE under a rolling deploy
|
|
937
|
+
(old + new instances overlap → old code breaks against the new schema):
|
|
938
|
+
• drop-column: old instances still SELECT/INSERT "orders"."legacy_total"; …
|
|
939
|
+
→ stop reading the column in code and deploy that first; drop it in a LATER deploy
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Two ways to handle it:
|
|
943
|
+
|
|
944
|
+
1. **No overlap window** — a maintenance-window or **scale-to-zero** deploy (old
|
|
945
|
+
pods gone before new ones start) has no simultaneous old code, so a single-step
|
|
946
|
+
drop/rename is fine. The advisory doesn't apply; ignore it.
|
|
947
|
+
2. **Zero-downtime rollout** — split the breaking change into two releases, each
|
|
948
|
+
of which keeps *both* code versions working (**expand/contract**):
|
|
949
|
+
- **Expand** (release N): add the new shape — a nullable column, a new table,
|
|
950
|
+
a backfill, dual-write from the new code. Old code ignores it.
|
|
951
|
+
- **Cut over**: the new code reads/writes the new shape; deploy it.
|
|
952
|
+
- **Contract** (release N+1): once no pod runs the old code, drop/rename/narrow
|
|
953
|
+
the now-unused old shape. This step's `db plan` is clean.
|
|
954
|
+
|
|
955
|
+
Renaming `orders.total` → `orders.amount` under zero downtime is: add `amount`
|
|
956
|
+
(expand) → backfill + dual-write → cut reads over → drop `total` (contract) — three
|
|
957
|
+
releases, never one, so no in-flight pod ever references a column that isn't there.
|
|
958
|
+
|
|
921
959
|
### If you DO run `voltro dev` in a cluster (dev / staging only)
|
|
922
960
|
|
|
923
961
|
`voltro dev` binds a small **boot-health surface** on its own port so a probe can
|
|
@@ -1059,6 +1097,33 @@ that route to a terminating pod before k8s finishes removing it from the Service
|
|
|
1059
1097
|
endpoints. With it, that window is drained. `terminationGracePeriodSeconds` must
|
|
1060
1098
|
be larger than the sleep plus the app's own teardown, or k8s SIGKILLs mid-drain.
|
|
1061
1099
|
|
|
1100
|
+
**Bound the app's own teardown with `VOLTRO_SHUTDOWN_GRACE_MS`.** After
|
|
1101
|
+
`SIGTERM`, the runtime runs its finalizers (pool close, plugin `onDeactivate`,
|
|
1102
|
+
analytics flush, trace persist) and then exits — but installing the signal
|
|
1103
|
+
handler removes node's default kill, so a finalizer that *never* completes (a
|
|
1104
|
+
pool drain against a database that is already gone, a wedged `onDeactivate`)
|
|
1105
|
+
would otherwise hang the process forever. A hard deadline caps that: teardown
|
|
1106
|
+
gets until the deadline, then the process exits regardless. It defaults to
|
|
1107
|
+
**10s**; set `VOLTRO_SHUTDOWN_GRACE_MS` (clamped to 1s–5min) to sit JUST UNDER
|
|
1108
|
+
your `terminationGracePeriodSeconds` minus the preStop sleep — so the app drains
|
|
1109
|
+
and exits *cleanly on its own* before k8s SIGKILLs it mid-drain:
|
|
1110
|
+
|
|
1111
|
+
```yaml
|
|
1112
|
+
spec:
|
|
1113
|
+
terminationGracePeriodSeconds: 30
|
|
1114
|
+
containers:
|
|
1115
|
+
- name: api
|
|
1116
|
+
env:
|
|
1117
|
+
# preStop sleep (5s) + app teardown (≤22s) < 30s grace, with headroom.
|
|
1118
|
+
- name: VOLTRO_SHUTDOWN_GRACE_MS
|
|
1119
|
+
value: "22000"
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
The close is clean on the client side too: on shutdown each live WebSocket is
|
|
1123
|
+
closed with a proper close frame (not an abrupt socket drop), and the web
|
|
1124
|
+
client's supervisor reconnects on any close — so an open dashboard re-attaches
|
|
1125
|
+
to a healthy replica across a rolling deploy without a page reload.
|
|
1126
|
+
|
|
1062
1127
|
## 8. Multiple replicas
|
|
1063
1128
|
|
|
1064
1129
|
Cache and KV default to **in-process** (per-replica). For a shared backend across replicas:
|
|
@@ -1087,7 +1152,7 @@ Schedules and aggregates auto-coordinate via an advisory lock on SQL stores —
|
|
|
1087
1152
|
- [ ] `auth.anonymousTenantRequired: true` (unless the app serves anonymous public data)
|
|
1088
1153
|
- [ ] `OTEL_EXPORTER_OTLP_ENDPOINT` + `OTEL_SERVICE_NAME` pointed at your collector
|
|
1089
1154
|
- [ ] `VOLTRO_LOG_FORMAT=json`; `sentryPlugin()` + `SENTRY_DSN` for errors
|
|
1090
|
-
- [ ] `terminationGracePeriodSeconds` generous for graceful drain
|
|
1155
|
+
- [ ] `terminationGracePeriodSeconds` generous for graceful drain; `VOLTRO_SHUTDOWN_GRACE_MS` set just under it (minus the preStop sleep)
|
|
1091
1156
|
- [ ] `CACHE_BACKEND` / `KV_BACKEND` + a cross-replica change bus when running >1 replica
|
|
1092
1157
|
|
|
1093
1158
|
|
|
@@ -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
|
|