@voltro/cli 0.20.2 → 0.22.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 +398 -0
- package/THIRD-PARTY-NOTICES.md +61 -61
- package/dist/{apiBuild-qZBEu59d.js → apiBuild-2zRxj2hJ.js} +93 -80
- package/dist/apiBuild-sqNmWmY5.js +2 -0
- package/dist/bin.js +41 -20
- package/dist/{commands-kYbfVoPO.js → commands-DGaeTn2f.js} +3055 -2640
- package/dist/dbCommand-CO3eSAZR.js +2 -0
- package/dist/{dbCommand-BPp9R0Wc.js → dbCommand-DVASmZj2.js} +305 -249
- package/dist/dev-BEYBSGJ7.js +3 -0
- package/dist/{dev-OHLsAq4K.js → dev-DeHVUxQx.js} +1732 -1686
- package/dist/index.js +1 -1
- package/dist/inspect-BA67TF6v.js +2 -0
- package/dist/inspect-_ldwsAwH.js +945 -0
- package/dist/{inspectMetrics-DvPNXmGA.js → inspectMetrics-4AVPn6IS.js} +584 -571
- package/dist/{manifestBuild-P9yuCY2d.js → manifestBuild-Bs1Uw22_.js} +1 -1
- package/dist/manifestBuild-i-fRHg_H.js +2 -0
- package/dist/serveCommand-DEVTtnLy.js +1161 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-CXQ7WL1W.js → start-BjGUIIvj.js} +389 -389
- package/dist/startEntry.js +2 -2
- package/package.json +27 -27
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +181 -5
- package/templates/agent-docs/data.md +58 -0
- package/templates/agent-docs/database/migrations.md +175 -31
- package/templates/agent-docs/database/schema.md +7 -0
- package/templates/agent-docs/database/seedsdialects.md +3 -3
- package/templates/agent-docs/plugins.md +1 -1
- package/templates/agent-docs/schema-driven-ui.md +2 -2
- package/templates/agent-docs/templates/apibackends.md +1 -1
- package/templates/agent-docs/whats-new.md +236 -9
- package/templates/apps/api-ai/package.json +12 -12
- package/templates/apps/api-auth/package.json +13 -13
- package/templates/apps/api-backend/package.json +12 -12
- package/templates/apps/api-backend-deactivation/package.json +12 -12
- package/templates/apps/api-backend-mail/package.json +11 -11
- package/templates/apps/api-backend-mariadb/package.json +14 -14
- package/templates/apps/api-backend-storage/package.json +11 -11
- package/templates/apps/api-data-advanced/package.json +13 -13
- package/templates/apps/api-durable/package.json +13 -13
- package/templates/apps/api-feature-flags/package.json +14 -14
- package/templates/apps/api-governance/package.json +13 -13
- package/templates/apps/api-kv/package.json +13 -13
- package/templates/apps/api-moderation/package.json +13 -13
- package/templates/apps/api-observability/package.json +13 -13
- package/templates/apps/api-ratelimit/package.json +13 -13
- package/templates/apps/api-rbac/package.json +13 -13
- package/templates/apps/api-rest/package.json +12 -12
- package/templates/apps/api-saas/package.json +16 -16
- package/templates/apps/api-search/package.json +13 -13
- package/templates/apps/api-versioning/package.json +13 -13
- package/templates/apps/api-webhooks/package.json +14 -14
- package/templates/apps/changelog/package.json +8 -8
- package/templates/apps/edge-functions/package.json +5 -5
- package/templates/apps/frontend-admin/package.json +10 -10
- package/templates/apps/frontend-app/package.json +10 -10
- package/templates/apps/frontend-blank/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +11 -11
- package/templates/apps/frontend-dashboard/package.json +9 -9
- package/templates/apps/frontend-docs/package.json +9 -9
- package/templates/apps/frontend-i18n/package.json +8 -8
- package/templates/apps/frontend-landing/package.json +9 -9
- package/templates/apps/frontend-spa/package.json +9 -9
- package/templates/apps/frontend-ssr/package.json +9 -9
- package/templates/apps/frontend-ssr-api/package.json +10 -10
- package/templates/apps/frontend-static-blog/package.json +8 -8
- package/dist/apiBuild-C9aHz4Yh.js +0 -2
- package/dist/dbCommand-DtUe-0dM.js +0 -2
- package/dist/dev-2AvdzDg2.js +0 -3
- package/dist/inspect-DcZ04OME.js +0 -2
- package/dist/inspect-Dwx0_tUj.js +0 -921
- package/dist/manifestBuild-D1MzJAiQ.js +0 -2
- package/dist/serveCommand-BKqTKWTX.js +0 -1136
- /package/templates/patches/{@effect__cluster@0.59.0.patch → @effect__cluster@0.60.0.patch} +0 -0
|
@@ -89,7 +89,8 @@ voltro db plan --against <url> # diff vs a remote env's inspect endpoint (see
|
|
|
89
89
|
voltro db apply # execute (dev only — refuses on NODE_ENV=production)
|
|
90
90
|
voltro db apply --note '...' # apply with a freeform note recorded in history
|
|
91
91
|
voltro db plans [--limit 20] # history from _voltro_migration_plans, newest first
|
|
92
|
-
voltro db drift # live-vs-
|
|
92
|
+
voltro db drift # live-vs-baseline check — exit 0 match, 3 no baseline, 4 drift
|
|
93
|
+
voltro db drift --accept # record the CURRENT live schema as the baseline (refuses unless db plan is empty)
|
|
93
94
|
voltro db squash --before <date> # consolidate history into one snapshot
|
|
94
95
|
voltro db restore-snapshot <id> # restore VOLTRO_SOFT_DROP=1 columns from a plan
|
|
95
96
|
|
|
@@ -119,6 +120,21 @@ there is no `--json` or `--sql`, and `voltro db apply` takes only
|
|
|
119
120
|
deploy step ([prod pipeline](./prod-pipeline.md)), not a pre-serialised
|
|
120
121
|
plan file.
|
|
121
122
|
|
|
123
|
+
## Framework tables ride the same differ
|
|
124
|
+
|
|
125
|
+
The `_voltro_*` tables the framework owns are planned, classified and applied by
|
|
126
|
+
exactly the same code as yours — on every dialect. A framework release that adds
|
|
127
|
+
a table, adds a column or reshapes one lands on the boot that follows your
|
|
128
|
+
upgrade, wherever your own schema changes land. There is no separate command and
|
|
129
|
+
no dialect-specific step.
|
|
130
|
+
|
|
131
|
+
One asymmetry is deliberate and worth knowing if you ever read a plan: a
|
|
132
|
+
framework-owned table that **nobody declares** — `cluster_*` from the workflow
|
|
133
|
+
engine, a plugin's table after you removed the plugin — is never planned for a
|
|
134
|
+
drop. "Nobody declared it, so do not drop it" and "we declare it, so keep it
|
|
135
|
+
current" are different rules; collapsing them is what once made framework tables
|
|
136
|
+
evolve on postgres and nowhere else.
|
|
137
|
+
|
|
122
138
|
## Where to go next
|
|
123
139
|
|
|
124
140
|
| Topic | Page |
|
|
@@ -367,6 +383,92 @@ If the live DB doesn't have a column called `firstName`, the marker is a no-op:
|
|
|
367
383
|
|
|
368
384
|
The marker isn't validated against the live DB at schema-build time — it would have to introspect during type-checking, which is expensive. The runtime check fires at plan time.
|
|
369
385
|
|
|
386
|
+
## The indexes come with it
|
|
387
|
+
|
|
388
|
+
Renaming a column is a metadata-only operation. Its **indexes** used to not be: index names are derived (`<table>_<column>_idx`), and no dialect renames an index when the column under it is renamed — so the planner saw `users_firstName_idx` on one side and `users_givenName_idx` on the other, and planned `DROP INDEX` + `CREATE INDEX`. On a large table that is a full B-tree rebuild: minutes of IO, and without `CONCURRENTLY` a write lock, behind a rename that was supposed to be instant.
|
|
389
|
+
|
|
390
|
+
The planner now folds that into a `rename-index` operation, which is a catalog-only statement everywhere it is emitted:
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
✓ rename-column users.firstName → users.givenName # catalog-only
|
|
394
|
+
✓ rename-index users_firstName_idx → users_givenName_idx # catalog-only, no rebuild
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
You do not annotate anything for this — it follows from the column rename you already declared.
|
|
398
|
+
|
|
399
|
+
Four cases deliberately still plan as drop + create, because pairing an old index with a new one has no evidence to stand on in them:
|
|
400
|
+
|
|
401
|
+
- **sqlite** — it has no rename statement at all. The plan you read matches what runs.
|
|
402
|
+
- **UNIQUE indexes** — they are constraint objects, and the syntax to rename one diverges by dialect.
|
|
403
|
+
- **Expression / json-path indexes** — the database normalises their key text, so there is no shape to compare; only the name, which is the thing that changed.
|
|
404
|
+
- **Two same-shaped indexes renamed at once** — nothing says which became which. Rebuilding both is slower; renaming the wrong one is worse.
|
|
405
|
+
|
|
406
|
+
## Renaming a TABLE
|
|
407
|
+
|
|
408
|
+
The same problem one level up, and with more at stake: a table rename and a
|
|
409
|
+
drop+create look identical to the differ — old table gone, new table present —
|
|
410
|
+
except that guessing wrong costs every row. So it needs a marker too, and it
|
|
411
|
+
reads like its column counterpart:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
// Before:
|
|
415
|
+
export const notes = table('notes', { id: id({ prefix: 'note' }), body: text() })
|
|
416
|
+
|
|
417
|
+
// After — without the marker:
|
|
418
|
+
export const notes = table('archive_notes', { id: id({ prefix: 'note' }), body: text() })
|
|
419
|
+
// → planner sees DROP TABLE notes + CREATE TABLE archive_notes
|
|
420
|
+
// → the DROP is `lossy` and blocked; nothing happens until you acknowledge it
|
|
421
|
+
|
|
422
|
+
// With the marker:
|
|
423
|
+
export const notes = table('archive_notes', { id: id({ prefix: 'note' }), body: text() })
|
|
424
|
+
.renamedFrom('notes')
|
|
425
|
+
// → one `rename-table` op, classified `safe`
|
|
426
|
+
// → `ALTER TABLE notes RENAME TO archive_notes` — catalog-only, the rows stay put
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Unlike an index rename, every dialect has this statement — sqlite included — so
|
|
430
|
+
there is no dialect on which this falls back to a rebuild.
|
|
431
|
+
|
|
432
|
+
**Its indexes come with it.** The same derivation that bites a column rename bites
|
|
433
|
+
harder here: `notes_pkey` and `notes_<col>_idx` are named after the table, and no
|
|
434
|
+
dialect renames them when the table is renamed. The planner emits a `rename-index`
|
|
435
|
+
for each so the catalog catches up:
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
✓ rename-table notes → archive_notes
|
|
439
|
+
✓ rename-index notes_pkey → archive_notes_pkey
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Without that the plan would try to drop the primary-key index and re-add it as a
|
|
443
|
+
plain UNIQUE, which postgres refuses outright.
|
|
444
|
+
|
|
445
|
+
**Three cases where the planner will NOT fold the rename**, each because folding
|
|
446
|
+
it could destroy data rather than move it — and none of them is silent:
|
|
447
|
+
|
|
448
|
+
- **The old name is still declared by something.** If your schema still has a
|
|
449
|
+
`notes` table, it is yours and stays put; the new table is created empty. This
|
|
450
|
+
is a legitimate outcome (it is what lets a framework plugin reclaim a name
|
|
451
|
+
without taking yours), so the plan runs — and the `create-table` line says why
|
|
452
|
+
the marker was not applied.
|
|
453
|
+
- **The new name already exists in the database.** → **refuses to plan.** Both
|
|
454
|
+
tables exist and only you know which holds the real rows. The fix tells you to
|
|
455
|
+
move them and drop one, or drop the empty one so the rename can run. Until
|
|
456
|
+
then the old table is untouched.
|
|
457
|
+
- **Two tables both claim the same old name.** → **refuses to plan.** Nothing
|
|
458
|
+
says which should receive the rows; remove the marker from all but one.
|
|
459
|
+
|
|
460
|
+
The last two refuse rather than degrade, because the quiet outcome — an empty
|
|
461
|
+
plan reading "schema up to date" while the old table still holds every row — is
|
|
462
|
+
the one that loses data by inaction.
|
|
463
|
+
|
|
464
|
+
**Lifecycle.** Same as `.renamedFrom()` on a column: a marker whose old table is
|
|
465
|
+
not in the database is a silent no-op, so it stays in your source across a staged
|
|
466
|
+
rollout and comes out once every environment has applied it.
|
|
467
|
+
|
|
468
|
+
**One constraint worth knowing:** a table whose name starts with `_` cannot derive
|
|
469
|
+
a typeid prefix, so it needs an explicit `id({ prefix: '…' })`. You will hear about
|
|
470
|
+
it at declaration, not at runtime.
|
|
471
|
+
|
|
370
472
|
## Lifecycle — when to remove the marker
|
|
371
473
|
|
|
372
474
|
Keep the marker until the rename has been applied in EVERY env you care about (dev, staging, prod). The framework tracks applied ops in `_voltro_migration_plans`:
|
|
@@ -2009,7 +2111,7 @@ Drift = the live database's schema doesn't match the fingerprint of the last app
|
|
|
2009
2111
|
|
|
2010
2112
|
Three trigger paths:
|
|
2011
2113
|
|
|
2012
|
-
1. **Manual** — `voltro db drift` runs the check on demand,
|
|
2114
|
+
1. **Manual** — `voltro db drift` runs the check on demand: exit 0 match, 3 no baseline, 4 drift
|
|
2013
2115
|
2. **On boot (dev)** — every `voltro dev` boot runs the planner, which detects drift implicitly (the plan will be non-empty)
|
|
2014
2116
|
3. **Periodic (cloud)** — the cloud dashboard polls each app's `/_voltro/inspect/migrations` endpoint; drift state is in the response
|
|
2015
2117
|
|
|
@@ -2018,16 +2120,39 @@ All three paths produce the same `DriftSnapshot` shape:
|
|
|
2018
2120
|
```ts
|
|
2019
2121
|
{
|
|
2020
2122
|
isDrifted: boolean,
|
|
2021
|
-
liveFingerprint: string, // current introspected
|
|
2022
|
-
lastAppliedFingerprint?: string, // newest
|
|
2123
|
+
liveFingerprint: string, // current introspected schema fingerprint
|
|
2124
|
+
lastAppliedFingerprint?: string, // the BASELINE the newest row recorded
|
|
2023
2125
|
lastAppliedAt?: string, // when it was applied
|
|
2024
2126
|
lastAppliedId?: string, // plan id
|
|
2025
2127
|
}
|
|
2026
2128
|
```
|
|
2027
2129
|
|
|
2028
|
-
`isDrifted: false` ↔ `liveFingerprint === lastAppliedFingerprint
|
|
2130
|
+
`isDrifted: false` ↔ `liveFingerprint === lastAppliedFingerprint`, **or** no baseline was recorded — nothing compared is not the same as nothing changed, and it is never reported as drift.
|
|
2131
|
+
|
|
2132
|
+
### What it compares, and what it does not
|
|
2133
|
+
|
|
2134
|
+
The baseline is `_voltro_migration_plans.liveFingerprint`: the fingerprint of the **live** schema as it was immediately after the last `voltro db apply`. Not `fingerprint`, which is the **declared** snapshot's hash — introspection cannot recover everything a declaration carries (generated expressions, `maxLength`, sensitivity markers), so a live hash and a declared hash never agree and comparing them reports drift on every clean database.
|
|
2135
|
+
|
|
2136
|
+
Both sides hash the **whole** live schema, framework tables included. A framework upgrade that adds a `_voltro_*` column therefore shows as drift until the next `db apply` records a new baseline — honest, since the live schema did change, and it self-heals on the apply the upgrade needs anyway.
|
|
2137
|
+
|
|
2138
|
+
### No baseline yet
|
|
2139
|
+
|
|
2140
|
+
A ledger row written before `liveFingerprint` existed has no baseline, and so does a database whose schema was already current when it upgraded. `voltro db drift` says so and **exits 3**:
|
|
2141
|
+
|
|
2142
|
+
```sh
|
|
2143
|
+
$ voltro db drift
|
|
2144
|
+
db drift: no drift baseline recorded yet — cannot compare
|
|
2145
|
+
```
|
|
2146
|
+
|
|
2147
|
+
Run `voltro db apply` once — a no-op apply backfills the baseline too. Until then use `voltro db plan`, which compares declared against live directly.
|
|
2029
2148
|
|
|
2030
|
-
|
|
2149
|
+
**Exit 3 is deliberately not 0.** "Did not compare" is not "clean", and a CI gate on the exit code has to be able to tell them apart — otherwise it passes vacuously on a stable schema, which is the failure drift detection exists to prevent.
|
|
2150
|
+
|
|
2151
|
+
| exit | meaning |
|
|
2152
|
+
|---|---|
|
|
2153
|
+
| 0 | compared, live matches the baseline |
|
|
2154
|
+
| 3 | no baseline — did NOT compare |
|
|
2155
|
+
| 4 | compared, live diverged |
|
|
2031
2156
|
|
|
2032
2157
|
## Common causes
|
|
2033
2158
|
|
|
@@ -2038,16 +2163,30 @@ Someone ran `ALTER TABLE ...` or `CREATE INDEX ...` via psql / DataGrip / Admine
|
|
|
2038
2163
|
```sh
|
|
2039
2164
|
$ voltro db drift
|
|
2040
2165
|
db drift: live schema DIVERGED from last applied state
|
|
2041
|
-
|
|
2042
|
-
live now:
|
|
2166
|
+
baseline: 8f507ba1e1aadad5 at 2026-06-15 14:32:00 (plan_mig_5k78)
|
|
2167
|
+
live now: a8f2c9d10b3f4e62
|
|
2168
|
+
|
|
2169
|
+
Something changed the live schema after the last apply. This command can see
|
|
2170
|
+
THAT it changed, not what or who — the fingerprints are hashes, not a diff.
|
|
2043
2171
|
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2172
|
+
Your DECLARED schema is already satisfied — `voltro db plan` reports 0 operations
|
|
2173
|
+
against this database. So the live schema is not wrong, only unrecorded: something
|
|
2174
|
+
applied a change without going through the planner (a hand-run ALTER, a DBA
|
|
2175
|
+
window, a restored dump), or it touched a table your code does not declare.
|
|
2047
2176
|
|
|
2048
|
-
|
|
2177
|
+
If that was deliberate and the schema is right, record it:
|
|
2178
|
+
|
|
2179
|
+
voltro db drift --accept
|
|
2180
|
+
|
|
2181
|
+
It updates the latest ledger row's baseline to the live schema and invents no
|
|
2182
|
+
history entry. Drift then measures from here.
|
|
2049
2183
|
```
|
|
2050
2184
|
|
|
2185
|
+
When `db plan` is NOT empty it says that instead, with the count — so the two
|
|
2186
|
+
cases are told apart by the command rather than left to you. It used to close by
|
|
2187
|
+
asserting that a zero-operation plan meant "a table your code does not declare",
|
|
2188
|
+
which is one of two possibilities and the less likely one.
|
|
2189
|
+
|
|
2051
2190
|
### Out-of-band auto-applier
|
|
2052
2191
|
|
|
2053
2192
|
Multiple tools applying to the same DB (the framework + a separate Flyway / Liquibase process / hand-written deploy script). The other tool's changes don't go through `_voltro_migration_plans`.
|
|
@@ -2062,31 +2201,36 @@ The drift detector ran against a read-replica that's lagging. Wait for the repli
|
|
|
2062
2201
|
|
|
2063
2202
|
## Reconciliation paths
|
|
2064
2203
|
|
|
2065
|
-
### Path 1 —
|
|
2204
|
+
### Path 1 — accept the live state
|
|
2066
2205
|
|
|
2067
|
-
When the live DB IS what you want (the manual DDL is correct, only
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
`voltro db apply` records a fresh `_voltro_migration_plans` row at the
|
|
2072
|
-
new fingerprint — re-baselining history without any DDL.
|
|
2206
|
+
When the live DB IS what you want (the manual DDL is correct, only bypassing the
|
|
2207
|
+
planner was sloppy), first make sure your declaration says so: edit the
|
|
2208
|
+
`*.entity.ts` files until `voltro db plan` diffs empty. Then record the live
|
|
2209
|
+
schema as the baseline:
|
|
2073
2210
|
|
|
2074
2211
|
```sh
|
|
2075
|
-
voltro db plan
|
|
2076
|
-
voltro db
|
|
2212
|
+
voltro db plan # must report 0 operations — declared == live
|
|
2213
|
+
voltro db drift --accept
|
|
2077
2214
|
```
|
|
2078
2215
|
|
|
2079
|
-
`
|
|
2080
|
-
|
|
2216
|
+
`--accept` writes the current live fingerprint onto the newest ledger row.
|
|
2217
|
+
Drift measures from there, and the next `voltro db drift` exits 0.
|
|
2081
2218
|
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2219
|
+
**It refuses unless `db plan` is empty**, and that guard is the whole point.
|
|
2220
|
+
Accepting a schema with operations still outstanding would record "this is what
|
|
2221
|
+
we applied" over a state nobody applied, and every later drift check would
|
|
2222
|
+
measure against that fiction. If operations are pending, run `voltro db apply` —
|
|
2223
|
+
that applies them AND writes a real baseline of its own.
|
|
2224
|
+
|
|
2225
|
+
It backfills the newest row rather than inserting one, because no migration ran
|
|
2226
|
+
and a history entry claiming otherwise would be worse than the gap it fills.
|
|
2086
2227
|
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2228
|
+
**An empty `voltro db apply` does NOT re-baseline an existing baseline.** It
|
|
2229
|
+
writes no DDL and no history row, so there is nothing for a `--note` to attach
|
|
2230
|
+
to — it will tell you the note was ignored rather than swallow it. (It does fill
|
|
2231
|
+
a baseline that is still NULL, which is a different case: a database that never
|
|
2232
|
+
had one.) `--accept` is the command whose job is to say "the live schema is
|
|
2233
|
+
right, the ledger just did not know".
|
|
2090
2234
|
|
|
2091
2235
|
### Path 2 — corrective plan against drift
|
|
2092
2236
|
|
|
@@ -2184,7 +2328,7 @@ The hardest part of drift response is figuring out **what** changed + **who** di
|
|
|
2184
2328
|
```
|
|
2185
2329
|
Detect: voltro db drift
|
|
2186
2330
|
Fix code: voltro db apply (apply corrective plan from current diff)
|
|
2187
|
-
Adopt DB: edit the *.entity.ts to match live, then voltro db
|
|
2331
|
+
Adopt DB: edit the *.entity.ts to match live until db plan is empty, then voltro db drift --accept
|
|
2188
2332
|
Backup: if data was lost, restore from your DB backup system — the framework can't help
|
|
2189
2333
|
```
|
|
2190
2334
|
|
|
@@ -73,6 +73,13 @@ Plain `text()` is **unbounded** — `TEXT` on Postgres/SQLite, `LONGTEXT` on MyS
|
|
|
73
73
|
|
|
74
74
|
Reach for **`.maxLength(n)`** when the column is a short identifier (a kind, slug, external id, resource discriminator) that participates in a `.unique([...])` or composite `.index([...])`: an unbounded column can only back a HASH long-unique constraint (MariaDB) or a prefix index, whereas a bounded `VARCHAR(n)` takes a plain BTREE key. It's cross-dialect (`VARCHAR(n)` on Postgres/MySQL/MariaDB, `NVARCHAR(n)` on MSSQL, `TEXT` on SQLite) — unlike `raw('varchar(20)')`, which hardcodes one dialect's DDL.
|
|
75
75
|
|
|
76
|
+
**Changing the bound on an EXISTING column is a real migration.** The differ compares `maxLength` on both sides, so `text()` → `text().maxLength(64)`, or 64 → 128, produces an operation:
|
|
77
|
+
|
|
78
|
+
- **widening** (a larger bound, or dropping the bound) is `safe` — no value can be lost;
|
|
79
|
+
- **narrowing** is `needs-backfill`: the `ALTER` fails at the database for any existing value longer than the new bound. The plan says so and gives you the query to run first.
|
|
80
|
+
|
|
81
|
+
Not compared on SQLite, which has no length-enforced type — a declared bound there is real, the live column is always `TEXT`, and comparing them would re-plan the same ALTER on every boot.
|
|
82
|
+
|
|
76
83
|
For **value** constraints (length ranges, regex, email, numeric min/max, cross-field invariants), use table-level `table().validate(Schema)` (see below) — that's the single validation surface, not a pile of per-column modifiers. There are no `.minLength()` / `.pattern()` column modifiers by design.
|
|
77
84
|
|
|
78
85
|
### `integer()`
|
|
@@ -996,7 +996,7 @@ If you write hand-rolled queries that read JSON columns through `unsafe()`, you'
|
|
|
996
996
|
> then applies it. Idempotent, and a no-op on every other dialect. Without it,
|
|
997
997
|
> workflow message/runner storage misbehaves on SQL Server.
|
|
998
998
|
|
|
999
|
-
`@effect/cluster`'s mssql branch uses `sp_getapplock` for shard claims and `MERGE … WHEN NOT MATCHED THEN INSERT … OUTPUT INSERTED` for runner upserts. Four bugs in `@effect/cluster@0.
|
|
999
|
+
`@effect/cluster`'s mssql branch uses `sp_getapplock` for shard claims and `MERGE … WHEN NOT MATCHED THEN INSERT … OUTPUT INSERTED` for runner upserts. Four bugs in `@effect/cluster@0.60.0` mssql code paths fail under the framework's workflow stack; the framework carries a `pnpm patch` (shipped in the CLI at `packages/cli/templates/patches/@effect__cluster@0.60.0.patch`, written into your project by `voltro add mssql`):
|
|
1000
1000
|
|
|
1001
1001
|
### Patch 1 — SqlRunnerStorage shard-lock MERGE alias
|
|
1002
1002
|
|
|
@@ -1024,7 +1024,7 @@ MSSQL forbids subqueries inside OUTPUT clauses (Msg 10705). The original `insert
|
|
|
1024
1024
|
|
|
1025
1025
|
The cluster's `deliver_at` column is BIGINT (storing millisecond epoch). Tedious binds JS `number` parameters as INT — which overflows for any post-2001 timestamp. The patch casts `deliver_at` to `BigInt` once at the top of `envelopeToRow` so all three message-kind switch arms emit `bigint | null`; other dialects accept bigint fine.
|
|
1026
1026
|
|
|
1027
|
-
All four patches are dialect-keyed (touch only the `mssql:` branch of `sql.onDialectOrElse`) so postgres / mysql / sqlite paths are bit-identical to upstream. The framework carries them and ships them to your project via `voltro add mssql` (see the note at the top of this section) —
|
|
1027
|
+
All four patches are dialect-keyed (touch only the `mssql:` branch of `sql.onDialectOrElse`) so postgres / mysql / sqlite paths are bit-identical to upstream. The framework carries them and ships them to your project via `voltro add mssql` (see the note at the top of this section) — the patch is how the fix reaches you. Upstream still carries these bugs on the current release — the framework re-verifies that on every bump (`git apply --check` against the new tarball) and re-keys the patch, because a `patchedDependencies` key is version-exact and a stale one fails the install.
|
|
1028
1028
|
|
|
1029
1029
|
## Read replicas — Always-On Availability Groups
|
|
1030
1030
|
|
|
@@ -1088,7 +1088,7 @@ For a full before-image or per-op fidelity, use **mariadb** (binlog CDC). For ze
|
|
|
1088
1088
|
- `voltro/packages/sql-mssql/src/replicationAdapter.ts` — `end_of_log_lsn` / `last_hardened_lsn` compare for RYW
|
|
1089
1089
|
- `voltro/packages/database/src/sqlCompiler.ts` — TOP / FETCH NEXT dispatch
|
|
1090
1090
|
- `voltro/packages/database/src/migrate.ts` — mssql DDL branch (line 72)
|
|
1091
|
-
- `packages/cli/templates/patches/@effect__cluster@0.
|
|
1091
|
+
- `packages/cli/templates/patches/@effect__cluster@0.60.0.patch` — the four upstream patches (shipped in the CLI; `voltro add mssql` writes it into your project)
|
|
1092
1092
|
|
|
1093
1093
|
|
|
1094
1094
|
|
|
@@ -92,7 +92,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
92
92
|
| `@voltro/plugin-openapi` | ✓ | OpenAPI 3.1 spec (`GET /openapi.json`) + Swagger-UI (`GET /docs`) generated from `defineRestRoute` descriptors AND (opt-in) rpc procedures (queries/mutations/actions/streams → `POST /rpc/<name>`) — input/output/error Schemas via `JSONSchema.make`. [→ details](/docs/plugins/openapi) |
|
|
93
93
|
| `@voltro/plugin-versioning` | ✓ | Full row history + time-travel — value snapshot of every insert/update/delete on listed tables into `_voltro_row_history` (rides the ChangeEvent tap); `rowHistory` / `rowAsOf` queries + `restoreAsOf` / `diffVersions`; TTL + per-row cap retention. [→ details](/docs/plugins/versioning) |
|
|
94
94
|
| `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator, swept `_voltro_presence` table, cross-instance. [→ details](/docs/plugins/presence) |
|
|
95
|
-
| `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `
|
|
95
|
+
| `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `_voltro_scim_users`/`_voltro_scim_groups`. [→ details](/docs/plugins/scim) |
|
|
96
96
|
| `@voltro/plugin-sso-saml` | ✓ | Enterprise SAML 2.0 SSO — SP-initiated login + Single Logout (both directions) + ACS + SP metadata under `/saml`; IdP-metadata-URL auto cert rotation, encrypted assertions, clock-skew, SP request signing. Signature verify via `@node-saml/node-saml` (optional+lazy), mints a framework session. [→ details](/docs/plugins/sso-saml) |
|
|
97
97
|
|
|
98
98
|
API keys are **first-class** (not a plugin): `apiKeys: true` in `app.config.ts` → Bearer-key auth + admin-gated `/v1/api-keys` management, hash-only storage. [→ details](/docs/configuration/api-keys)
|
|
@@ -143,7 +143,7 @@ create-vs-update: they're *different mutations with different input schemas*, so
|
|
|
143
143
|
they're naturally different forms — no "CRUD mode" switch.
|
|
144
144
|
|
|
145
145
|
```tsx
|
|
146
|
-
import { AutoForm } from '@voltro/
|
|
146
|
+
import { AutoForm } from '@voltro/ui'
|
|
147
147
|
|
|
148
148
|
// fields from the mutation's input schema; client+server share the schema;
|
|
149
149
|
// submits via the mutation with op-correct optimistic (insert prepends, etc.)
|
|
@@ -177,7 +177,7 @@ A combined create-or-edit screen is a three-line wrapper:
|
|
|
177
177
|
object → a `custom` placeholder asking for a render-prop.
|
|
178
178
|
- **Rung 1 — one custom widget** via a `<Field>` render-prop:
|
|
179
179
|
```tsx
|
|
180
|
-
import { AutoForm, Field, AsyncSelect } from '@voltro/
|
|
180
|
+
import { AutoForm, Field, AsyncSelect } from '@voltro/ui'
|
|
181
181
|
|
|
182
182
|
<AutoForm api="app" mutation="todos.create">
|
|
183
183
|
{() => (
|
|
@@ -2056,7 +2056,7 @@ curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
|
|
|
2056
2056
|
# → { "ok": false, "error": { "_tag": "EntitlementExceeded", "entitlement": "projects", "limit": 3, "used": 3 } }
|
|
2057
2057
|
```
|
|
2058
2058
|
|
|
2059
|
-
Each successful create also logs `[notify:project] → …: Project created` (console channel) and persists a `
|
|
2059
|
+
Each successful create also logs `[notify:project] → …: Project created` (console channel) and persists a `_voltro_notification_inbox` row.
|
|
2060
2060
|
|
|
2061
2061
|
## Enable durable analytics
|
|
2062
2062
|
|