@voltro/cli 0.52.0 → 0.53.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 +229 -0
- package/THIRD-PARTY-NOTICES.md +8311 -3318
- package/dist/{apiBuild-CSFI8QGq.js → apiBuild-CaPfoWku.js} +11 -5
- package/dist/apiBuild-DHtLXYx9.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/build-D-OnvNMf.js +843 -0
- package/dist/{checkCommand-COmqc2cB.js → checkCommand-C5elt0tW.js} +6 -6
- package/dist/{checkCommand-2SbqzukH.js → checkCommand-D2ZduVlh.js} +1 -1
- package/dist/codegen-BWpt3VgF.js +2 -0
- package/dist/{codegen-VF479Cnb.js → codegen-FEk8AZHb.js} +2 -2
- package/dist/{codegenCommand-DCdG2JN-.js → codegenCommand-BOiWQ5hz.js} +4 -4
- package/dist/{codemodRunner-r7J9lIa7.js → codemodRunner-BjtB2lq6.js} +109 -11
- package/dist/{commands-Cc_nV8WI.js → commands-DyxAmhP0.js} +30 -30
- package/dist/{dashboardCommand-C-gKvwqh.js → dashboardCommand-BdKTyT13.js} +5 -5
- package/dist/{dataCommand-BgpBHnlB.js → dataCommand-Bab9X7s8.js} +2 -2
- package/dist/{dbCommand-sHedr-NJ.js → dbCommand-06O2finM.js} +277 -236
- package/dist/dbCommand-B1EXBC6f.js +2 -0
- package/dist/{dev-CRHoCEiy.js → dev-C6LGF4iY.js} +3006 -2426
- package/dist/{dev--A3nsxA3.js → dev-GjJWAYo2.js} +1 -1
- package/dist/doctorCommand-B0hX0tdz.js +2 -0
- package/dist/{doctorCommand-DtfJ3FA6.js → doctorCommand-etMkflRc.js} +101 -69
- package/dist/{dormancyCommand-Drn7o0No.js → dormancyCommand-UwZ1AZzB.js} +1 -1
- package/dist/{embeddingsCommand-Z-jO1fWN.js → embeddingsCommand-C70zWHwo.js} +1 -1
- package/dist/{envCommand-D4gCrrTZ.js → envCommand-dSyKvRkM.js} +18 -18
- package/dist/{evolveCommand-CMROeKeA.js → evolveCommand-CG0_ebO5.js} +2 -2
- package/dist/fileConventions-DASGEmj-.js +35 -0
- package/dist/{fileTaxonomy-DvDUV9wq.js → fileTaxonomy-B7uxipWS.js} +55 -55
- package/dist/fontPipeline-LxIHa1vo.js +2 -0
- package/dist/fontPipeline-Tsh8kZfA.js +152 -0
- package/dist/frameworkTableAssembly-C_7Z-rMs.js +2 -0
- package/dist/{frameworkTableAssembly-w-XnLa3q.js → frameworkTableAssembly-DKx3ba3S.js} +5 -5
- package/dist/imagePipeline-B_GVJgm6.js +2 -0
- package/dist/imagePipeline-CBZmjT4i.js +127 -0
- package/dist/index.js +1 -1
- package/dist/{infoCommand-DXM868o_.js → infoCommand-_53iOc_j.js} +1 -1
- package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
- package/dist/inspect-CuoDInfZ.js +2 -0
- package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
- package/dist/manifestBuild-C4-J1-m_.js +2 -0
- package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
- package/dist/{metaCommands-C6RFmF1r.js → metaCommands-Cn2oboG4.js} +8 -2
- package/dist/{migrate-D0F-eTlK.js → migrate-Cko9rswM.js} +2 -2
- package/dist/{pageConvention-CzUiSbtU.js → pageConvention-C938S8oC.js} +1 -1
- package/dist/{privacyCommand-DGdopOI6.js → privacyCommand-DWTQMC6R.js} +2 -2
- package/dist/{probeCommand-C9gazU0H.js → probeCommand-DkGGLknv.js} +83 -24
- package/dist/{renderModeScan-CcH2X1_D.js → renderModeScan-CUbOeOAg.js} +3 -1
- package/dist/{renderProfile-Ck32Fzxr.js → renderProfile-CskIgAfn.js} +2 -2
- package/dist/{runtimeTrace-BPQyCmC5.js → runtimeTrace-c0APJz7E.js} +1 -1
- package/dist/{sdkgen-Se88ifTd.js → sdkgen-BiQCgIEr.js} +1 -1
- package/dist/{serveCommand-DkP3OT0W.js → serveCommand-CueKQgzl.js} +869 -805
- package/dist/serveCommand-DsnrVN3U.js +2 -0
- package/dist/serveEntry.js +1 -1
- package/dist/start-BJzZLbt8.js +3 -0
- package/dist/start-ekPan8BT.js +1510 -0
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-BwNEDlSU.js → staticCommand-xlSL-IWk.js} +1 -1
- package/dist/{test-f3amja6a.js → test-BWPQcRoB.js} +1 -1
- package/dist/updateCommand-Bqql_rsQ.js +2 -0
- package/dist/{updateCommand-BMk2e4ky.js → updateCommand-C_8I8Rzo.js} +139 -115
- package/dist/webDev-C7jWJ5dX.js +2 -0
- package/dist/{webDev-BgWL9gKV.js → webDev-oczpugbx.js} +1455 -922
- package/dist/{webhooksCommand-CoIO3jbj.js → webhooksCommand-4SVPDjKg.js} +1 -1
- package/package.json +60 -19
- package/templates/AGENTS.md +5 -3
- package/templates/agent-docs/_index.md +5 -3
- package/templates/agent-docs/_manifest.json +20 -4
- package/templates/agent-docs/ai.md +2 -2
- package/templates/agent-docs/authentication.md +1 -1
- package/templates/agent-docs/cli.md +2 -2
- package/templates/agent-docs/configuration.md +17 -0
- package/templates/agent-docs/data.md +470 -26
- package/templates/agent-docs/database/advancedqueries.md +7 -7
- package/templates/agent-docs/database/columntypes.md +2 -2
- package/templates/agent-docs/database/querying.md +1 -1
- package/templates/agent-docs/database/schema.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +1 -1
- package/templates/agent-docs/database/transactions.md +3 -3
- package/templates/agent-docs/deployment.md +8 -0
- package/templates/agent-docs/internationalization.md +2 -2
- package/templates/agent-docs/introduction.md +7 -1
- package/templates/agent-docs/local-first-mobile.md +102 -4
- package/templates/agent-docs/observability.md +2 -0
- package/templates/agent-docs/plugins/auth.md +1 -1
- package/templates/agent-docs/plugins/comments.md +142 -0
- package/templates/agent-docs/plugins/notifications.md +47 -4
- package/templates/agent-docs/plugins/presence.md +16 -3
- package/templates/agent-docs/plugins/prometheus.md +1 -1
- package/templates/agent-docs/plugins/queue.md +129 -0
- package/templates/agent-docs/plugins.md +11 -7
- package/templates/agent-docs/reference.md +16 -4
- package/templates/agent-docs/routing.md +527 -3
- package/templates/agent-docs/schema-driven-ui.md +214 -3
- package/templates/agent-docs/security.md +3 -3
- package/templates/agent-docs/whats-new.md +160 -66
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/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-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +10 -10
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/app.config.ts +26 -2
- package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
- package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
- package/templates/apps/changelog/package.json +8 -8
- package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
- package/templates/apps/changelog/src/globals.d.ts +1 -1
- package/templates/apps/changelog/src/locales/de.ts +1 -1
- package/templates/apps/changelog/src/locales/en.ts +1 -1
- package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
- package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
- package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
- package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
- package/templates/apps/changelog/src/pages/page.tsx +18 -12
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/package.json +8 -6
- package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
- package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
- package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
- package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +6 -6
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
- package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
- package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-BYBpL7Pz.js +0 -2
- package/dist/build-CPgcMQug.js +0 -793
- package/dist/codegen-CctkDO-1.js +0 -2
- package/dist/dbCommand-DNb6yeOG.js +0 -2
- package/dist/doctorCommand-CqoWA2p5.js +0 -2
- package/dist/fileConventions-DOqD3lPS.js +0 -34
- package/dist/frameworkTableAssembly-C6ETawPR.js +0 -2
- package/dist/inspect-CuGDYES0.js +0 -2
- package/dist/manifestBuild-CPjhvM62.js +0 -2
- package/dist/serveCommand-DLc-BznW.js +0 -2
- package/dist/start-DfL3fOiN.js +0 -3
- package/dist/start-jw89Xbqy.js +0 -1339
- package/dist/updateCommand-5gFVfK5q.js +0 -2
- package/dist/webDev-CZbTsDcH.js +0 -2
- package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
- package/templates/apps/changelog/src/lib/releases.ts +0 -21
- package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
|
@@ -173,8 +173,10 @@ A combined create-or-edit screen is a three-line wrapper:
|
|
|
173
173
|
### The customization ladder
|
|
174
174
|
|
|
175
175
|
- **Rung 0 — zero config.** Fields render from the schema: a `Schema.Literal`
|
|
176
|
-
union → `select`; string → text; boolean → checkbox; `Date` → date;
|
|
177
|
-
|
|
176
|
+
union → `select`; string → text; boolean → checkbox; `Schema.Date` → date;
|
|
177
|
+
a nested struct → a real `<fieldset>` SECTION with its legend and dotted
|
|
178
|
+
fields; widgets receive `onBlur`, so errors reveal on leave-field exactly
|
|
179
|
+
like the headless binding.
|
|
178
180
|
- **Rung 1 — one custom widget** via a `<Field>` render-prop:
|
|
179
181
|
```tsx
|
|
180
182
|
import { AutoForm, Field, AsyncSelect } from '@voltro/ui'
|
|
@@ -214,6 +216,189 @@ Schema.Struct({
|
|
|
214
216
|
})
|
|
215
217
|
```
|
|
216
218
|
|
|
219
|
+
### Validation — one schema, translated messages, server field errors
|
|
220
|
+
|
|
221
|
+
Client and server validate the SAME input schema, and the messages a user
|
|
222
|
+
sees are structured, not developer text. Every failed check maps to a stable
|
|
223
|
+
message id with params — `validation.required`, `validation.minLength {min}`,
|
|
224
|
+
`validation.minValue {min}` — rendered through a built-in en/de catalog. The
|
|
225
|
+
locale follows `<html lang>` (the framework's locale contract); override it
|
|
226
|
+
per form with `locale:`, or wire your own catalog in one line:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
// ONCE, at the app root — every form under it resolves ids through your catalog
|
|
230
|
+
<ValidationMessagesProvider messages={(id, params) => t(id, params)}>
|
|
231
|
+
<App />
|
|
232
|
+
</ValidationMessagesProvider>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
A per-form `messages:` option still exists and wins over the provider; a
|
|
236
|
+
resolver returning `undefined` falls through to the built-ins per id, so a
|
|
237
|
+
partial catalog costs nothing.
|
|
238
|
+
|
|
239
|
+
A widget kit that resolves ids itself reads the same resolver with
|
|
240
|
+
`useValidationMessages()`. It returns the provider's function, or
|
|
241
|
+
`undefined` when no provider is mounted — so a kit can fall through to the
|
|
242
|
+
built-in catalog instead of shipping its own.
|
|
243
|
+
|
|
244
|
+
Errors key the FULL field path (`address.city`, `entries.0.startsAt`), and a
|
|
245
|
+
`message` annotation on a schema may BE an id with params — as may the issues
|
|
246
|
+
a struct-level `filter` returns, which land at THEIR field:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const Input = Schema.Struct({
|
|
250
|
+
name: Schema.String.pipe(Schema.minLength(2)),
|
|
251
|
+
startsAt: Schema.Number,
|
|
252
|
+
endsAt: Schema.Number,
|
|
253
|
+
}).pipe(
|
|
254
|
+
Schema.filter((v) =>
|
|
255
|
+
v.startsAt < v.endsAt ? undefined : [{ path: ['endsAt'], message: 'validation.beforeStart' }],
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Server-side rules route to their field too.** An executor raises a typed
|
|
261
|
+
field error through the always-present `ctx.validation` — no declaration
|
|
262
|
+
needed, `ValidationError` is auto-merged into every mutation's and action's
|
|
263
|
+
wire error union exactly like `ScopeError`:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// executor
|
|
267
|
+
if (await emailTaken(input.email)) {
|
|
268
|
+
return yield* ctx.validation.fail('email', 'validation.emailTaken')
|
|
269
|
+
}
|
|
270
|
+
yield* ctx.validation.require(input.startsAt < input.endsAt, 'endsAt', 'validation.beforeStart')
|
|
271
|
+
// several at once: ctx.validation.failAll([{ field, message }, …])
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The binding routes it: the message lands in `errors.email` (translated
|
|
275
|
+
through the same catalog), the form stays editable, and `submitError` only
|
|
276
|
+
carries what NO field can — a `BusinessRuleViolation` whose rule pinpointed
|
|
277
|
+
a `field` routes the same way. Custom widget kits get the same judgement via
|
|
278
|
+
`fieldIssuesOf(error)` from `@voltro/protocol`.
|
|
279
|
+
|
|
280
|
+
**Async checks gate the submit.** Bind a uniqueness probe to its field and
|
|
281
|
+
`submit` waits for it — an `invalid` verdict blocks with the message on that
|
|
282
|
+
field, an unsettled check fails closed after 5s:
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const email = useAsyncValidation('app', 'users.emailAvailable', values.email ?? '', {
|
|
286
|
+
interpret: (r) => ({ valid: (r as { available: boolean }).available, message: 'Email taken' }),
|
|
287
|
+
})
|
|
288
|
+
const form = useFormBinding('app', 'users.create', { asyncFields: { email } })
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**The binding is typed — `toInput` included.** `createHooks<AppProcedures>('app')`
|
|
292
|
+
returns `useFormBinding` beside the other hooks — tag as a literal type,
|
|
293
|
+
`values`/`defaults` and the submit output inferred from the descriptor. With
|
|
294
|
+
`toInput`, the form gets its OWN `Values` shape and the mapper's return is
|
|
295
|
+
checked against the mutation's input by the compiler — a mapping that stops
|
|
296
|
+
producing the wire shape is a type error, not a runtime refusal.
|
|
297
|
+
|
|
298
|
+
### The full binding — nested values, arrays, timing, one state
|
|
299
|
+
|
|
300
|
+
`useFormBinding` carries a complete form, not just flat fields. Nested structs
|
|
301
|
+
flatten into SECTIONS (`address.city`, grouped under `section('address')`),
|
|
302
|
+
arrays of structs become field arrays, and every field is reachable as a
|
|
303
|
+
bound handle a widget kit spreads onto its input:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
const form = useFormBinding('app', 'employees.update', {
|
|
307
|
+
defaults: fromRow(employee),
|
|
308
|
+
toInput: (values) => ({ id: employee.id, ...employeePatch(values) }),
|
|
309
|
+
errorPath: { fullName: 'name' },
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const city = form.field('address.city') // { value, setValue, onBlur, error, required, label, a11y, … }
|
|
313
|
+
form.array('entries').push({ startsAt: '' })
|
|
314
|
+
form.section('address') // the section's descriptors
|
|
315
|
+
form.state // { isDirty, canSubmit, isSubmitting, isSubmitSuccessful, submissionAttempts, errorCount, pending, … }
|
|
316
|
+
form.reset(nextDefaults) // switch the edited record without a remount
|
|
317
|
+
form.focusFirstInvalid()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Error timing has defaults a form can trust:** a form NEVER opens with
|
|
321
|
+
errors — a field reveals its error after ITS blur or after the first submit
|
|
322
|
+
attempt, then live (`validate: { onChange: 'afterTouched' }`; `'always'` and
|
|
323
|
+
`'never'` exist). `isValid`/`canSubmit` always tell the truth underneath, so
|
|
324
|
+
the save button disables correctly while the user is not yet being scolded.
|
|
325
|
+
|
|
326
|
+
**Values ≠ mutation input:** `toInput` maps form values to the wire input
|
|
327
|
+
BEFORE validation; input-schema issues route back to form fields via
|
|
328
|
+
`errorPath` (same-name fields map automatically). For composed saves,
|
|
329
|
+
`onSubmit: async ({ input, values, mutate }) => …` owns the write and keeps
|
|
330
|
+
optimistic + error routing.
|
|
331
|
+
|
|
332
|
+
**Per-field rendering:** with `subscribe: 'fields'` the binding only
|
|
333
|
+
re-renders on submission-level changes, and each field component subscribes
|
|
334
|
+
narrowly:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
const BoundField = ({ form, path }: { form: FormBinding<Record<string, unknown>, unknown>; path: string }) => {
|
|
338
|
+
const f = useFormField(form, path)
|
|
339
|
+
return <input id={f.a11y.id} value={String(f.value ?? '')} onChange={(e) => f.setValue(e.target.value)} onBlur={f.onBlur} aria-invalid={f.a11y['aria-invalid']} />
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Annotate structure on the schema itself: `formField({ section, order, label,
|
|
344
|
+
widget })` rides an annotation, `description` becomes help text, and
|
|
345
|
+
`Schema.Date` / `Schema.DateTimeUtc` map to date/datetime widgets. The engine
|
|
346
|
+
underneath is an implementation detail — no engine type appears in the public
|
|
347
|
+
API, and production builds stub its devtools channel automatically.
|
|
348
|
+
|
|
349
|
+
### Reference fields, uploads, and testing the form
|
|
350
|
+
|
|
351
|
+
**Reference fields.** Mark a schema field as a table reference and it renders
|
|
352
|
+
as a picker-shaped field whose VALUE stays the id (or id list — which is
|
|
353
|
+
exactly what a target's declared `relations:` consumes):
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
const EmployeesUpdateInput = Schema.Struct({
|
|
357
|
+
id: Schema.String,
|
|
358
|
+
storeId: Schema.String.annotations(formField({ reference: 'stores' })),
|
|
359
|
+
assignedStores: Schema.Array(Schema.String).annotations(formField({ reference: 'stores' })),
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The descriptor carries `widget: 'reference'` + `reference: 'stores'`; bind
|
|
364
|
+
the shipped `<AsyncSelect>` (or your own picker) to it via a render-prop —
|
|
365
|
+
the default registry deliberately renders the render-prop note, because a
|
|
366
|
+
live picker needs a query binding only the app can name.
|
|
367
|
+
|
|
368
|
+
**Uploads as field values.** `useUpload` returns a `fileId`; hold it in a
|
|
369
|
+
field and LINK it in the submit — the composed save is what `onSubmit` is
|
|
370
|
+
for, so the upload that never got attached cannot happen silently:
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
const form = useFormBinding('app', 'tasks.create', {
|
|
374
|
+
onSubmit: async ({ input, values, mutate }) => {
|
|
375
|
+
const row = await mutate(input)
|
|
376
|
+
// attachments: [...existing, ...uploaded fileIds] — linked HERE, not forgotten
|
|
377
|
+
return row
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Leaving a dirty form** is guarded by the router: `useBlocker(form.state.isDirty)`
|
|
383
|
+
holds SPA navigations (Back button included) and arms the native
|
|
384
|
+
`beforeunload` prompt — see the routing docs.
|
|
385
|
+
|
|
386
|
+
**Testing.** `renderFormBinding` (from `@voltro/testing/client`) drives the
|
|
387
|
+
REAL binding against a fake api — fill, blur, submit, read the visible
|
|
388
|
+
errors; a mutation handler that throws `ValidationError({ field })`
|
|
389
|
+
exercises the same routing path a server refusal takes:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const form = await renderFormBinding('users.create', {
|
|
393
|
+
binding: { schema: UsersCreateInput },
|
|
394
|
+
mutation: () => { throw new ValidationError({ field: 'email', message: 'validation.emailTaken' }) },
|
|
395
|
+
})
|
|
396
|
+
await form.submit()
|
|
397
|
+
expect(form.errors()['email']).toBe('validation.emailTaken')
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Runs under jsdom (`// @vitest-environment jsdom`).
|
|
401
|
+
|
|
217
402
|
### Forms without JavaScript
|
|
218
403
|
|
|
219
404
|
On a server-rendered page, `<AutoForm>` works with JavaScript disabled — or not
|
|
@@ -720,7 +905,33 @@ multi-device CRDT (single-user/device, honest about its limits).
|
|
|
720
905
|
|
|
721
906
|
```tsx
|
|
722
907
|
const outbox = useOutbox({ send })
|
|
723
|
-
// → { queue, enqueue, replay, online, pending, conflicts }
|
|
908
|
+
// → { queue, enqueue, replay, online, pending, conflicts, resolveConflict }
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
**Durable across reloads.** Pass `persistence` and the queue survives:
|
|
912
|
+
entries rehydrate on mount, every transition persists (delivered entries are
|
|
913
|
+
compacted away), and a failing backing degrades to the in-memory behaviour
|
|
914
|
+
instead of breaking the outbox. The one real implementation is
|
|
915
|
+
`outboxPersistence()` from `@voltro/local-first`, backed by the same
|
|
916
|
+
`PersistenceAdapter` the sync engine drains — one durable queue per device:
|
|
917
|
+
|
|
918
|
+
```tsx
|
|
919
|
+
import { createIndexedDbPersistence, outboxPersistence } from '@voltro/local-first'
|
|
920
|
+
|
|
921
|
+
const adapter = await createIndexedDbPersistence()
|
|
922
|
+
const outbox = useOutbox({ send, persistence: outboxPersistence(adapter) })
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Resolving a conflict.** A conflicted entry blocks everything behind it
|
|
926
|
+
(causality). `resolveConflict(id, input)` replaces its input with the RESOLVED
|
|
927
|
+
value, returns it to pending and replays — typically computed with
|
|
928
|
+
`resolveWithPolicy()` from `@voltro/local-first`, where `crdtText()` columns
|
|
929
|
+
merge and scalars follow the declared `conflictPolicy()`:
|
|
930
|
+
|
|
931
|
+
```tsx
|
|
932
|
+
outbox.resolveConflict(entry.id, resolveWithPolicy(policy, local, remote, {
|
|
933
|
+
crdtColumns: ['body'],
|
|
934
|
+
}))
|
|
724
935
|
```
|
|
725
936
|
|
|
726
937
|
|
|
@@ -195,7 +195,7 @@ would break every link into your api.
|
|
|
195
195
|
|
|
196
196
|
### Raw WebSocket gateways are guarded before the upgrade
|
|
197
197
|
|
|
198
|
-
A [`defineWebSocket` gateway](/docs/data/subscriptions#raw-websocket-gateways
|
|
198
|
+
A [`defineWebSocket` gateway](/docs/data/subscriptions#raw-websocket-gateways-definewebsocket)
|
|
199
199
|
mounts its own upgrade path beside the rpc socket, and the listener treats it
|
|
200
200
|
exactly like the rpc upgrade:
|
|
201
201
|
|
|
@@ -479,7 +479,7 @@ deferred registry, the shell's bundle tags, and React's own bootstrap/settle
|
|
|
479
479
|
scripts — while the policy header itself stays the middleware's to set via
|
|
480
480
|
`responseHeaders`, carrying the same nonce. An `isr` page combined with
|
|
481
481
|
`cspNonce` refuses the render: a cached nonce is a lie the browser enforces.
|
|
482
|
-
Mechanics + examples: [Middleware → cspNonce](/docs/routing/middleware#a-per-request-csp-nonce
|
|
482
|
+
Mechanics + examples: [Middleware → cspNonce](/docs/routing/middleware#a-per-request-csp-nonce-cspnonce).
|
|
483
483
|
|
|
484
484
|
## Incoming webhooks must verify their caller
|
|
485
485
|
|
|
@@ -582,7 +582,7 @@ dependency, like a rate-limit counter in Redis — catches internally and
|
|
|
582
582
|
degrades **loudly**. `@voltro/plugin-ratelimit`'s `httpShield` does exactly
|
|
583
583
|
that: a Redis outage means unlimited-with-a-warning, never a self-inflicted API
|
|
584
584
|
outage. The full authoring guidance is in the
|
|
585
|
-
[plugin contract](/docs/plugins/contract#onhttprequest-httprequestinterceptor
|
|
585
|
+
[plugin contract](/docs/plugins/contract#onhttprequest-httprequestinterceptor-pre-auth-http-pipeline-hook).
|
|
586
586
|
|
|
587
587
|
## Response compression — and where BREACH sits
|
|
588
588
|
|