@zerotal/arch 1.8.1 → 1.9.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/docs/admin/actions.md +15 -0
- package/docs/admin/auth.md +10 -0
- package/docs/admin/dashboard.md +12 -0
- package/docs/admin/extending-ui.md +14 -0
- package/docs/admin/forms.md +15 -0
- package/docs/admin/operations.md +12 -0
- package/docs/admin/resources.md +6 -0
- package/docs/admin/tables.md +21 -0
- package/docs/audit.md +5 -0
- package/docs/authentication.md +110 -1
- package/docs/broadcasting/references.md +17 -0
- package/docs/cache.md +5 -0
- package/docs/carbon.md +5 -0
- package/docs/changelog.md +125 -0
- package/docs/client/index.md +17 -0
- package/docs/commands.md +6 -0
- package/docs/components.md +73 -0
- package/docs/config-system.md +54 -0
- package/docs/cookies.md +6 -0
- package/docs/deployment.md +103 -13
- package/docs/devtools.md +5 -0
- package/docs/email-verification.md +26 -1
- package/docs/encryption.md +21 -0
- package/docs/errors.md +2 -0
- package/docs/flow/components.md +54 -0
- package/docs/flow/forms.md +57 -0
- package/docs/flow/references.md +14 -0
- package/docs/getting-started.md +38 -0
- package/docs/health.md +19 -0
- package/docs/helpers.md +99 -0
- package/docs/i18n.md +5 -0
- package/docs/inertia/props.md +70 -0
- package/docs/lock.md +15 -0
- package/docs/logger.md +38 -0
- package/docs/migrations.md +47 -0
- package/docs/monitor.md +59 -0
- package/docs/notifications.md +11 -0
- package/docs/orm/casts.md +6 -0
- package/docs/orm/lifecycle.md +18 -0
- package/docs/orm/queries.md +10 -0
- package/docs/orm/relationships.md +30 -0
- package/docs/queue.md +10 -0
- package/docs/rate-limiting.md +39 -4
- package/docs/responses.md +23 -0
- package/docs/routing.md +16 -0
- package/docs/scheduler.md +11 -0
- package/docs/session.md +6 -0
- package/docs/social.md +10 -0
- package/docs/storage.md +21 -0
- package/docs/support-policy.md +13 -1
- package/docs/telemetry.md +8 -0
- package/docs/tenancy.md +6 -0
- package/docs/testing/index.md +6 -0
- package/docs/validator.md +9 -0
- package/docs/view.md +6 -0
- package/package.json +3 -3
package/docs/config-system.md
CHANGED
|
@@ -42,6 +42,45 @@ const apiKey = env("API_KEY"); // no fallback → string | undefined
|
|
|
42
42
|
|
|
43
43
|
> **Note** — When you need a value to be present, use `requireEnv("APP_KEY")` instead — it throws a `ConfigError` at boot if the variable is unset, rather than returning `undefined`.
|
|
44
44
|
|
|
45
|
+
### Declaring the whole environment — `EnvSchema`
|
|
46
|
+
|
|
47
|
+
`env()` is per-call and forgiving: an unset variable is `undefined` and you find out where it is
|
|
48
|
+
used. `@zerotal/core/env` is the other end — declare every variable the app reads, once, and the
|
|
49
|
+
boot either produces a fully typed frozen object or fails with every problem listed at the same
|
|
50
|
+
time:
|
|
51
|
+
|
|
52
|
+
```typescript fragment
|
|
53
|
+
// env.ts
|
|
54
|
+
import { EnvSchema, t } from "@zerotal/core/env";
|
|
55
|
+
|
|
56
|
+
export const env = EnvSchema.define({
|
|
57
|
+
APP_KEY: t.string().min(32),
|
|
58
|
+
PORT: t.port().default(3000),
|
|
59
|
+
DATABASE_URL: t.string(),
|
|
60
|
+
LOG_LEVEL: t.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
61
|
+
SENTRY_DSN: t.url().optional(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
env.PORT; // number — never undefined, because it has a default
|
|
65
|
+
env.LOG_LEVEL; // "debug" | "info" | "warn" | "error", narrowed to the literals
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**It reports every failure at once.** A schema with three missing variables fails the boot
|
|
69
|
+
naming all three, rather than one per restart — which is the difference between one fix and
|
|
70
|
+
three round trips through a deploy. The failure is an `EnvSchemaError` carrying an
|
|
71
|
+
`EnvFieldError` per field.
|
|
72
|
+
|
|
73
|
+
| Type | What it is |
|
|
74
|
+
| ---------------- | --------------------------------------------------------------------------- |
|
|
75
|
+
| `EnvSchemaError` | The boot failure, listing every field that did not validate. |
|
|
76
|
+
| `EnvFieldError` | One field's problem: which variable, and what was wrong with it. |
|
|
77
|
+
| `FieldType` | The builders `t` offers — string, number, boolean, port, url, enum. |
|
|
78
|
+
| `EnvOutput<S>` | The typed object a schema produces. `typeof env` where you need to pass it. |
|
|
79
|
+
| `InferDef<D>` | The type one field definition resolves to. |
|
|
80
|
+
|
|
81
|
+
Use `env()` for a value read in one place and `EnvSchema` for the set an app cannot start
|
|
82
|
+
without. They coexist; the schema is not a replacement for the helper.
|
|
83
|
+
|
|
45
84
|
## Config files
|
|
46
85
|
|
|
47
86
|
Config files live in `config/`. Each file exports a typed object via a package helper:
|
|
@@ -250,6 +289,21 @@ if (config("app.env") === "production") {
|
|
|
250
289
|
| `ConfigLoader.get` | `get(key: string, fallback?): value` | Dot-path read against the loaded map. |
|
|
251
290
|
| `ConfigLoader.validate` | `validate(): this` | Run each file's optional `validate(config)` export, throwing on failure. |
|
|
252
291
|
|
|
292
|
+
## Types
|
|
293
|
+
|
|
294
|
+
| Type | What it is |
|
|
295
|
+
| ------------------- | -------------------------------------------------------------------------------------- |
|
|
296
|
+
| `ConfigValidator` | What `registerConfigValidator` takes — a function handed the config, reporting issues. |
|
|
297
|
+
| `ConfigIssue` | One finding: its namespace, message, and level. |
|
|
298
|
+
| `ConfigIssueLevel` | Whether an issue refuses a production boot or is only worth reading. |
|
|
299
|
+
| `ConventionsConfig` | The `conventions` namespace — where the framework looks for models, jobs and the rest. |
|
|
300
|
+
| `AppTlsConfig` | TLS settings under `app.tls`. |
|
|
301
|
+
| `AssetLoaderKind` | How an asset is loaded by the build — the `loader` values `assets.loaders` accepts. |
|
|
302
|
+
|
|
303
|
+
A validator reporting a **fatal** issue refuses a production-like boot rather than warning. That
|
|
304
|
+
is the whole point of the level: an app that boots with a broken configuration serves wrong
|
|
305
|
+
answers rather than failing, and the failure is the cheaper outcome.
|
|
306
|
+
|
|
253
307
|
## Next steps
|
|
254
308
|
|
|
255
309
|
- [Conventions](/docs/conventions) — the auto-discovery settings under the `conventions` key.
|
package/docs/cookies.md
CHANGED
|
@@ -93,6 +93,12 @@ You rarely set cookies by hand — two parts of the framework manage their own:
|
|
|
93
93
|
(non-`HttpOnly`) cookie after every request so Axios/Inertia can echo it back as
|
|
94
94
|
the `X-XSRF-TOKEN` header.
|
|
95
95
|
|
|
96
|
+
## Types
|
|
97
|
+
|
|
98
|
+
`CookieOptions` is what every cookie-setting call accepts — `maxAge`, `path`, `domain`,
|
|
99
|
+
`httpOnly`, `secure`, `sameSite`. It is exported so a helper that sets a cookie in more than one
|
|
100
|
+
place can take the same shape.
|
|
101
|
+
|
|
96
102
|
## Next steps
|
|
97
103
|
|
|
98
104
|
- [Session](/docs/session) — signed, `HttpOnly` cookie-backed state (the usual choice).
|
package/docs/deployment.md
CHANGED
|
@@ -63,18 +63,45 @@ the wrong database.
|
|
|
63
63
|
|
|
64
64
|
Each entry is a `DeployTarget`:
|
|
65
65
|
|
|
66
|
-
| Field
|
|
67
|
-
|
|
|
68
|
-
| `url`
|
|
69
|
-
| `steps`
|
|
66
|
+
| Field | Meaning |
|
|
67
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `url` | The public URL in this environment. What `--probe` handshakes against when given no URL of its own. |
|
|
69
|
+
| `steps` | Override the release steps. Defaults to `DEFAULT_DEPLOY_STEPS` — `assets:build`, `inertia:build`, `migrate`. Each names a `zt` command, and one that is not registered is skipped, so an app without Inertia simply has no Inertia step. |
|
|
70
|
+
| `preflight` | Your own commands, run in the preflight phase — after the config validators and `doctor`, before anything is built or migrated. A non-zero exit refuses the release. Defaults to `release:check` when the app registers a command by that name. |
|
|
70
71
|
|
|
71
72
|
Omit the file entirely and you get `DEFAULT_DEPLOY_TARGETS`: `production` and
|
|
72
73
|
`staging`, both with the default steps.
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
### Your own release gate
|
|
76
|
+
|
|
77
|
+
The framework's preflight knows the things a framework can know: that this really is the
|
|
78
|
+
environment you think it is, that the config validators pass, that `doctor` is happy. It
|
|
79
|
+
cannot know that this workspace has no cancellation policy, that mail is still wired to the
|
|
80
|
+
`log` driver so nothing is ever sent, or that the owner account is still on the password
|
|
81
|
+
`admin:create` issued it. Those refusals are yours.
|
|
82
|
+
|
|
83
|
+
Write them as a command and name it `release:check` (exported as
|
|
84
|
+
`CONVENTIONAL_PREFLIGHT_COMMAND`). The pipeline finds it by name — nothing to wire up:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// app/commands/ReleaseCheckCommand.ts
|
|
88
|
+
import { Command } from "zerotal";
|
|
89
|
+
|
|
90
|
+
export class ReleaseCheckCommand extends Command {
|
|
91
|
+
static commandName = "release:check";
|
|
92
|
+
static description = "Refuse a release this app is not ready for";
|
|
93
|
+
static needsApp = true;
|
|
94
|
+
|
|
95
|
+
async run(): Promise<void> {
|
|
96
|
+
const problems: string[] = [];
|
|
97
|
+
if (Bun.env["APP_KEY"] === "base64:CHANGE_ME") problems.push("APP_KEY is the example one.");
|
|
98
|
+
if (problems.length > 0) throw new Error(problems.join(" "));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A throw, or any non-zero exit, stops the release before `assets:build` has run. To run
|
|
104
|
+
something else — or more than one thing — name them:
|
|
78
105
|
|
|
79
106
|
```ts
|
|
80
107
|
// config/deploy.ts
|
|
@@ -82,14 +109,16 @@ export default {
|
|
|
82
109
|
targets: {
|
|
83
110
|
production: {
|
|
84
111
|
url: "https://example.com",
|
|
85
|
-
|
|
112
|
+
preflight: ["release:check", "smoke:mail"],
|
|
86
113
|
},
|
|
87
114
|
},
|
|
88
115
|
};
|
|
89
116
|
```
|
|
90
117
|
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
A name in `preflight` that is not a registered command **fails the deploy** rather than being
|
|
119
|
+
skipped. That is the opposite of how `steps` treats an absent command, and deliberately so: a
|
|
120
|
+
missing `inertia:build` means the app has no Inertia, while a missing gate means the gate is
|
|
121
|
+
not running — which is the state this exists to prevent. A gate nothing calls is a comment.
|
|
93
122
|
|
|
94
123
|
Two things worth adding while you are there: `assets:build` and `inertia:build` accept
|
|
95
124
|
`--clean`, which removes anything in the output directory the build did not write — see
|
|
@@ -180,6 +209,54 @@ bun zt migrate
|
|
|
180
209
|
Auto-`synchronize` is **hard-off in production** — generate and commit
|
|
181
210
|
[migrations](/docs/migrations) during development and run them on deploy.
|
|
182
211
|
|
|
212
|
+
## Back up the database
|
|
213
|
+
|
|
214
|
+
On SQLite the database is one file, which makes `cp` look like a backup. It is not one. A
|
|
215
|
+
live SQLite database has pages in flight; copying the file while the server is serving can
|
|
216
|
+
capture a half-written page, and the result is a file that sits in your retention directory
|
|
217
|
+
for months and turns out to be corrupt on the one morning you open it.
|
|
218
|
+
|
|
219
|
+
`zt db:backup` uses SQLite's own `VACUUM INTO`, which takes a read lock and writes a
|
|
220
|
+
complete database while the server keeps serving. It needs no `sqlite3` binary on the box:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# in your project root
|
|
224
|
+
bun zt db:backup --dir=/var/backups/app --keep=30 --require-rows=bookings,invoices
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| Flag | What it does |
|
|
228
|
+
| ---------------- | ---------------------------------------------------------------------------- |
|
|
229
|
+
| `--dir` | Where snapshots go. Default `storage/backups`. |
|
|
230
|
+
| `--keep` | How many to keep, newest first. `0` keeps every one. Default `14`. |
|
|
231
|
+
| `--require-rows` | Tables that must not be empty in the snapshot. **Set this.** |
|
|
232
|
+
| `--rehearse` | Also perform the restore — copy the snapshot, open the copy, check it there. |
|
|
233
|
+
|
|
234
|
+
Every snapshot is opened and integrity-checked the moment it is written, and **every failure
|
|
235
|
+
path exits non-zero**. That is what makes it safe to run from a timer: a bad night leaves a
|
|
236
|
+
failed unit somebody can see, rather than a green one and no file.
|
|
237
|
+
|
|
238
|
+
`--require-rows` is the flag that turns "a file was written" into "the file has the business
|
|
239
|
+
in it". An empty `bookings` table in a snapshot of a live system is not a small discrepancy,
|
|
240
|
+
and it is invisible in a byte count.
|
|
241
|
+
|
|
242
|
+
Run `--rehearse` on a schedule of its own — weekly is plenty. A backup nobody has restored is
|
|
243
|
+
a hope, and the restore is the operation you will be doing at 3am.
|
|
244
|
+
|
|
245
|
+
```ini
|
|
246
|
+
# /etc/systemd/system/app-backup.service
|
|
247
|
+
[Service]
|
|
248
|
+
Type=oneshot
|
|
249
|
+
WorkingDirectory=/srv/app
|
|
250
|
+
Environment=APP_ENV=production
|
|
251
|
+
ExecStart=/srv/app/node_modules/.bin/bun zt db:backup --keep=30 --require-rows=bookings
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Pair it with a `.timer`, and let the failed unit be your alert. Retention is handled by
|
|
255
|
+
`--keep`; anything the command did not write is never touched.
|
|
256
|
+
|
|
257
|
+
On PostgreSQL or MySQL the command refuses and points at `pg_dump` / `mysqldump`, which is
|
|
258
|
+
where that job belongs.
|
|
259
|
+
|
|
183
260
|
## Build assets
|
|
184
261
|
|
|
185
262
|
Build the frontend bundle as a release step, so `public/` holds compiled output before
|
|
@@ -350,6 +427,11 @@ The symptom is a legitimate visitor getting a 429 they did not earn.
|
|
|
350
427
|
Count the proxies you actually run. Setting `trustedProxies: 3` with one proxy in front
|
|
351
428
|
reads an entry the client supplied.
|
|
352
429
|
|
|
430
|
+
`zt doctor` warns about this: a production-like deployment with a registered throttle and no
|
|
431
|
+
`trustedProxies` is reported as almost certainly wrong. It is a warning rather than a failure
|
|
432
|
+
because the framework cannot see your deployment — an app served directly, with nothing in
|
|
433
|
+
front of it, is correctly configured exactly as it stands.
|
|
434
|
+
|
|
353
435
|
### Never gate the transport path
|
|
354
436
|
|
|
355
437
|
**Browsers do not attach basic-auth credentials to a WebSocket handshake.** An HTTP auth
|
|
@@ -513,8 +595,9 @@ Run the worker as a **second** container/service from the same image with the co
|
|
|
513
595
|
`bun zt worker`.
|
|
514
596
|
|
|
515
597
|
On a server with no Node installed, `bun install` can fail on a transitive package whose
|
|
516
|
-
`postinstall` shells out to `node`.
|
|
517
|
-
|
|
598
|
+
`postinstall` shells out to `node`. The `bun` npm package is the usual one — its
|
|
599
|
+
`postinstall` downloads a Bun binary the box already has, and there is no `node` to run the
|
|
600
|
+
script with. `--ignore-scripts` resolves it, but check what you are skipping first:
|
|
518
601
|
|
|
519
602
|
```bash
|
|
520
603
|
# every package with an install script, before you skip them all
|
|
@@ -523,6 +606,13 @@ for p in node_modules/*/package.json node_modules/@*/*/package.json; do
|
|
|
523
606
|
done
|
|
524
607
|
```
|
|
525
608
|
|
|
609
|
+
### `startZerotal` options
|
|
610
|
+
|
|
611
|
+
`StartZerotalOptions` is what `zt.ts` may pass — currently `configDir`, for an app whose config
|
|
612
|
+
does not live at `./config`. `isDevSurfaceAllowed(env)` is the check every dev-only surface
|
|
613
|
+
gates on, exported so an app's own dev tooling can gate the same way; it **fails closed**, so an
|
|
614
|
+
unset `APP_ENV` does not qualify.
|
|
615
|
+
|
|
526
616
|
## Next steps
|
|
527
617
|
|
|
528
618
|
- [Configuration](/docs/config-system) — environment variables and config files.
|
package/docs/devtools.md
CHANGED
|
@@ -1004,6 +1004,11 @@ meaningfully different from `{}` — a node can be both a branch and a leaf.
|
|
|
1004
1004
|
unfiltered list plus whether it heads a group (`groupKey`, `groupSize`) or is a
|
|
1005
1005
|
folded follow-up (`child`).
|
|
1006
1006
|
|
|
1007
|
+
## Types
|
|
1008
|
+
|
|
1009
|
+
`TraceStoreOptions` sets how many traces are kept and for how long.
|
|
1010
|
+
`DevtoolsClientOptions` configures the browser side.
|
|
1011
|
+
|
|
1007
1012
|
## Next steps
|
|
1008
1013
|
|
|
1009
1014
|
- [Logger](/docs/logger) — structured logging that surfaces in the Logs tab.
|
|
@@ -49,12 +49,29 @@ register ──▶ Url.sign(/auth/verify?id&email, ttl) ──▶ email link
|
|
|
49
49
|
|
|
50
50
|
## Migration
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Whether you need one **depends on what your schema's source of truth is** — `zt doctor`
|
|
53
|
+
reports which mode this app is in.
|
|
54
|
+
|
|
55
|
+
- **Models are the source of truth** (`synchronize` on). You do not need a migration. The
|
|
56
|
+
mixin registers `email_verified_at` imperatively and the table is built from what the
|
|
57
|
+
models declare, so the column comes with it.
|
|
58
|
+
- **Migrations are the source of truth** (`synchronize` off — the production default). You
|
|
59
|
+
do. The boot-time concern only _adds_ a column to a table that already exists; it never
|
|
60
|
+
revisits one it has seen. A `create users` migration that does not mention
|
|
61
|
+
`email_verified_at` produces a table without it, and then every query touching the column
|
|
62
|
+
fails with `no such column: email_verified_at` — all at once, in tests that have nothing to
|
|
63
|
+
do with email, with nothing pointing at the mixin.
|
|
64
|
+
|
|
65
|
+
`zt doctor` fails on that combination and names the table, so you find it before the suite
|
|
66
|
+
does.
|
|
67
|
+
|
|
68
|
+
Write the migration **guarded**:
|
|
53
69
|
|
|
54
70
|
```typescript fragment
|
|
55
71
|
// database/migrations/002_add_email_verified_at.ts
|
|
56
72
|
export default class AddEmailVerifiedAt extends Migration {
|
|
57
73
|
async up(schema: Schema) {
|
|
74
|
+
if (await schema.hasColumn("users", "email_verified_at")) return;
|
|
58
75
|
await schema.table("users", (table) => {
|
|
59
76
|
table.timestamp("email_verified_at").nullable();
|
|
60
77
|
});
|
|
@@ -67,6 +84,14 @@ export default class AddEmailVerifiedAt extends Migration {
|
|
|
67
84
|
}
|
|
68
85
|
```
|
|
69
86
|
|
|
87
|
+
The guard is not defensive habit. Any database that has booted the app since the mixin was
|
|
88
|
+
composed already **has** the column — the concern added it. An unguarded migration then fails
|
|
89
|
+
with `duplicate column name`, during the `migrate` step of `deploy:production`, which is the
|
|
90
|
+
worst possible moment to learn this.
|
|
91
|
+
|
|
92
|
+
`remember_token` from `Authenticatable` is provisioned the same way and carries the same
|
|
93
|
+
condition.
|
|
94
|
+
|
|
70
95
|
## User model
|
|
71
96
|
|
|
72
97
|
Expose the column and a convenience getter the middleware and controller can read:
|
package/docs/encryption.md
CHANGED
|
@@ -373,6 +373,27 @@ it works only after the app has booted. In a unit test that never boots, constru
|
|
|
373
373
|
| `Url.sign(base, params?, min?, secret?)` | `(base: string, params?, expiresInMinutes?, secret?) => string` | Build an HMAC-signed, time-limited URL. |
|
|
374
374
|
| `Url.verify(url, secret?)` | `(signedUrl: string, secret?: string) => boolean` | Verify a signed URL; `false` if tampered or expired. |
|
|
375
375
|
|
|
376
|
+
## Hashing helpers
|
|
377
|
+
|
|
378
|
+
Three pure helpers sit beside `Crypt`, for the cases that are not encryption:
|
|
379
|
+
|
|
380
|
+
| Helper | Use |
|
|
381
|
+
| --------------------- | ---------------------------------------------------------------------- |
|
|
382
|
+
| `sha256Hex(value)` | A hex digest. For a lookup key or a blind index, never for a password. |
|
|
383
|
+
| `hmacHex(value, key)` | A keyed digest — a webhook signature, a tamper-evident token. |
|
|
384
|
+
| `safeEqual(a, b)` | Constant-time comparison. Use it for every secret comparison. |
|
|
385
|
+
|
|
386
|
+
**`safeEqual` is the one that matters.** `a === b` on a token returns as soon as two bytes
|
|
387
|
+
differ, and the time it took is a measurement of how much of the prefix was right — enough, over
|
|
388
|
+
many attempts, to recover a secret a character at a time. Comparing anything an attacker
|
|
389
|
+
supplies against anything you hold goes through `safeEqual`.
|
|
390
|
+
|
|
391
|
+
### Signed URLs
|
|
392
|
+
|
|
393
|
+
`URLSigner` is what `signedUrl()` and `ValidateSignatureMiddleware` are built on. Reach for it
|
|
394
|
+
directly when you need to sign or verify outside a request — a link minted by a scheduled job,
|
|
395
|
+
or a signature checked by a worker.
|
|
396
|
+
|
|
376
397
|
## Next steps
|
|
377
398
|
|
|
378
399
|
- [Authentication](/docs/authentication) — where hashed passwords are verified at login.
|
package/docs/errors.md
CHANGED
|
@@ -273,6 +273,8 @@ So a package that owns an error class can contribute a **diagnosis**, rendered a
|
|
|
273
273
|
|
|
274
274
|
It works on SQLite, PostgreSQL and MySQL — matched on the driver's error code where there is one (`42P01`, `42703`, `1146`, `1054`) and on the message otherwise.
|
|
275
275
|
|
|
276
|
+
`bun zt doctor` asks the same question before anything breaks, and warns when migrations are pending — see [Migrations](/docs/migrations#running-migrations). The overlay is reactive by nature: it needs a request to have already failed, which means somebody has already lost the thread of what they were doing.
|
|
277
|
+
|
|
276
278
|
> **The button exists only in development.** The endpoint behind it refuses unless [`devSurfacesEnabled()`](/docs/deployment) is true, and that gate **fails closed**: an unset `APP_ENV` does not qualify. The route is not even registered otherwise.
|
|
277
279
|
>
|
|
278
280
|
> It also requires a single-use token minted into the page, and passes the same origin check the WebSocket endpoints use. A dev server on `localhost:3000` is reachable by any site you have open in another tab, and "run every pending migration" is not something a random page should be able to trigger.
|
package/docs/flow/components.md
CHANGED
|
@@ -859,6 +859,60 @@ Content-Security-Policy:
|
|
|
859
859
|
}
|
|
860
860
|
```
|
|
861
861
|
|
|
862
|
+
## Types
|
|
863
|
+
|
|
864
|
+
Every built-in component exports the type of its own props, named after it. Reach for one when
|
|
865
|
+
you wrap a component rather than use it directly — the usual reason an app needs the type at all:
|
|
866
|
+
|
|
867
|
+
```tsx
|
|
868
|
+
import { Modal, type ModalProps } from "@zerotal/flow";
|
|
869
|
+
|
|
870
|
+
/** Our confirm dialog: the same API, one decision already made. */
|
|
871
|
+
export function ConfirmModal(props: Omit<ModalProps, "closeable">) {
|
|
872
|
+
return <Modal closeable={false} {...props} />;
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
```text
|
|
877
|
+
DescriptionProps DisclosureProps DrawerProps DropdownProps
|
|
878
|
+
ErrorBoundaryProps ErrorProps ErrorsProps
|
|
879
|
+
FieldsetProps FileUploadProps FlashProps ForProps
|
|
880
|
+
InfiniteScrollProps LegendProps LinkProps
|
|
881
|
+
ListboxProps LoadingProps ModalProps PagerProps
|
|
882
|
+
SectionContentProps SectionOutletProps
|
|
883
|
+
VirtualizeProps
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
`ListboxOption` is one entry in a `<Listbox>`, and `UploadRef` is what `<FileUpload>` hands back
|
|
887
|
+
for a file the server has accepted.
|
|
888
|
+
|
|
889
|
+
### The flash API
|
|
890
|
+
|
|
891
|
+
`this.flash(…)` is the short form. The builder underneath it is exported, and so are the shapes
|
|
892
|
+
it produces — worth having when a helper composes a message rather than writing one inline:
|
|
893
|
+
|
|
894
|
+
| Type | What it is |
|
|
895
|
+
| -------------------- | ------------------------------------------------------------------------ |
|
|
896
|
+
| `FlashLevel` | `"info" \| "success" \| "warning" \| "error"` — what colours the toast. |
|
|
897
|
+
| `FlashMessage` | One flash: its level, text, and any actions. |
|
|
898
|
+
| `FlashOptions` | Per-message overrides — duration, position, whether it can be dismissed. |
|
|
899
|
+
| `FlashBuilder` | The fluent form, for a message assembled in more than one step. |
|
|
900
|
+
| `FlashPosition` | Where the container puts it, matching `<Flash position>`. |
|
|
901
|
+
| `FlashAction` | A button on the toast: its label and what it does. |
|
|
902
|
+
| `FlashActionStyle` | How that button is drawn. |
|
|
903
|
+
| `FlashActionVariant` | Its emphasis. |
|
|
904
|
+
| `FlashCallback` | What runs when the action is pressed. |
|
|
905
|
+
| `RedirectFlash` | A flash that survives a redirect, so it appears on the page you land on. |
|
|
906
|
+
|
|
907
|
+
`ErrorField` and `ValidationRules` are the validation side of the same story: what one field's
|
|
908
|
+
errors look like, and the rules a component declares.
|
|
909
|
+
|
|
910
|
+
### Elsewhere
|
|
911
|
+
|
|
912
|
+
`DurableOption` configures `@durable` state, `SessionOptions` the session a component reads, and
|
|
913
|
+
`UrlOptions` how `@url` state is written into the query string. `EventName` is the union of
|
|
914
|
+
events a component can listen for.
|
|
915
|
+
|
|
862
916
|
## Next steps
|
|
863
917
|
|
|
864
918
|
- [Flow overview](/docs/flow) — the guide's front page and the rest of the sections.
|
package/docs/flow/forms.md
CHANGED
|
@@ -545,6 +545,63 @@ export class AvatarUploader extends Component {
|
|
|
545
545
|
dedicated `temp` disk are natural follow-ups; the `TemporaryUploadedFile` API is designed to
|
|
546
546
|
absorb them without changing component code.
|
|
547
547
|
|
|
548
|
+
## Enhancing a plain form — no component
|
|
549
|
+
|
|
550
|
+
Everything above needs a Flow component. This does not.
|
|
551
|
+
|
|
552
|
+
A page that is just server-rendered HTML — no `Router.flow`, no `Component` — can still have a
|
|
553
|
+
form that submits without the page flashing. Add `data-enhance`:
|
|
554
|
+
|
|
555
|
+
```html
|
|
556
|
+
<form method="post" action="/subscribe" data-enhance>
|
|
557
|
+
<p class="error">{{ error }}</p>
|
|
558
|
+
<input name="email" />
|
|
559
|
+
<button type="submit">Subscribe</button>
|
|
560
|
+
</form>
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
The form posts through `fetch`, and **the matching form in the response replaces this one in
|
|
564
|
+
place**. A validation error re-renders the form with the message in it, and that is what lands
|
|
565
|
+
on the page — the rest of the document is untouched, so nothing scrolls, nothing flashes, and
|
|
566
|
+
what the person typed is still there.
|
|
567
|
+
|
|
568
|
+
### Include the script
|
|
569
|
+
|
|
570
|
+
The enhancement is a separate, dependency-free bundle. Flow pages get
|
|
571
|
+
`/__flow/runtime.js`; a plain page gets nothing, which is the whole reason this exists. Put the
|
|
572
|
+
tag in the layout that renders your non-Flow pages:
|
|
573
|
+
|
|
574
|
+
```tsx
|
|
575
|
+
import { flowEnhanceTag } from "@zerotal/flow";
|
|
576
|
+
|
|
577
|
+
// in your layout's <head>
|
|
578
|
+
flowEnhanceTag(); // <script src="/__flow/enhance.js" defer></script>
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Nothing fails if you forget it — the forms simply post the way they always did. That is the
|
|
582
|
+
design, and it is also why the tag is easy to leave out and never notice.
|
|
583
|
+
|
|
584
|
+
### What it does
|
|
585
|
+
|
|
586
|
+
| Situation | What happens |
|
|
587
|
+
| ---------------- | ------------------------------------------------------------------------------- |
|
|
588
|
+
| Normal submit | The response's matching form replaces this one; focus and caret are restored. |
|
|
589
|
+
| Validation error | Same — the server re-rendered the form, so the error is in the markup. |
|
|
590
|
+
| Redirect | Followed, the document swapped, and `pushState`d so the address bar agrees. |
|
|
591
|
+
| Network failure | Falls back to a native submit, so nothing typed is lost. |
|
|
592
|
+
| No JavaScript | An ordinary form post. `data-enhance` is additive; there is no fallback to rot. |
|
|
593
|
+
|
|
594
|
+
`data-enhance-target="#selector"` replaces something else instead of the form — a results
|
|
595
|
+
panel, a list. `data-enhance="false"` opts a form out. While a submission is in flight the form
|
|
596
|
+
carries `data-enhance-busy`, which is a styling hook and a re-entry guard.
|
|
597
|
+
|
|
598
|
+
A `flow:enhanced` event fires on `window` after each swap, with
|
|
599
|
+
`detail.navigated` saying whether it followed a redirect.
|
|
600
|
+
|
|
601
|
+
> **Note** — This is enhancement, not a component. There is no state, no socket and no server
|
|
602
|
+
> round-trip beyond the form's own post. When the page needs live state, reach for a
|
|
603
|
+
> [component](/docs/flow/components).
|
|
604
|
+
|
|
548
605
|
## Next steps
|
|
549
606
|
|
|
550
607
|
- [Flow overview](/docs/flow) — the guide's front page and the rest of the sections.
|
package/docs/flow/references.md
CHANGED
|
@@ -216,6 +216,20 @@ override async render() {
|
|
|
216
216
|
| `x-show="$flow.open && $flow.count > 0"` | Raw Alpine expression |
|
|
217
217
|
| `flow:click="increment"` | Hand-written Flow directive (accepted but not needed in JSX) |
|
|
218
218
|
|
|
219
|
+
## Plain-form enhancement
|
|
220
|
+
|
|
221
|
+
For forms on pages with no Flow component. See
|
|
222
|
+
[Forms](/docs/flow/forms#enhancing-a-plain-form--no-component).
|
|
223
|
+
|
|
224
|
+
| Attribute / export | Meaning |
|
|
225
|
+
| --------------------- | -------------------------------------------------------------------------------------- |
|
|
226
|
+
| `data-enhance` | Submit through `fetch` and patch the response in. `"false"` opts out. |
|
|
227
|
+
| `data-enhance-target` | CSS selector for what to replace, instead of the form itself. |
|
|
228
|
+
| `data-enhance-busy` | Set on the form while a submission is in flight. Styling hook and re-entry guard. |
|
|
229
|
+
| `flowEnhanceTag()` | The `<script>` tag to put in a non-Flow layout. |
|
|
230
|
+
| `FLOW_ENHANCE_PATH` | `/__flow/enhance.js` — the path the bundle is served at. |
|
|
231
|
+
| `flow:enhanced` | Window event after each swap; `detail.navigated` says whether a redirect was followed. |
|
|
232
|
+
|
|
219
233
|
## Client expressions
|
|
220
234
|
|
|
221
235
|
Inside a client expression — `onClick={() => this.X(...)}` — `this.` resolves to the live client runtime (no server round-trip to start it). You write the **same names as on the server** — no `$`-prefixed syntax, and it all type-checks.
|
package/docs/getting-started.md
CHANGED
|
@@ -16,6 +16,44 @@ provider.
|
|
|
16
16
|
is configured for **SQLite**, which needs nothing installed — PostgreSQL and
|
|
17
17
|
MySQL are supported and are a `DATABASE_URL` away.
|
|
18
18
|
|
|
19
|
+
### One project, one Bun
|
|
20
|
+
|
|
21
|
+
`engines.bun` is a floor, not a lock, and nothing in npm enforces it. A project can end up
|
|
22
|
+
with two runtimes in it without anyone choosing that: the shell's `bun`, and a different one
|
|
23
|
+
in `node_modules/bun`, put there by a transitive peer dependency nobody declared. The work
|
|
24
|
+
then splits between them — the server served by one, the suite run by the other — and nothing
|
|
25
|
+
says so.
|
|
26
|
+
|
|
27
|
+
That is expensive because the difference is real but narrow. The SQLite bindings, `node:`
|
|
28
|
+
compatibility and the test runner itself all change between releases, so a couple of
|
|
29
|
+
assertions happen to be runtime-sensitive and the rest are not. When those two fail you go
|
|
30
|
+
looking for a bug in the code they touch. And a suite that passes is not evidence either: it
|
|
31
|
+
only means no test happened to stand on a difference.
|
|
32
|
+
|
|
33
|
+
So **`zt` refuses to run when the two disagree**:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Two Bun runtimes are in play. This process is Bun 1.3.14, but the project
|
|
37
|
+
installs Bun 1.4.0 (/srv/app/node_modules/bun/package.json).
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is not a pin. The version to agree on is whichever one the project installed, so
|
|
41
|
+
`bun update bun` moves it and nothing needs editing — what is enforced is that there is only
|
|
42
|
+
one. Fix it either way:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun update bun # move the installed one to match your shell
|
|
46
|
+
node_modules/.bin/bun # or run everything through the installed one
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Most projects never see this, because most have no `bun` in `node_modules` to disagree with.
|
|
50
|
+
To boot anyway — mid-upgrade, say — set `ZT_ALLOW_RUNTIME_MISMATCH=1`, which downgrades the
|
|
51
|
+
refusal to a warning. The refusal is a `RuntimeMismatchError`, which carries both versions in
|
|
52
|
+
its `context` for anything scripting around it.
|
|
53
|
+
|
|
54
|
+
`zt test` spawns the binary running it, not whatever `bun` resolves to on `PATH`, so the
|
|
55
|
+
suite runs on the runtime the guard just checked.
|
|
56
|
+
|
|
19
57
|
## Create a new project
|
|
20
58
|
|
|
21
59
|
```bash
|
package/docs/health.md
CHANGED
|
@@ -271,6 +271,25 @@ What a check returns to describe its own state. A check may also return nothing
|
|
|
271
271
|
| `message` | `string` | Optional human-readable detail. |
|
|
272
272
|
| `meta` | `Record<string, unknown>` | Optional structured metadata surfaced in the report. |
|
|
273
273
|
|
|
274
|
+
### Types
|
|
275
|
+
|
|
276
|
+
| Type | What it is |
|
|
277
|
+
| ---------------------- | --------------------------------------------------------------------------- |
|
|
278
|
+
| `HealthStatus` | `"ok" \| "degraded" \| "down"` — what one check, and the aggregate, report. |
|
|
279
|
+
| `HealthCheckReport` | One check's result: its name, status, duration, and any message. |
|
|
280
|
+
| `HealthConfigShape` | What `HealthConfig()` accepts. |
|
|
281
|
+
| `ResolvedHealthConfig` | The same after defaults are filled in — what the endpoint actually reads. |
|
|
282
|
+
|
|
283
|
+
### The doctor's own types
|
|
284
|
+
|
|
285
|
+
`zt doctor` is extensible: a provider contributes checks through `doctorChecks()`, and
|
|
286
|
+
`runDoctor(app)` runs them all. A check returns a `DoctorCheckResult` — `ok`, `warn` or `fail`,
|
|
287
|
+
with a message and the `fix` printed under it — and the report is `DoctorReportEntry[]`, each
|
|
288
|
+
pairing a check with what it found.
|
|
289
|
+
|
|
290
|
+
A `fail` refuses a deploy; a `warn` does not. Choose deliberately: a check that warns about
|
|
291
|
+
something fatal is ignored, and one that fails on something conditional gets worked around.
|
|
292
|
+
|
|
274
293
|
## Next steps
|
|
275
294
|
|
|
276
295
|
- [Telemetry](/docs/telemetry) — collect the metrics behind your readiness checks.
|