@voltro/cli 0.18.0 → 0.20.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 +245 -3
- package/bin/voltro.mjs +11 -0
- package/dist/apiBuild-B1FDtx1y.js +2 -0
- package/dist/{apiBuild-CPDTJHkH.js → apiBuild-CSCVI1wC.js} +2 -2
- package/dist/bin.js +2 -2
- package/dist/{commands-laJDMj2m.js → commands-DQ0mUwDe.js} +2074 -1771
- package/dist/{dev-BOFxC21E.js → dev-DnHlqqoF.js} +1607 -1566
- package/dist/{dev-DOK0w6ZW.js → dev-Z1E_twG9.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-D1DmLeJs.js → inspectMetrics-BL8kZOv3.js} +196 -176
- package/dist/serveCommand-Dh4qd49N.js +1136 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-Clvz4IJb.js → start-IVgQe2YD.js} +368 -368
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +25 -9
- package/templates/AGENTS.md +26 -10
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/authentication.md +9 -0
- package/templates/agent-docs/database/migrations.md +35 -2
- package/templates/agent-docs/database/misc.md +42 -3
- package/templates/agent-docs/database/querying.md +2 -0
- package/templates/agent-docs/routing.md +13 -0
- package/templates/agent-docs/whats-new.md +44 -189
- 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/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-D1UBJ4TM.js +0 -2
- package/dist/serveCommand-CxcxHc9Y.js +0 -1129
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { et as e } from "./inspectMetrics-BL8kZOv3.js";
|
|
2
|
+
import { t } from "./start-IVgQe2YD.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.20.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.107.0",
|
|
63
63
|
"@effect/sql": "^0.51.1",
|
|
64
64
|
"@effect/workflow": "^0.18.2",
|
|
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.20.0",
|
|
66
|
+
"@voltro/cache": "0.20.0",
|
|
67
|
+
"@voltro/data-transfer": "0.20.0",
|
|
68
|
+
"@voltro/database": "0.20.0",
|
|
69
|
+
"@voltro/env": "0.20.0",
|
|
70
|
+
"@voltro/kv": "0.20.0",
|
|
71
|
+
"@voltro/logger": "0.20.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.20.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.20.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.20.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.20.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.20.0",
|
|
77
|
+
"@voltro/protocol": "0.20.0",
|
|
78
|
+
"@voltro/runtime": "0.20.0",
|
|
79
|
+
"@voltro/serverless": "0.20.0",
|
|
80
|
+
"@voltro/workflow": "0.20.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -103,30 +103,41 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
103
103
|
in-handler for checks that need LOADED data. A hand-written `requireScope(...)`
|
|
104
104
|
at the top of every executor — or a hand-kept map from rpc tag to policy rule,
|
|
105
105
|
which is fail-open by omission — is what `guards:` exists to delete.
|
|
106
|
-
6. **
|
|
106
|
+
6. **Which ROWS may the caller see** (not: may they make the call)? →
|
|
107
|
+
**`setRowFilter`** — a predicate derived from the Subject, AND-merged into
|
|
108
|
+
EVERY read of that table, including every subscription delivery. `guards:`
|
|
109
|
+
and this answer different questions and the pair is easy to conflate:
|
|
110
|
+
- `guards:` → *may I call this procedure?* → a typed `ScopeError`.
|
|
111
|
+
- `setRowFilter` → *which rows may I see?* → the rows are simply absent.
|
|
112
|
+
A `WHERE ownerId = me` in the list handler covers the query and **NOT** the
|
|
113
|
+
stream, which is the failure this deletes. Register it from a
|
|
114
|
+
`*.startup.tsx`; its `load` must read through an UNFILTERED store (the boot
|
|
115
|
+
store / the `database` handle) — reading through the filtered one applies the
|
|
116
|
+
filter to itself and blows the stack. Depth → **`authentication`**.
|
|
117
|
+
7. **Storing a token, secret, or credential in a column?** → **`.encrypted()`**
|
|
107
118
|
on the column. Boot fails loudly if no cipher is configured, so an
|
|
108
119
|
`.encrypted()` column can never silently persist plaintext.
|
|
109
|
-
|
|
120
|
+
8. **Writing an Effect-form handler?** → `const store = yield* EffectStore` —
|
|
110
121
|
its failures land on the typed error channel. `Effect.promise(() =>
|
|
111
122
|
ctx.store.query(…))` throws that channel away and turns a store failure into
|
|
112
123
|
a defect.
|
|
113
|
-
|
|
124
|
+
9. **Cursor pagination?** → **`paginateBy(descriptor, column, cursor, limit,
|
|
114
125
|
direction?)`** (or **`paginateById`**, its `id`-column shorthand). Not a
|
|
115
126
|
hand-rolled limit+1 / slice / `hasMore` triple. `direction` flips the
|
|
116
127
|
COMPARISON as well as the sort — a `desc` feed pages with `<`.
|
|
117
|
-
|
|
128
|
+
10. **Assembling data whose SHAPE depends on the data** (a tree walk where each
|
|
118
129
|
level's ids come from the level above)? → **`ctx.load` / `ctx.loadMany`** —
|
|
119
130
|
same-tick reads of one table coalesce into one `WHERE id IN (...)`, so the
|
|
120
131
|
walk costs one query per LEVEL. Use `relations()` + `.with()` whenever the
|
|
121
132
|
shape IS static; this is the fallback, not the default.
|
|
122
|
-
|
|
133
|
+
11. **A mutation must cause an EXTERNAL side effect** (webhook, Jira sync,
|
|
123
134
|
payment)? → **`ctx.outbox.enqueue(effect, payload)`** + a
|
|
124
135
|
**`defineOutboxHandler`** in a `*.outbox.ts`. The enqueue writes through the
|
|
125
136
|
mutation's TRANSACTION, so the intent commits with the write or not at all;
|
|
126
137
|
delivery happens after commit, with backoff and a dead-letter. Do NOT call
|
|
127
138
|
the remote from the mutation (not transactional), and do not hand-build a
|
|
128
139
|
deliveries table + drain cron — that IS this primitive.
|
|
129
|
-
|
|
140
|
+
12. **Calling a third party ON BEHALF OF A USER** (their Jira token, their Slack
|
|
130
141
|
grant)? → **`defineConnection({ id, kind: 'oauth2' | 'pat' })`** in a
|
|
131
142
|
`*.connection.ts`, then **`ctx.connections.get(id)`**. One declaration yields
|
|
132
143
|
the encrypted per-user token store, the OAuth authorize/callback pair,
|
|
@@ -140,6 +151,7 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
140
151
|
| `const rows = …; if (!rows[0]) throw new NotFound()` | `.one()` |
|
|
141
152
|
| 3+ sequential `store.query` to assemble related data | `relations()` + `.with()` |
|
|
142
153
|
| a notify/webhook helper called at the end of a mutation | `defineSubscriber` / `defineReaction` |
|
|
154
|
+
| `WHERE ownerId = me` in every list handler | `setRowFilter` (covers the SUBSCRIPTION too) |
|
|
143
155
|
| a counter recomputed by scanning rows on every read | `defineAggregate` (+ `incremental:`) |
|
|
144
156
|
| `requireScope(...)` as the first line of every executor | `guards:` on the descriptor |
|
|
145
157
|
| a token/secret column written as plain text | `.encrypted()` |
|
|
@@ -456,7 +468,7 @@ separate markers, and using one to answer another's question is the mistake:
|
|
|
456
468
|
|
|
457
469
|
| Marker | Answers | Enforced by |
|
|
458
470
|
|---|---|---|
|
|
459
|
-
| `.serverOnly()` | may this value leave the server AT ALL? |
|
|
471
|
+
| `.serverOnly()` | may this value leave the server AT ALL? | **`voltro serve` fails to boot; `voltro doctor` exits non-zero.** `voltro dev` only warns |
|
|
460
472
|
| `.sensitive('class')` / `.safe()` | may it appear in a `voltro data export`? | the export's masking profile, **fail-closed** |
|
|
461
473
|
| `.encrypted()` | is it encrypted AT REST? | the store's codec |
|
|
462
474
|
|
|
@@ -470,8 +482,12 @@ export const users = table('users', {
|
|
|
470
482
|
```
|
|
471
483
|
|
|
472
484
|
- **`.serverOnly()` is the one that decides "leak / no leak".** A wire-reachable
|
|
473
|
-
query that declares such a column in its OUTPUT does not
|
|
474
|
-
|
|
485
|
+
query that declares such a column in its OUTPUT does not reach production:
|
|
486
|
+
`voltro serve` refuses to boot and `voltro doctor` exits non-zero, so the
|
|
487
|
+
failure happens at deploy time rather than in a bundle. `voltro dev` only
|
|
488
|
+
WARNS — mid-edit a refused boot is worse than the bug — so do not read a green
|
|
489
|
+
dev boot as a clean audit. `VOLTRO_SERVER_ONLY=strict` makes dev fail too;
|
|
490
|
+
put `voltro doctor` in CI if you want one gate that covers both.
|
|
475
491
|
- **`.encrypted()` is NOT an exposure marker.** It says the bytes at rest are
|
|
476
492
|
encrypted; the runtime decrypts them for a handler, so an encrypted column
|
|
477
493
|
flows to the client exactly like any other unless it is ALSO `.serverOnly()`.
|
package/templates/AGENTS.md
CHANGED
|
@@ -103,30 +103,41 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
103
103
|
in-handler for checks that need LOADED data. A hand-written `requireScope(...)`
|
|
104
104
|
at the top of every executor — or a hand-kept map from rpc tag to policy rule,
|
|
105
105
|
which is fail-open by omission — is what `guards:` exists to delete.
|
|
106
|
-
6. **
|
|
106
|
+
6. **Which ROWS may the caller see** (not: may they make the call)? →
|
|
107
|
+
**`setRowFilter`** — a predicate derived from the Subject, AND-merged into
|
|
108
|
+
EVERY read of that table, including every subscription delivery. `guards:`
|
|
109
|
+
and this answer different questions and the pair is easy to conflate:
|
|
110
|
+
- `guards:` → *may I call this procedure?* → a typed `ScopeError`.
|
|
111
|
+
- `setRowFilter` → *which rows may I see?* → the rows are simply absent.
|
|
112
|
+
A `WHERE ownerId = me` in the list handler covers the query and **NOT** the
|
|
113
|
+
stream, which is the failure this deletes. Register it from a
|
|
114
|
+
`*.startup.tsx`; its `load` must read through an UNFILTERED store (the boot
|
|
115
|
+
store / the `database` handle) — reading through the filtered one applies the
|
|
116
|
+
filter to itself and blows the stack. Depth → **`authentication`**.
|
|
117
|
+
7. **Storing a token, secret, or credential in a column?** → **`.encrypted()`**
|
|
107
118
|
on the column. Boot fails loudly if no cipher is configured, so an
|
|
108
119
|
`.encrypted()` column can never silently persist plaintext.
|
|
109
|
-
|
|
120
|
+
8. **Writing an Effect-form handler?** → `const store = yield* EffectStore` —
|
|
110
121
|
its failures land on the typed error channel. `Effect.promise(() =>
|
|
111
122
|
ctx.store.query(…))` throws that channel away and turns a store failure into
|
|
112
123
|
a defect.
|
|
113
|
-
|
|
124
|
+
9. **Cursor pagination?** → **`paginateBy(descriptor, column, cursor, limit,
|
|
114
125
|
direction?)`** (or **`paginateById`**, its `id`-column shorthand). Not a
|
|
115
126
|
hand-rolled limit+1 / slice / `hasMore` triple. `direction` flips the
|
|
116
127
|
COMPARISON as well as the sort — a `desc` feed pages with `<`.
|
|
117
|
-
|
|
128
|
+
10. **Assembling data whose SHAPE depends on the data** (a tree walk where each
|
|
118
129
|
level's ids come from the level above)? → **`ctx.load` / `ctx.loadMany`** —
|
|
119
130
|
same-tick reads of one table coalesce into one `WHERE id IN (...)`, so the
|
|
120
131
|
walk costs one query per LEVEL. Use `relations()` + `.with()` whenever the
|
|
121
132
|
shape IS static; this is the fallback, not the default.
|
|
122
|
-
|
|
133
|
+
11. **A mutation must cause an EXTERNAL side effect** (webhook, Jira sync,
|
|
123
134
|
payment)? → **`ctx.outbox.enqueue(effect, payload)`** + a
|
|
124
135
|
**`defineOutboxHandler`** in a `*.outbox.ts`. The enqueue writes through the
|
|
125
136
|
mutation's TRANSACTION, so the intent commits with the write or not at all;
|
|
126
137
|
delivery happens after commit, with backoff and a dead-letter. Do NOT call
|
|
127
138
|
the remote from the mutation (not transactional), and do not hand-build a
|
|
128
139
|
deliveries table + drain cron — that IS this primitive.
|
|
129
|
-
|
|
140
|
+
12. **Calling a third party ON BEHALF OF A USER** (their Jira token, their Slack
|
|
130
141
|
grant)? → **`defineConnection({ id, kind: 'oauth2' | 'pat' })`** in a
|
|
131
142
|
`*.connection.ts`, then **`ctx.connections.get(id)`**. One declaration yields
|
|
132
143
|
the encrypted per-user token store, the OAuth authorize/callback pair,
|
|
@@ -140,6 +151,7 @@ line below replaces something real apps write by hand hundreds of times.
|
|
|
140
151
|
| `const rows = …; if (!rows[0]) throw new NotFound()` | `.one()` |
|
|
141
152
|
| 3+ sequential `store.query` to assemble related data | `relations()` + `.with()` |
|
|
142
153
|
| a notify/webhook helper called at the end of a mutation | `defineSubscriber` / `defineReaction` |
|
|
154
|
+
| `WHERE ownerId = me` in every list handler | `setRowFilter` (covers the SUBSCRIPTION too) |
|
|
143
155
|
| a counter recomputed by scanning rows on every read | `defineAggregate` (+ `incremental:`) |
|
|
144
156
|
| `requireScope(...)` as the first line of every executor | `guards:` on the descriptor |
|
|
145
157
|
| a token/secret column written as plain text | `.encrypted()` |
|
|
@@ -456,7 +468,7 @@ separate markers, and using one to answer another's question is the mistake:
|
|
|
456
468
|
|
|
457
469
|
| Marker | Answers | Enforced by |
|
|
458
470
|
|---|---|---|
|
|
459
|
-
| `.serverOnly()` | may this value leave the server AT ALL? |
|
|
471
|
+
| `.serverOnly()` | may this value leave the server AT ALL? | **`voltro serve` fails to boot; `voltro doctor` exits non-zero.** `voltro dev` only warns |
|
|
460
472
|
| `.sensitive('class')` / `.safe()` | may it appear in a `voltro data export`? | the export's masking profile, **fail-closed** |
|
|
461
473
|
| `.encrypted()` | is it encrypted AT REST? | the store's codec |
|
|
462
474
|
|
|
@@ -470,8 +482,12 @@ export const users = table('users', {
|
|
|
470
482
|
```
|
|
471
483
|
|
|
472
484
|
- **`.serverOnly()` is the one that decides "leak / no leak".** A wire-reachable
|
|
473
|
-
query that declares such a column in its OUTPUT does not
|
|
474
|
-
|
|
485
|
+
query that declares such a column in its OUTPUT does not reach production:
|
|
486
|
+
`voltro serve` refuses to boot and `voltro doctor` exits non-zero, so the
|
|
487
|
+
failure happens at deploy time rather than in a bundle. `voltro dev` only
|
|
488
|
+
WARNS — mid-edit a refused boot is worse than the bug — so do not read a green
|
|
489
|
+
dev boot as a clean audit. `VOLTRO_SERVER_ONLY=strict` makes dev fail too;
|
|
490
|
+
put `voltro doctor` in CI if you want one gate that covers both.
|
|
475
491
|
- **`.encrypted()` is NOT an exposure marker.** It says the bytes at rest are
|
|
476
492
|
encrypted; the runtime decrypts them for a handler, so an encrypted column
|
|
477
493
|
flows to the client exactly like any other unless it is ALSO `.serverOnly()`.
|
|
@@ -579,7 +595,7 @@ each plugin's own README.
|
|
|
579
595
|
|
|
580
596
|
| Topic | Open | Summary |
|
|
581
597
|
|---|---|---|
|
|
582
|
-
| **What's new in 0.
|
|
598
|
+
| **What's new in 0.20.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. |
|
|
583
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. |
|
|
584
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. |
|
|
585
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.20.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. |
|
|
@@ -590,6 +590,15 @@ column read raw hands back the string `enc:v1:…`, which compares, concatenates
|
|
|
590
590
|
renders and logs perfectly well, and simply never matches the token you compare
|
|
591
591
|
it to.
|
|
592
592
|
|
|
593
|
+
**The soft-delete row is the one to read twice if you are porting raw SQL onto
|
|
594
|
+
this store.** A read here behaves the way your SQL did and returns tombstones —
|
|
595
|
+
the store does *not* start appending `deletedAt IS NULL` behind you. So a lookup
|
|
596
|
+
that must see a soft-deleted row (a login that revives a returning user, say)
|
|
597
|
+
needs no opt-out and no `.withDeleted()`; that opt-out belongs to `ctx.store`,
|
|
598
|
+
which *does* apply the filter. Assuming the filter is present is the more
|
|
599
|
+
expensive mistake of the two: it turns a working login into a "not found →
|
|
600
|
+
insert → unique violation on email", and nothing about the code says so.
|
|
601
|
+
|
|
593
602
|
This is also what changes when you move a read **off** hand-written SQL and onto
|
|
594
603
|
the store. Raw SQL sees ciphertext and you decrypt it yourself — `decryptField`
|
|
595
604
|
from `@voltro/runtime` is the escape hatch for exactly that. Through either
|
|
@@ -1937,6 +1937,19 @@ source of truth. Just restoring the snapshot brings the data back
|
|
|
1937
1937
|
but the declared schema still says the column shouldn't exist; the
|
|
1938
1938
|
next apply would re-drop it.
|
|
1939
1939
|
|
|
1940
|
+
## The differ leaves snapshots alone
|
|
1941
|
+
|
|
1942
|
+
A soft drop leaves an object in the database that no schema declares —
|
|
1943
|
+
that is the whole point of it. The planner treats every
|
|
1944
|
+
`<name>__dropped_<ts>` as framework-managed and never plans a drop for
|
|
1945
|
+
it, the same way it skips `_voltro_*` and the cluster engine's tables.
|
|
1946
|
+
This holds for columns and for tables.
|
|
1947
|
+
|
|
1948
|
+
You do not need `VOLTRO_DB_IGNORE_TABLES` for a snapshot, and you should
|
|
1949
|
+
not add one: that list is for YOUR unmanaged infra tables, and an entry
|
|
1950
|
+
there would still be in your config long after `gc-snapshots` reclaimed
|
|
1951
|
+
the snapshot.
|
|
1952
|
+
|
|
1940
1953
|
## GC
|
|
1941
1954
|
|
|
1942
1955
|
Snapshot columns aren't automatically dropped. They survive until
|
|
@@ -2612,7 +2625,15 @@ export default migration({
|
|
|
2612
2625
|
// VOLTRO_DESTRUCTIVE_OK=1 voltro db apply --note 'retiring oldUsersTable per ticket #...'
|
|
2613
2626
|
```
|
|
2614
2627
|
|
|
2615
|
-
**
|
|
2628
|
+
**Delete the entity and drop the table in the SAME change.** The intuitive
|
|
2629
|
+
order — remove the code first, sort the schema out after — is the broken one:
|
|
2630
|
+
with `VOLTRO_AUTO_MIGRATE=1` the very next boot sees an undeclared table,
|
|
2631
|
+
refuses, and the app crashloops until the drop is authorised. There is nothing
|
|
2632
|
+
to recover from, but the app is down while you work it out. Take the entity out
|
|
2633
|
+
together with the `VOLTRO_DESTRUCTIVE_OK` apply that removes its table, or leave
|
|
2634
|
+
the entity in place until you are ready to run both.
|
|
2635
|
+
|
|
2636
|
+
**Three tables this never proposes dropping:**
|
|
2616
2637
|
|
|
2617
2638
|
- **`actors`** — the framework-provided audit subject. You don't declare an
|
|
2618
2639
|
`actors.entity.ts`; `db plan` / `db apply` auto-include the built-in `actors`
|
|
@@ -2633,6 +2654,10 @@ export default migration({
|
|
|
2633
2654
|
Strapi→Voltro `_strapi_id_map` sitting in the DB won't refuse-to-boot on a
|
|
2634
2655
|
`drop-table`. (The framework already self-excludes its own `_voltro_*` /
|
|
2635
2656
|
`cluster_*` runtime tables; this is the user list on top of that.)
|
|
2657
|
+
- **Soft-drop snapshots** — a `<name>__dropped_<ts>` left behind by
|
|
2658
|
+
`VOLTRO_SOFT_DROP=1`. The differ treats it as framework-managed until
|
|
2659
|
+
`voltro db gc-snapshots` reclaims it, so do NOT add one to
|
|
2660
|
+
`VOLTRO_DB_IGNORE_TABLES`.
|
|
2636
2661
|
|
|
2637
2662
|
## A migrate / apply DDL statement failed — find which one
|
|
2638
2663
|
|
|
@@ -2699,7 +2724,15 @@ it now is, must have nothing left to do.
|
|
|
2699
2724
|
|
|
2700
2725
|
**It is a framework bug, not a mistake in your schema.** The named operations
|
|
2701
2726
|
emit DDL that does not take effect. Report the operation kinds plus the column
|
|
2702
|
-
types involved.
|
|
2727
|
+
types involved.
|
|
2728
|
+
|
|
2729
|
+
**Read the named operations before you trust the "no-op" wording**, though —
|
|
2730
|
+
the message states a CAUSE, and a cause can be wrong. One release told users
|
|
2731
|
+
`drop-table <name>__dropped_<ts>` was a no-op when the DDL had worked perfectly:
|
|
2732
|
+
`VOLTRO_SOFT_DROP=1` had renamed the table, and the planner then read its own
|
|
2733
|
+
snapshot as one more undeclared table and proposed dropping it again. Fixed, and
|
|
2734
|
+
worth knowing as the shape to look for: an operation naming an object that the
|
|
2735
|
+
PREVIOUS operation created or renamed is a planner blind spot, not dead DDL. In the meantime the schema is unchanged and safe — nothing was
|
|
2703
2736
|
half-applied, and no fingerprint was written, so `voltro db plan` still shows you
|
|
2704
2737
|
the truth.
|
|
2705
2738
|
|
|
@@ -248,9 +248,48 @@ Enforcement: the [`crud.*` read helpers](/docs/data/crud) strip `.serverOnly()`
|
|
|
248
248
|
columns from every returned row **automatically** — you declare the exposure
|
|
249
249
|
policy once at the schema and can't forget it on a handler. For a hand-written
|
|
250
250
|
query, omit the column from the `output` schema (and don't put it in the returned
|
|
251
|
-
object)
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
object).
|
|
252
|
+
|
|
253
|
+
A hand-written output is the case `crud.*` cannot cover, so an audit checks it:
|
|
254
|
+
a wire-reachable query whose `source` table carries a `.serverOnly()` column that
|
|
255
|
+
its `output` **declares**. What that costs is different per command, on purpose:
|
|
256
|
+
|
|
257
|
+
| Command | On a leak |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `voltro serve` | **the boot fails** |
|
|
260
|
+
| `voltro doctor` | **exits non-zero** — put it in CI |
|
|
261
|
+
| `voltro dev` | warns, naming the query and column |
|
|
262
|
+
|
|
263
|
+
Dev only warns because a refused boot between two keystrokes is worse than the
|
|
264
|
+
bug; production is the opposite, so that is where the gate is. **Do not read a
|
|
265
|
+
green `voltro dev` boot as a clean audit** — the warning sits in the boot log
|
|
266
|
+
among everything else. `voltro doctor` is the check to automate.
|
|
267
|
+
|
|
268
|
+
`VOLTRO_SERVER_ONLY` moves the line in both directions: `strict` makes `voltro
|
|
269
|
+
dev` fail too, `warn` downgrades `voltro serve` to a warning, `off` silences it
|
|
270
|
+
entirely. The downgrades are documented rather than hidden because the
|
|
271
|
+
alternative to a stated escape hatch is deleting the marker — and a check whose
|
|
272
|
+
only way out is to disable it gets disabled.
|
|
273
|
+
|
|
274
|
+
### Gate CI on the audit having RUN, not on its silence
|
|
275
|
+
|
|
276
|
+
`voltro doctor` can only run this audit if it can load your descriptors. When it
|
|
277
|
+
cannot, it says so instead of claiming a pass:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
• serverOnly: NOT CHECKED — the app's descriptors could not be loaded (not a pass)
|
|
281
|
+
reason: Transform failed with 1 error:
|
|
282
|
+
src/queries/broken.query.ts:2:5: ERROR: Expected ";" but found "is"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`voltro doctor --json` carries the same answer as `serverOnly: { checked, reason?, leaks? }`.
|
|
286
|
+
Assert on `checked` — an app that leans on `.serverOnly()` should treat a
|
|
287
|
+
persistent skip as a failure, because a skipped audit and a clean one look
|
|
288
|
+
identical from the outside.
|
|
289
|
+
|
|
290
|
+
(`voltro check` does **not** run this audit. It has a live-api mode that has no
|
|
291
|
+
access to your table definitions, and a rule that fires in one of its two modes
|
|
292
|
+
would be worse than one that fires in neither.)
|
|
254
293
|
|
|
255
294
|
Distinct from `.encrypted()` on purpose: encryption at rest says nothing about
|
|
256
295
|
who may receive the plaintext — a private note you decrypt *for its owner* is a
|
|
@@ -114,6 +114,8 @@ ctx.store.select('notes')
|
|
|
114
114
|
|
|
115
115
|
There is no `'like'`. It used to be here, and it was a lie: it mapped to `contains`, so `.where('path', 'like', '/api/%')` matched only rows literally containing the characters `/api/%` and the wildcard you wrote did nothing. An operator named after SQL's must honour your wildcards or not exist.
|
|
116
116
|
|
|
117
|
+
> **Writing an in-memory store for tests? Do not reimplement these.** `evaluatePredicate` is exported from `@voltro/database` and is the same function the memory store runs, so a test double built on it agrees with a real database by construction. Hand-rolling the switch is where a stub quietly diverges — a `case 'contains'` written as `String(a).includes(b)` is case-SENSITIVE where the real one folds case, and coerces a number where the real one returns `false`. That stub passes queries a live dialect fails, which is the worst direction for a test double to be wrong in.
|
|
118
|
+
|
|
117
119
|
These are the only operators the ergonomic `.where(col, op, value)` form accepts. For `IS NULL` / `NOT IN` / `IS NOT NULL`, pass a predicate built with the `@voltro/database` helpers:
|
|
118
120
|
|
|
119
121
|
```ts
|
|
@@ -258,6 +258,19 @@ No naming trick is needed to keep something out of the router: the absence of th
|
|
|
258
258
|
|
|
259
259
|
> Before this convention, every `.tsx` under `src/pages/` became a route, so a co-located component silently got a URL. That route rendered nothing and nobody visited it in dev; the failure surfaced at the first production build. If you are upgrading, `voltro update` renames your pages for you.
|
|
260
260
|
|
|
261
|
+
## Two routes, one screen
|
|
262
|
+
|
|
263
|
+
When two URLs must render the same component — a versioned path kept alive because devices in the field are configured against it, say — re-export it instead of copying it:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
// src/pages/v2/page.tsx → /v2, rendering exactly what / renders
|
|
267
|
+
export { default, renderMode } from '../page'
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`export { default } from '…'` satisfies the page contract: the module has a default export, it just did not declare it here. Forwarding `renderMode` alongside it is what keeps the two routes from drifting apart — the build follows the forward when it computes the render profile, so `/v2` is classified the same as `/`, not silently as `static`.
|
|
271
|
+
|
|
272
|
+
`export { default as Screen } from '../page'` is the opposite: it renames the default away, leaving this module without one. That still fails the contract.
|
|
273
|
+
|
|
261
274
|
## Trailing slashes
|
|
262
275
|
|
|
263
276
|
The canonical form is **no trailing slash** — always link with `<Link to="/about">`, not `<Link to="/about/">`.
|