@zerotal/arch 1.7.4 → 1.7.5

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/README.md +4 -2
  3. package/docs/about.md +16 -16
  4. package/docs/admin/actions.md +9 -9
  5. package/docs/admin/auth.md +2 -2
  6. package/docs/admin/dashboard.md +5 -5
  7. package/docs/admin/extending-ui.md +12 -12
  8. package/docs/admin/extending.md +16 -7
  9. package/docs/admin/forms.md +4 -4
  10. package/docs/admin/index.md +2 -2
  11. package/docs/admin/operations.md +9 -9
  12. package/docs/admin/resources.md +3 -3
  13. package/docs/admin/structure.md +6 -6
  14. package/docs/admin/tables.md +8 -8
  15. package/docs/admin/testing.md +4 -4
  16. package/docs/ai.md +118 -11
  17. package/docs/application.md +12 -12
  18. package/docs/arch.md +25 -3
  19. package/docs/assets.md +11 -11
  20. package/docs/audit.md +13 -13
  21. package/docs/authentication.md +39 -39
  22. package/docs/authorization.md +13 -13
  23. package/docs/broadcasting/channels.md +2 -2
  24. package/docs/broadcasting/client.md +2 -2
  25. package/docs/broadcasting/events.md +9 -10
  26. package/docs/broadcasting/index.md +1 -1
  27. package/docs/broadcasting/testing.md +5 -5
  28. package/docs/cache.md +15 -13
  29. package/docs/carbon.md +35 -35
  30. package/docs/changelog.md +105 -1
  31. package/docs/client/index.md +13 -13
  32. package/docs/commands.md +4 -4
  33. package/docs/components.md +116 -115
  34. package/docs/config-system.md +4 -4
  35. package/docs/container.md +27 -27
  36. package/docs/context.md +27 -27
  37. package/docs/contributing.md +28 -0
  38. package/docs/controllers.md +10 -10
  39. package/docs/conventions.md +23 -15
  40. package/docs/cookies.md +3 -3
  41. package/docs/csrf.md +6 -6
  42. package/docs/database.md +22 -17
  43. package/docs/deployment.md +2 -2
  44. package/docs/devtools.md +16 -16
  45. package/docs/email-verification.md +8 -8
  46. package/docs/encryption.md +7 -7
  47. package/docs/errors.md +6 -6
  48. package/docs/events.md +12 -11
  49. package/docs/flow/components.md +32 -32
  50. package/docs/flow/decorators.md +18 -18
  51. package/docs/flow/events.md +16 -16
  52. package/docs/flow/forms.md +17 -17
  53. package/docs/flow/icons.md +5 -5
  54. package/docs/flow/index.md +20 -20
  55. package/docs/flow/layouts.md +35 -35
  56. package/docs/flow/lifecycle.md +16 -16
  57. package/docs/flow/models.md +9 -9
  58. package/docs/flow/pagination.md +8 -8
  59. package/docs/flow/performance.md +7 -7
  60. package/docs/flow/references.md +3 -3
  61. package/docs/flow/routing.md +26 -26
  62. package/docs/flow/testing.md +20 -20
  63. package/docs/getting-started.md +18 -13
  64. package/docs/health.md +17 -8
  65. package/docs/helpers.md +17 -17
  66. package/docs/i18n.md +10 -10
  67. package/docs/inertia/devtools.md +4 -4
  68. package/docs/inertia/index.md +6 -6
  69. package/docs/inertia/props.md +21 -21
  70. package/docs/inertia/rendering.md +9 -9
  71. package/docs/inertia/ssr.md +2 -2
  72. package/docs/lifecycle.md +2 -2
  73. package/docs/lock.md +11 -11
  74. package/docs/logger.md +12 -12
  75. package/docs/media.md +22 -22
  76. package/docs/middleware.md +17 -17
  77. package/docs/migrations.md +13 -13
  78. package/docs/monitor.md +6 -6
  79. package/docs/notifications.md +24 -24
  80. package/docs/orm/casts.md +14 -14
  81. package/docs/orm/factories.md +8 -8
  82. package/docs/orm/index.md +22 -20
  83. package/docs/orm/lifecycle.md +13 -13
  84. package/docs/orm/queries.md +28 -28
  85. package/docs/orm/relationships.md +16 -16
  86. package/docs/orm/serialization.md +11 -11
  87. package/docs/package-development.md +10 -10
  88. package/docs/pagination.md +14 -14
  89. package/docs/password-reset.md +6 -6
  90. package/docs/providers.md +11 -11
  91. package/docs/query-builder.md +18 -18
  92. package/docs/queue.md +14 -12
  93. package/docs/rate-limiting.md +6 -6
  94. package/docs/responses.md +14 -14
  95. package/docs/roles-and-2fa.md +9 -9
  96. package/docs/routing.md +44 -44
  97. package/docs/scaffolding.md +1 -1
  98. package/docs/scheduler.md +10 -10
  99. package/docs/seeding.md +6 -6
  100. package/docs/session.md +16 -14
  101. package/docs/social.md +14 -14
  102. package/docs/storage.md +25 -13
  103. package/docs/structure.md +2 -2
  104. package/docs/support-policy.md +23 -10
  105. package/docs/telemetry.md +12 -12
  106. package/docs/tenancy.md +34 -40
  107. package/docs/testing/browser.md +6 -6
  108. package/docs/testing/console.md +3 -3
  109. package/docs/testing/database.md +10 -10
  110. package/docs/testing/flow-browser.md +6 -6
  111. package/docs/testing/http.md +12 -12
  112. package/docs/testing/index.md +3 -3
  113. package/docs/testing/mocking.md +8 -8
  114. package/docs/upgrade.md +1 -1
  115. package/docs/validator.md +19 -19
  116. package/docs/view.md +14 -14
  117. package/package.json +4 -4
  118. package/src/index.ts +25 -1
  119. package/src/tools/searchDocs.ts +7 -0
package/docs/responses.md CHANGED
@@ -27,7 +27,7 @@ import type { HttpContext } from "zerotal";
27
27
 
28
28
  ## JSON
29
29
 
30
- ```typescript
30
+ ```typescript fragment
31
31
  // in a controller
32
32
  ctx.json(data); // 200 OK — Content-Type: application/json
33
33
  ctx.json(data, 201); // 201 Created
@@ -44,7 +44,7 @@ JSON-serialisable is accepted: plain objects, arrays, ORM models, `null`.
44
44
  `view()` accepts either pre-rendered markup, or a view component plus its props.
45
45
  It prepends `<!DOCTYPE html>` and sets `Content-Type: text/html`:
46
46
 
47
- ```typescript
47
+ ```typescript fragment
48
48
  // in a controller
49
49
  import { WelcomePage } from "../../resources/views/WelcomePage.tsx";
50
50
 
@@ -69,7 +69,7 @@ once for the whole project, and already present in scaffolded apps. See the
69
69
 
70
70
  For htmx, Turbo Streams, or any partial render — no DOCTYPE prepended:
71
71
 
72
- ```typescript
72
+ ```typescript fragment
73
73
  // in a controller
74
74
  ctx.html('<p class="alert">Saved!</p>');
75
75
  ctx.html(renderPartial(data), 200);
@@ -81,7 +81,7 @@ Render a Markdown string to a full HTML page using Bun's built-in
81
81
  `Bun.markdown.html()`. Tables, strikethrough, tasklists, autolinks, and heading
82
82
  IDs are enabled by default:
83
83
 
84
- ```typescript
84
+ ```typescript fragment
85
85
  // in a controller
86
86
  const content = await Bun.file("./docs/guide.md").text();
87
87
  ctx.markdown(content);
@@ -93,7 +93,7 @@ falling back to `"Docs"`.
93
93
 
94
94
  ## Redirects
95
95
 
96
- ```typescript
96
+ ```typescript fragment
97
97
  // in a controller
98
98
  ctx.redirect("/dashboard"); // 302 Found
99
99
  ctx.redirect("/dashboard", 303); // 303 See Other ← use after POST/PUT
@@ -120,7 +120,7 @@ points to a different origin.
120
120
 
121
121
  Return a `Response` directly with the appropriate headers:
122
122
 
123
- ```typescript
123
+ ```typescript fragment
124
124
  // in a controller
125
125
  const file = Bun.file("./exports/report.csv");
126
126
 
@@ -136,7 +136,7 @@ ctx.response = new Response(file as unknown as BodyInit, {
136
136
 
137
137
  Set `ctx.response` to any `Response` object — the pipeline sends it verbatim:
138
138
 
139
- ```typescript
139
+ ```typescript fragment
140
140
  // in a controller
141
141
  ctx.response = new Response("pong", { status: 200 });
142
142
 
@@ -157,7 +157,7 @@ ctx.response = new Response(stream, {
157
157
  Every helper assigns a fresh `Response` to `ctx.response`. To add a header,
158
158
  rebuild it from the existing one:
159
159
 
160
- ```typescript
160
+ ```typescript fragment
161
161
  // in a controller, after setting ctx.response via any helper
162
162
  const existing = ctx.response!;
163
163
  const headers = new Headers(existing.headers);
@@ -175,7 +175,7 @@ Flash a message and redirect in one step — the flashed value is available via
175
175
  `ctx.flashed()` on the next request. Flashing requires an active session
176
176
  (see [Session](/docs/session)) and silently no-ops without one:
177
177
 
178
- ```typescript
178
+ ```typescript fragment
179
179
  // in a controller
180
180
  ctx.flash("success", "Post created!");
181
181
  ctx.redirect("/posts", 303);
@@ -190,7 +190,7 @@ redirects.
190
190
  Use `wantsJson()` (true when the client sends `Accept: application/json`) to
191
191
  respond differently based on what the client accepts:
192
192
 
193
- ```typescript
193
+ ```typescript fragment
194
194
  // in a controller
195
195
  async destroy(ctx: HttpContext): Promise<void> {
196
196
  await post.delete();
@@ -211,7 +211,7 @@ the controller entirely with `Router.view()`. A static props object is evaluated
211
211
  once at registration; a factory function receives the `HttpContext` per request
212
212
  and may be async:
213
213
 
214
- ```typescript
214
+ ```typescript fragment
215
215
  // routes/web.ts
216
216
  import { Router } from "zerotal";
217
217
 
@@ -244,7 +244,7 @@ sentence you would have written in a comment.
244
244
  **Assert the status by meaning, not by number**, so a failure says what went
245
245
  wrong rather than what integer it saw:
246
246
 
247
- ```typescript
247
+ ```typescript fragment
248
248
  // tests/http/posts.test.ts
249
249
  import { test } from "bun:test";
250
250
  import { createApp } from "../helpers.ts";
@@ -268,7 +268,7 @@ reach for `assertStatus(n)` only when the code has no name.
268
268
  how a broken route passes. `assertRedirect` checks the `Location` header;
269
269
  `assertJson` parses the body:
270
270
 
271
- ```typescript
271
+ ```typescript fragment
272
272
  // tests/http/posts.test.ts
273
273
  res.assertRedirect("/posts/hello"); // 3xx + Location
274
274
  res.assertJson({ id: 1, title: "Hello" }); // exact body match
@@ -283,7 +283,7 @@ field is added, which trains people to update tests without reading them.
283
283
  **Headers and cookies are part of the response contract** when a client depends
284
284
  on them:
285
285
 
286
- ```typescript
286
+ ```typescript fragment
287
287
  // tests/http/downloads.test.ts
288
288
  res.assertHeader("Content-Type", "text/csv");
289
289
  res.assertHeader("Content-Disposition");
@@ -80,7 +80,7 @@ export default AuthConfig({
80
80
 
81
81
  2FA stores three columns on the user. Add a migration:
82
82
 
83
- ```typescript
83
+ ```typescript fragment
84
84
  // database/migrations/003_add_two_factor_to_users.ts
85
85
  export default class AddTwoFactorToUsers extends Migration {
86
86
  async up(schema: Schema): Promise<void> {
@@ -102,7 +102,7 @@ export default class AddTwoFactorToUsers extends Migration {
102
102
 
103
103
  Then expose them on the model:
104
104
 
105
- ```typescript
105
+ ```typescript fragment
106
106
  // app/models/User.ts
107
107
  @(table("users").withTimestamps())
108
108
  export class User extends AuthUser {
@@ -124,7 +124,7 @@ Enrolment is a two-step flow: generate a secret and show its QR code, then verif
124
124
  the user's first code before persisting. `TwoFactor.verifyCode()` and
125
125
  `TwoFactor.generateRecoveryCodes()` are synchronous.
126
126
 
127
- ```typescript
127
+ ```typescript fragment
128
128
  // app/controllers/TwoFactorController.ts
129
129
  import { TwoFactor } from "@zerotal/auth";
130
130
 
@@ -191,7 +191,7 @@ After login, the challenge controller verifies a TOTP code (falling back to a
191
191
  recovery code) and marks the session as confirmed. `verifyRecoveryCode()` returns
192
192
  the remaining codes synchronously so you can persist the consumed set:
193
193
 
194
- ```typescript
194
+ ```typescript fragment
195
195
  // app/controllers/TwoFactorChallengeController.ts
196
196
  import { TwoFactor, TWO_FACTOR_SESSION_KEY } from "@zerotal/auth";
197
197
 
@@ -242,7 +242,7 @@ export class TwoFactorChallengeController extends Controller {
242
242
  Add `TwoFactorMiddleware` **after** `AuthMiddleware` in any group that should
243
243
  require 2FA:
244
244
 
245
- ```typescript
245
+ ```typescript fragment
246
246
  // routes/web.ts
247
247
  import { AuthMiddleware, TwoFactorMiddleware } from "@zerotal/auth";
248
248
 
@@ -276,7 +276,7 @@ TwoFactorMiddleware.challengeRoute = "/auth/2fa";
276
276
  `getQrCodeSvg()` returns the scannable code as an inline `<svg>`, drawn in your
277
277
  process. Inline it — do not fetch it, and do not log it:
278
278
 
279
- ```tsx
279
+ ```tsx fragment
280
280
  // in a Flow page
281
281
  const qr = TwoFactor.getQrCodeSvg(user.email, secret, { size: 220 });
282
282
 
@@ -303,14 +303,14 @@ anyone enrolling on the device that holds the authenticator needs another way in
303
303
  — the secret in readable blocks to type, or the `otpauth://` URI from
304
304
  `getQrCodeUrl()` as a link, which opens the authenticator app directly:
305
305
 
306
- ```tsx
306
+ ```tsx fragment
307
307
  <a href={TwoFactor.getQrCodeUrl(user.email, secret)}>Open in my authenticator</a>
308
308
  ```
309
309
 
310
310
  To draw the code yourself — to a canvas, a PNG, or your own markup — `encodeQr()`
311
311
  returns the module matrix and `qrSvg()` renders one:
312
312
 
313
- ```ts
313
+ ```ts fragment
314
314
  import { encodeQr } from "@zerotal/auth";
315
315
 
316
316
  const matrix = encodeQr(TwoFactor.getQrCodeUrl(user.email, secret));
@@ -359,7 +359,7 @@ test("a recovery code verifies once and is then consumed", () => {
359
359
  what an authenticator app would show, so the test exercises the TOTP path rather
360
360
  than working around it:
361
361
 
362
- ```typescript
362
+ ```typescript fragment
363
363
  // tests/http/two-factor.test.ts
364
364
  import { test } from "bun:test";
365
365
  import { createApp } from "../helpers.ts";
package/docs/routing.md CHANGED
@@ -25,7 +25,7 @@ import { Router, route } from "zerotal";
25
25
 
26
26
  Register a `GET` route to a controller action, or to an inline closure:
27
27
 
28
- ```typescript
28
+ ```typescript fragment
29
29
  // routes/index.ts
30
30
  import { Router } from "zerotal";
31
31
 
@@ -53,7 +53,7 @@ rules.
53
53
 
54
54
  ### HTTP verbs
55
55
 
56
- ```typescript
56
+ ```typescript fragment
57
57
  // routes/index.ts
58
58
  Router.get("/users", UserController, "index");
59
59
  Router.post("/users", UserController, "store");
@@ -64,7 +64,7 @@ Router.delete("/users/:id", UserController, "destroy");
64
64
 
65
65
  Each call returns a `RouteRegistration` you can chain:
66
66
 
67
- ```typescript
67
+ ```typescript fragment
68
68
  // routes/index.ts
69
69
  Router.get("/posts/:slug", PostController, "show").name("posts.show").bind("post", Post);
70
70
  ```
@@ -73,7 +73,7 @@ Router.get("/posts/:slug", PostController, "show").name("posts.show").bind("post
73
73
 
74
74
  Pass middleware classes as the fourth argument:
75
75
 
76
- ```typescript
76
+ ```typescript fragment
77
77
  // routes/index.ts
78
78
  Router.get("/dashboard", DashboardController, "index", [AuthMiddleware]);
79
79
  Router.post("/posts", PostController, "store", [AuthMiddleware, ThrottleMiddleware]);
@@ -102,7 +102,7 @@ Router.get("/posts/:slug", (ctx: HttpContext<{ slug: string }>) =>
102
102
  Middleware is passed as the **third** argument in closure form (there's no action
103
103
  name), and the returned `RouteRegistration` still chains `.name()` / `.bind()`:
104
104
 
105
- ```typescript
105
+ ```typescript fragment
106
106
  // routes/index.ts
107
107
  Router.get("/admin", (ctx) => ctx.json({ ok: true }), [AuthMiddleware]);
108
108
  Router.get("/posts/:post", (ctx: HttpContext<{ post: Post }>) =>
@@ -118,7 +118,7 @@ Router.get("/posts/:post", (ctx: HttpContext<{ post: Post }>) =>
118
118
 
119
119
  `Router.resource()` registers all seven RESTful actions in one call:
120
120
 
121
- ```typescript
121
+ ```typescript fragment
122
122
  // routes/index.ts
123
123
  Router.resource("posts", PostController);
124
124
  ```
@@ -138,7 +138,7 @@ are accepted.
138
138
 
139
139
  #### Filtering actions
140
140
 
141
- ```typescript
141
+ ```typescript fragment
142
142
  // routes/index.ts
143
143
  // Register only these actions
144
144
  Router.resource("photos", PhotoController).only(["index", "show"]);
@@ -149,7 +149,7 @@ Router.resource("tags", TagController).except(["create", "edit"]);
149
149
 
150
150
  #### Resource middleware
151
151
 
152
- ```typescript
152
+ ```typescript fragment
153
153
  // routes/index.ts
154
154
  Router.resource("comments", CommentController, [AuthMiddleware]);
155
155
  ```
@@ -159,7 +159,7 @@ Router.resource("comments", CommentController, [AuthMiddleware]);
159
159
  Register a GET route that renders a `@zerotal/core` JSX component directly —
160
160
  no controller class needed for simple pages:
161
161
 
162
- ```typescript
162
+ ```typescript fragment
163
163
  // routes/index.ts
164
164
  import { AboutPage } from "../resources/views/AboutPage.tsx";
165
165
  import { DashboardPage } from "../resources/views/DashboardPage.tsx";
@@ -182,7 +182,7 @@ Router.view("/settings", SettingsPage, (ctx) => ({ user: ctx.user }), [AuthMiddl
182
182
 
183
183
  The route chains `.name()` and `.withLayout()`:
184
184
 
185
- ```typescript
185
+ ```typescript fragment
186
186
  // routes/index.ts
187
187
  Router.view("/about", AboutPage).name("about").withLayout(AppLayout);
188
188
  ```
@@ -191,7 +191,7 @@ Router.view("/about", AboutPage).name("about").withLayout(AppLayout);
191
191
 
192
192
  Serve a local directory under a URL prefix:
193
193
 
194
- ```typescript
194
+ ```typescript fragment
195
195
  // routes/index.ts
196
196
  Router.static("/assets", "./public/assets");
197
197
  Router.static("/uploads", "public/uploads");
@@ -211,7 +211,7 @@ is pre-registered at boot as a static `Response` (zero JS per request); pass
211
211
 
212
212
  Serve a directory of `.md` files as rendered HTML pages:
213
213
 
214
- ```typescript
214
+ ```typescript fragment
215
215
  // routes/index.ts
216
216
  Router.markdown("/docs", "./docs");
217
217
 
@@ -232,7 +232,7 @@ Register a handler that receives the raw `Request` and bypasses the entire globa
232
232
  middleware pipeline — no `HttpContext`, no session, no auth. Useful for internal
233
233
  health checks or asset endpoints:
234
234
 
235
- ```typescript
235
+ ```typescript fragment
236
236
  // routes/index.ts
237
237
  Router.raw("GET", "/__ping", () => new Response("pong"));
238
238
  Router.raw("GET", "/health", async () => {
@@ -252,7 +252,7 @@ Router.raw("GET", "/health", async () => {
252
252
 
253
253
  Added by the `@zerotal/flow` package:
254
254
 
255
- ```typescript
255
+ ```typescript fragment
256
256
  // routes/index.ts
257
257
  import { Router } from "zerotal";
258
258
 
@@ -267,7 +267,7 @@ See the [Flow](/docs/flow) guide for full details.
267
267
  Groups apply a shared prefix and/or middleware stack to a set of routes. Groups
268
268
  nest — prefix and middleware accumulate.
269
269
 
270
- ```typescript
270
+ ```typescript fragment
271
271
  // routes/index.ts
272
272
  Router.group({ prefix: "/api/v1" }, () => {
273
273
  Router.get("/users", UserController, "index");
@@ -276,7 +276,7 @@ Router.group({ prefix: "/api/v1" }, () => {
276
276
  });
277
277
  ```
278
278
 
279
- ```typescript
279
+ ```typescript fragment
280
280
  // routes/index.ts
281
281
  Router.group({ prefix: "/api/v1", middleware: AuthMiddleware }, () => {
282
282
  Router.resource("posts", PostController);
@@ -294,7 +294,7 @@ routes declared after it.
294
294
  Define a group of middleware classes under a string key, then reference it by
295
295
  name:
296
296
 
297
- ```typescript
297
+ ```typescript fragment
298
298
  // in a ServiceProvider or bootstrap
299
299
  Router.middlewareGroup("api", [ThrottleMiddleware, JsonMiddleware]);
300
300
  Router.middlewareGroup("web", [SessionMiddleware, CsrfMiddleware]);
@@ -316,7 +316,7 @@ it everywhere that group is used instead of requiring an edit at every call site
316
316
 
317
317
  ### Nested groups
318
318
 
319
- ```typescript
319
+ ```typescript fragment
320
320
  // routes/index.ts
321
321
  Router.group({ prefix: "/admin" }, () => {
322
322
  Router.group({ middleware: [AuthMiddleware, AdminMiddleware] }, () => {
@@ -345,7 +345,7 @@ group.
345
345
  The `domain` option scopes a group of routes to a specific host. Dynamic `:label`
346
346
  segments are captured and exposed on the context via `ctx.subdomains`.
347
347
 
348
- ```typescript
348
+ ```typescript fragment
349
349
  // routes/index.ts
350
350
  import { Router } from "zerotal";
351
351
 
@@ -360,7 +360,7 @@ Router.group({ domain: ":tenant.app.com" }, () => {
360
360
  });
361
361
  ```
362
362
 
363
- ```typescript
363
+ ```typescript fragment
364
364
  // app/controllers/DashboardController.ts
365
365
  class DashboardController {
366
366
  index(ctx: HttpContext) {
@@ -383,7 +383,7 @@ time.
383
383
  `@zerotal/tenancy`'s `SubdomainResolver` resolves the tenant _model_ from the same
384
384
  subdomain a domain group captures, so the two compose directly:
385
385
 
386
- ```typescript
386
+ ```typescript fragment
387
387
  // routes/index.ts
388
388
  Router.group({ domain: ":tenant.app.com" }, () => {
389
389
  Router.get("/dashboard", DashboardController, "index");
@@ -397,7 +397,7 @@ Configuring the resolver is covered in [Multi-tenancy](/docs/tenancy).
397
397
 
398
398
  Assign a name and generate URLs from it:
399
399
 
400
- ```typescript
400
+ ```typescript fragment
401
401
  // routes/index.ts
402
402
  Router.get("/posts/:slug", PostController, "show").name("posts.show");
403
403
 
@@ -436,7 +436,7 @@ route("docs.show", { "*": ["guides", "intro"] }); // → '/docs/guides/intro'
436
436
  Run `bun zt route:types` and the names above stop being strings the compiler has
437
437
  to take on faith:
438
438
 
439
- ```typescript
439
+ ```typescript fragment
440
440
  route("psots.show", { slug }); // ✗ not assignable to RouteName
441
441
  route("posts.show"); // ✗ Expected 2 arguments, but got 1
442
442
  route("posts.show", {}); // ✗ Property 'slug' is missing
@@ -475,7 +475,7 @@ it always did — every name accepted, nothing checked.
475
475
  **When the name is not known at compile time** — read from config, chosen by a
476
476
  package — use the escape hatch, which does the same work with no checking:
477
477
 
478
- ```typescript
478
+ ```typescript fragment
479
479
  route.dynamic(config("app.home_route"), { id });
480
480
  ```
481
481
 
@@ -506,7 +506,7 @@ server renders — a `view` build's `href` attributes and form actions included.
506
506
  A browser bundle is a different process with no router to read, so there it needs
507
507
  the table handed to it once, at your entry point:
508
508
 
509
- ```typescript
509
+ ```typescript fragment
510
510
  // resources/js/app.js
511
511
  import { defineRoutes } from "zerotal/routes";
512
512
  import { ROUTES } from "../../types/routes.generated";
@@ -516,7 +516,7 @@ defineRoutes(ROUTES);
516
516
 
517
517
  From there the call is the one you already know:
518
518
 
519
- ```typescript
519
+ ```typescript fragment
520
520
  route("posts.show", { slug }); // → '/posts/hello'
521
521
  route("posts.index", {}, { page: 2 }); // → '/posts?page=2'
522
522
  ```
@@ -532,7 +532,7 @@ type-checks in a controller type-checks in a component.
532
532
  `defineRoutes()` also puts `route()` on `globalThis`, so a page, a component or a
533
533
  controller calls it with nothing at the top of the file:
534
534
 
535
- ```tsx
535
+ ```tsx fragment
536
536
  // no import line
537
537
  <a href={route("posts.show", { slug })}>{post.title}</a>
538
538
  ```
@@ -585,7 +585,7 @@ Nothing to install, and the names are the same ones the server rendered with.
585
585
  `route()` gives you a URL. A form needs two things — where to send the request
586
586
  and how — and a URL alone leaves the second one to be typed out beside it:
587
587
 
588
- ```typescript
588
+ ```typescript fragment
589
589
  // The URL is generated; the verb is a guess that happens to be right today.
590
590
  form.post(route("posts.comments.store", { post: id }));
591
591
  ```
@@ -593,7 +593,7 @@ form.post(route("posts.comments.store", { post: id }));
593
593
  `bun zt route:types` also writes a `METHODS` table, so the verb can come from
594
594
  the same place the URL does. `action()` returns both:
595
595
 
596
- ```typescript
596
+ ```typescript fragment
597
597
  // resources/js/app.js
598
598
  import { defineRouteMethods, defineRoutes } from "zerotal/routes";
599
599
  import { METHODS, ROUTES } from "../../types/routes.generated";
@@ -602,7 +602,7 @@ defineRoutes(ROUTES);
602
602
  defineRouteMethods(METHODS);
603
603
  ```
604
604
 
605
- ```typescript
605
+ ```typescript fragment
606
606
  import { action } from "zerotal/routes";
607
607
 
608
608
  const endpoint = action("posts.comments.store", { post: id });
@@ -634,7 +634,7 @@ Opt in with `app.fileBasedRouting()` in `bootstrap/app.ts`, passing a map of
634
634
  named route groups (each `web`/`api` group brings its own default prefix and
635
635
  middleware) to directories:
636
636
 
637
- ```typescript
637
+ ```typescript fragment
638
638
  // bootstrap/app.ts
639
639
  import { Application, basePath } from "zerotal";
640
640
  import providers from "./providers";
@@ -667,7 +667,7 @@ segment); `index` becomes the directory URL.
667
667
 
668
668
  A file may export handlers for one or more HTTP verbs:
669
669
 
670
- ```typescript
670
+ ```typescript fragment
671
671
  // app/routes/users/[id].ts
672
672
  import type { HttpContext } from "zerotal";
673
673
 
@@ -725,7 +725,7 @@ Place a `_middleware.ts` file in any directory to protect all routes under it.
725
725
  The scanner walks from the root down to the file's directory and stacks
726
726
  middleware outermost-first:
727
727
 
728
- ```typescript
728
+ ```typescript fragment
729
729
  // app/routes/api/v2/me/_middleware.ts
730
730
  import { RequireAuthMiddleware } from "../../../middleware/RequireAuthMiddleware.ts";
731
731
 
@@ -762,7 +762,7 @@ When `@zerotal/flow` is installed, the file scanner recognises Flow
762
762
  configuration. Verb handlers (`POST`, `DELETE`, etc.) in the same file still
763
763
  register normally alongside the page:
764
764
 
765
- ```typescript
765
+ ```typescript fragment
766
766
  // app/flow/pages/(protected)/dashboard.tsx
767
767
  import type { HttpContext } from "zerotal";
768
768
 
@@ -789,7 +789,7 @@ route param whose name matches a model resolves to a loaded instance with no
789
789
  configuration. `:user` resolves via `User`, `:post` via `Post`, `:blogPost` via
790
790
  `BlogPost` (and a plural `:users` resolves to `User` too, via singularization).
791
791
 
792
- ```typescript
792
+ ```typescript fragment
793
793
  // routes/index.ts — nothing to declare:
794
794
  Router.get("/users/:user", UserController, "show");
795
795
 
@@ -811,7 +811,7 @@ A model that resolves by something other than its primary key says so once, on t
811
811
  model, rather than at every route that mentions it. Declare
812
812
  `static resolveRouteBinding` and it is used wherever that model binds:
813
813
 
814
- ```typescript
814
+ ```typescript fragment
815
815
  // app/models/User.ts
816
816
  @table("users")
817
817
  export class User extends Model {
@@ -852,7 +852,7 @@ controller**. That ordering is a guarantee, not an implementation detail:
852
852
 
853
853
  Two static properties on the model control implicit binding:
854
854
 
855
- ```typescript
855
+ ```typescript fragment
856
856
  // app/models/User.ts
857
857
  @table("users")
858
858
  export class User extends Model {
@@ -879,7 +879,7 @@ There are exactly two ways to change how a param resolves, and they differ only
879
879
  scope: put it on the **model** when it is how that model always resolves, or on the
880
880
  **route** when it belongs to that one route.
881
881
 
882
- ```typescript
882
+ ```typescript fragment
883
883
  // in a controller — either way, it is already resolved
884
884
  async show(ctx: HttpContext) {
885
885
  const user = ctx.model<User>('user'); // no DB call needed here
@@ -894,7 +894,7 @@ controller runs.
894
894
 
895
895
  Overrides the model's own resolution for a single route:
896
896
 
897
- ```typescript
897
+ ```typescript fragment
898
898
  // routes/index.ts
899
899
  Router.get("/posts/:post", PostController, "show").name("posts.show").bind("post", Post);
900
900
 
@@ -909,7 +909,7 @@ Router.get("/articles/:article", ArticleController, "show").bind("article", (val
909
909
  The resolved instance is available two ways — via `ctx.model()`, or on
910
910
  `ctx.params` under the param's name:
911
911
 
912
- ```typescript
912
+ ```typescript fragment
913
913
  // in a controller — via ctx.model()
914
914
  async show(ctx: HttpContext) {
915
915
  const post = ctx.model<Post>('post');
@@ -945,7 +945,7 @@ keys — registering the same path twice overwrites the first.
945
945
  For resource routes, register literal paths **before** dynamic ones when there is
946
946
  a naming conflict:
947
947
 
948
- ```typescript
948
+ ```typescript fragment
949
949
  // routes/index.ts
950
950
  // Correct — /posts/create is matched before /posts/:slug
951
951
  Router.get("/posts/create", PostController, "showCreate");
@@ -994,7 +994,7 @@ Both forms boot the app, so a route a provider registers is included.
994
994
 
995
995
  ### Programmatic inspection
996
996
 
997
- ```typescript
997
+ ```typescript fragment
998
998
  // anywhere after boot
999
999
  Router.routes; // ReadonlyMap<string, RouteDefinition>
1000
1000
  Router.namedRoutes; // ReadonlyMap<string, string> (name → path)
@@ -1008,7 +1008,7 @@ Router.middlewareFor("GET", "/dashboard"); // MiddlewareClass[]
1008
1008
  Use `HttpContext.fake()` to unit-test controllers and route handlers without a
1009
1009
  running server:
1010
1010
 
1011
- ```typescript
1011
+ ```typescript fragment
1012
1012
  // src/tests/PostController.test.ts
1013
1013
  import { HttpContext } from "zerotal";
1014
1014
 
@@ -1022,7 +1022,7 @@ await new PostController().show(ctx);
1022
1022
  For full integration tests, boot the real app with `createTestApp()` from
1023
1023
  `@zerotal/testing` and exercise it over real requests:
1024
1024
 
1025
- ```typescript
1025
+ ```typescript fragment
1026
1026
  // src/tests/PostTest.ts
1027
1027
  import { beforeAll, afterAll, it } from "bun:test";
1028
1028
  import { createTestApp, type TestApp } from "@zerotal/testing";
@@ -269,7 +269,7 @@ The underlying codemod utilities are exported from `zerotal` for use in your
269
269
  own generators, install scripts, or migration tools. Each is idempotent — a no-op
270
270
  when the change is already present.
271
271
 
272
- ```ts
272
+ ```ts fragment
273
273
  // in your own generator or script
274
274
  import {
275
275
  addImport,
package/docs/scheduler.md CHANGED
@@ -80,7 +80,7 @@ cadence with either a `cron` string or the fluent `frequency()` method. Every
80
80
  `Schedule` subclass under `app/schedules/` is discovered and registered
81
81
  automatically — no manual wiring, no central list.
82
82
 
83
- ```typescript
83
+ ```typescript fragment
84
84
  // app/schedules/SendDailyReports.ts
85
85
  import { Schedule } from "@zerotal/scheduler";
86
86
  import { Queue } from "@zerotal/queue";
@@ -100,7 +100,7 @@ export class SendDailyReports extends Schedule {
100
100
  Prefer the fluent frequency builder over a raw cron string when it reads better —
101
101
  override `frequency()` and return a configured task:
102
102
 
103
- ```typescript
103
+ ```typescript fragment
104
104
  // app/schedules/WarmCache.ts
105
105
  import { Schedule, type SchedulerBuilder } from "@zerotal/scheduler";
106
106
 
@@ -152,7 +152,7 @@ Every setting is an optional property (or method) on your `Schedule` subclass:
152
152
  > Discovery warns at boot when it sees static schedule config, and
153
153
  > `bun zt doctor` reports it.
154
154
 
155
- ```typescript
155
+ ```typescript fragment
156
156
  // app/schedules/NightlyBackup.ts
157
157
  import { Schedule, type SchedulerBuilder } from "@zerotal/scheduler";
158
158
 
@@ -213,7 +213,7 @@ expose fluent cadence methods. Each returns the configured task.
213
213
  For quick, in-code definitions (e.g. inside a provider) use the `Scheduler` facade,
214
214
  which exposes the underlying manager fluently:
215
215
 
216
- ```typescript
216
+ ```typescript fragment
217
217
  // in a provider's onBooted()
218
218
  import { Scheduler } from "@zerotal/scheduler";
219
219
 
@@ -228,7 +228,7 @@ Scheduler.add("rotate-logs", "0 */6 * * *", () => rotateLogs());
228
228
  `ScheduledTask`, so you can chain the same fluent tuning the class form exposes
229
229
  declaratively:
230
230
 
231
- ```typescript
231
+ ```typescript fragment
232
232
  // in a provider's onBooted()
233
233
  Scheduler.job("nightly-backup", () => runBackup())
234
234
  .dailyAt("02:30")
@@ -332,7 +332,7 @@ is configured (Redis or SQLite via the [lock primitive](/docs/lock)) — also ta
332
332
  machines. Cross-process locking is **on by default**; pass `{ crossProcess: false }`
333
333
  to guard within this process only:
334
334
 
335
- ```typescript
335
+ ```typescript fragment
336
336
  // app/schedules/RebuildSearchIndex.ts
337
337
  withoutOverlapping = { expiresAfterMinutes: 30 }; // cross-process (default)
338
338
  // withoutOverlapping = { crossProcess: false }; // in-process guard only
@@ -370,7 +370,7 @@ Anything the task writes to `console.log` can be persisted or emailed:
370
370
  > **Note** — `sendOutputTo` is a facade-only tuning method; on a `Schedule`
371
371
  > subclass, use the `appendOutputTo` or `emailOutputTo` properties.
372
372
 
373
- ```typescript
373
+ ```typescript fragment
374
374
  // app/schedules/GenerateSitemap.ts
375
375
  import { Schedule } from "@zerotal/scheduler";
376
376
 
@@ -404,7 +404,7 @@ ScheduledTask.outputMailer = async (email, subject, body) => {
404
404
  Every run emits a framework event you can listen for to feed metrics, logs, or
405
405
  alerts. Subscribe in a provider's `onBooted()`:
406
406
 
407
- ```typescript
407
+ ```typescript fragment
408
408
  // in a provider's onBooted()
409
409
  import { FrameworkEvents } from "zerotal";
410
410
  import { TaskRan, TaskFailed, TaskSkipped } from "@zerotal/scheduler";
@@ -446,7 +446,7 @@ as data. To keep the scheduler but drop the section, set
446
446
  `ScheduledTask` exposes introspection getters and a `runNow()` that executes the
447
447
  handler immediately, bypassing the cron/time-window guards — ideal in tests:
448
448
 
449
- ```typescript
449
+ ```typescript fragment
450
450
  // in a test
451
451
  import { Scheduler } from "@zerotal/scheduler";
452
452
 
@@ -474,7 +474,7 @@ bun zt worker # starts the queue worker + scheduler
474
474
  For simpler deployments, `AppServiceProvider.onStarted()` can run inline polling
475
475
  instead of a dedicated worker process (skip it when this IS the worker):
476
476
 
477
- ```typescript
477
+ ```typescript fragment
478
478
  // app/providers/AppServiceProvider.ts (onStarted)
479
479
  override async onStarted(): Promise<void> {
480
480
  if (Bun.env.APP_ENV === "worker") return; // dedicated worker handles it