@zerotal/arch 1.7.4 → 1.8.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +2 -0
  4. package/docs/about.md +16 -16
  5. package/docs/admin/actions.md +9 -9
  6. package/docs/admin/auth.md +2 -2
  7. package/docs/admin/dashboard.md +5 -5
  8. package/docs/admin/extending-ui.md +12 -12
  9. package/docs/admin/extending.md +16 -7
  10. package/docs/admin/forms.md +4 -4
  11. package/docs/admin/index.md +2 -2
  12. package/docs/admin/operations.md +9 -9
  13. package/docs/admin/resources.md +3 -3
  14. package/docs/admin/structure.md +6 -6
  15. package/docs/admin/tables.md +8 -8
  16. package/docs/admin/testing.md +4 -4
  17. package/docs/ai.md +118 -11
  18. package/docs/application.md +12 -12
  19. package/docs/arch.md +25 -3
  20. package/docs/assets.md +11 -11
  21. package/docs/audit.md +13 -13
  22. package/docs/authentication.md +39 -39
  23. package/docs/authorization.md +13 -13
  24. package/docs/broadcasting/channels.md +2 -2
  25. package/docs/broadcasting/client.md +2 -2
  26. package/docs/broadcasting/events.md +9 -10
  27. package/docs/broadcasting/index.md +1 -1
  28. package/docs/broadcasting/testing.md +5 -5
  29. package/docs/cache.md +15 -13
  30. package/docs/carbon.md +35 -35
  31. package/docs/changelog.md +268 -1
  32. package/docs/client/index.md +13 -13
  33. package/docs/commands.md +36 -4
  34. package/docs/components.md +116 -115
  35. package/docs/config-system.md +4 -4
  36. package/docs/container.md +27 -27
  37. package/docs/context.md +27 -27
  38. package/docs/contributing.md +28 -0
  39. package/docs/controllers.md +10 -10
  40. package/docs/conventions.md +23 -15
  41. package/docs/cookies.md +3 -3
  42. package/docs/csrf.md +6 -6
  43. package/docs/database.md +22 -17
  44. package/docs/deployment.md +99 -2
  45. package/docs/devtools.md +16 -16
  46. package/docs/email-verification.md +8 -8
  47. package/docs/encryption.md +7 -7
  48. package/docs/errors.md +6 -6
  49. package/docs/events.md +12 -11
  50. package/docs/flow/components.md +32 -32
  51. package/docs/flow/decorators.md +18 -18
  52. package/docs/flow/events.md +16 -16
  53. package/docs/flow/forms.md +17 -17
  54. package/docs/flow/icons.md +5 -5
  55. package/docs/flow/index.md +21 -21
  56. package/docs/flow/layouts.md +99 -35
  57. package/docs/flow/lifecycle.md +16 -16
  58. package/docs/flow/models.md +9 -9
  59. package/docs/flow/pagination.md +8 -8
  60. package/docs/flow/performance.md +7 -7
  61. package/docs/flow/references.md +3 -3
  62. package/docs/flow/routing.md +26 -26
  63. package/docs/flow/testing.md +20 -20
  64. package/docs/getting-started.md +18 -13
  65. package/docs/health.md +17 -8
  66. package/docs/helpers.md +17 -17
  67. package/docs/i18n.md +10 -10
  68. package/docs/inertia/devtools.md +4 -4
  69. package/docs/inertia/index.md +6 -6
  70. package/docs/inertia/props.md +21 -21
  71. package/docs/inertia/rendering.md +9 -9
  72. package/docs/inertia/ssr.md +2 -2
  73. package/docs/lifecycle.md +2 -2
  74. package/docs/lock.md +11 -11
  75. package/docs/logger.md +12 -12
  76. package/docs/media.md +22 -22
  77. package/docs/middleware.md +17 -17
  78. package/docs/migrations.md +13 -13
  79. package/docs/monitor.md +6 -6
  80. package/docs/notifications.md +24 -24
  81. package/docs/orm/casts.md +14 -14
  82. package/docs/orm/factories.md +8 -8
  83. package/docs/orm/index.md +22 -20
  84. package/docs/orm/lifecycle.md +13 -13
  85. package/docs/orm/queries.md +28 -28
  86. package/docs/orm/relationships.md +16 -16
  87. package/docs/orm/serialization.md +11 -11
  88. package/docs/package-development.md +10 -10
  89. package/docs/pagination.md +14 -14
  90. package/docs/password-reset.md +6 -6
  91. package/docs/providers.md +11 -11
  92. package/docs/query-builder.md +18 -18
  93. package/docs/queue.md +14 -12
  94. package/docs/rate-limiting.md +6 -6
  95. package/docs/responses.md +14 -14
  96. package/docs/roles-and-2fa.md +9 -9
  97. package/docs/routing.md +44 -44
  98. package/docs/scaffolding.md +1 -1
  99. package/docs/scheduler.md +10 -10
  100. package/docs/seeding.md +6 -6
  101. package/docs/session.md +16 -14
  102. package/docs/social.md +14 -14
  103. package/docs/storage.md +25 -13
  104. package/docs/structure.md +2 -2
  105. package/docs/support-policy.md +23 -10
  106. package/docs/telemetry.md +12 -12
  107. package/docs/tenancy.md +34 -40
  108. package/docs/testing/browser.md +6 -6
  109. package/docs/testing/console.md +3 -3
  110. package/docs/testing/database.md +10 -10
  111. package/docs/testing/flow-browser.md +6 -6
  112. package/docs/testing/http.md +12 -12
  113. package/docs/testing/index.md +29 -3
  114. package/docs/testing/mocking.md +8 -8
  115. package/docs/upgrade.md +1 -1
  116. package/docs/validator.md +19 -19
  117. package/docs/view.md +14 -14
  118. package/package.json +4 -4
  119. package/src/config.ts +8 -0
  120. package/src/index.ts +25 -1
  121. package/src/install/ArchInstallCommand.ts +46 -3
  122. package/src/install/guidelines.ts +84 -0
  123. package/src/install/shape.ts +177 -0
  124. package/src/install/skills.ts +271 -0
  125. package/src/provider/ArchProvider.ts +148 -0
  126. package/src/tools/searchDocs.ts +7 -0
package/docs/lock.md CHANGED
@@ -90,7 +90,7 @@ The `Lock` facade resolves the live `LockManager` from the container on every
90
90
  call. `Lock.try` acquires once, runs your callback, and always releases — even if
91
91
  the callback throws:
92
92
 
93
- ```ts
93
+ ```ts fragment
94
94
  // in a controller or service
95
95
  import { Lock } from "zerotal/lock";
96
96
 
@@ -111,7 +111,7 @@ crashes.
111
111
  Acquire once, run the callback, release. Throws `LockNotAcquiredError`
112
112
  immediately if the lock is already held:
113
113
 
114
- ```ts
114
+ ```ts fragment
115
115
  // in a controller
116
116
  import { Lock, LockNotAcquiredError } from "zerotal/lock";
117
117
 
@@ -133,7 +133,7 @@ try {
133
133
  Wait up to `options.timeout` seconds for the lock to become free, then run the
134
134
  callback:
135
135
 
136
- ```ts
136
+ ```ts fragment
137
137
  // in a service
138
138
  import { Lock } from "zerotal/lock";
139
139
 
@@ -158,7 +158,7 @@ For complex flows where you need explicit acquire/release control. `Lock.make`
158
158
  returns a `ManagedLock` but does **not** acquire it — call `.acquire()` or
159
159
  `.block()` yourself:
160
160
 
161
- ```ts
161
+ ```ts fragment
162
162
  // in a service
163
163
  import { Lock } from "zerotal/lock";
164
164
 
@@ -205,7 +205,7 @@ different questions at once.
205
205
  Refreshing separates them. Pass `refresh: true` and the lock is extended in the
206
206
  background for as long as the callback runs:
207
207
 
208
- ```typescript
208
+ ```typescript fragment
209
209
  await Lock.block(
210
210
  "report:monthly",
211
211
  60,
@@ -232,7 +232,7 @@ believing they are the only one.
232
232
 
233
233
  So the callback's `AbortSignal` is aborted and `LockLostError` is thrown:
234
234
 
235
- ```typescript
235
+ ```typescript fragment
236
236
  try {
237
237
  await Lock.block("report:monthly", 60, run, { refresh: true });
238
238
  } catch (err) {
@@ -257,7 +257,7 @@ still valid, and nothing written before refreshing existed needs to change.
257
257
  A manual [`ManagedLock`](#lockmake-manual-handle) exposes the same thing directly,
258
258
  for flows that span steps rather than sitting inside one callback:
259
259
 
260
- ```typescript
260
+ ```typescript fragment
261
261
  const lock = Lock.make("import:batch", 60);
262
262
  if (await lock.acquire()) {
263
263
  try {
@@ -286,7 +286,7 @@ it is what `refresh()` does.
286
286
 
287
287
  ### Idempotent job processing
288
288
 
289
- ```ts
289
+ ```ts fragment
290
290
  // app/jobs/ProcessOrderJob.ts
291
291
  import { Lock } from "zerotal/lock";
292
292
 
@@ -304,7 +304,7 @@ export class ProcessOrderJob extends Job {
304
304
 
305
305
  ### Rate-limited report generation
306
306
 
307
- ```ts
307
+ ```ts fragment
308
308
  // app/controllers/ReportController.ts
309
309
  import { Lock, LockNotAcquiredError } from "zerotal/lock";
310
310
 
@@ -341,7 +341,7 @@ Contention surfaces as a single typed error, `LockNotAcquiredError`:
341
341
 
342
342
  The `Lock` facade re-exports it as `Lock.NotAcquired` for terse catch blocks:
343
343
 
344
- ```ts
344
+ ```ts fragment
345
345
  // in a controller
346
346
  import { Lock } from "zerotal/lock";
347
347
 
@@ -386,7 +386,7 @@ export class MyLockDriver implements LockDriver {
386
386
  Bind a `LockManager` built around it in a provider that runs **after**
387
387
  `LockProvider` (last write wins on the `"lock"` key):
388
388
 
389
- ```ts
389
+ ```ts fragment
390
390
  // app/providers/AppServiceProvider.ts
391
391
  import { ServiceProvider } from "zerotal";
392
392
  import { LockManager } from "zerotal/lock";
package/docs/logger.md CHANGED
@@ -91,7 +91,7 @@ export default LoggingConfig({
91
91
 
92
92
  ### The two sinks
93
93
 
94
- ```ts
94
+ ```ts fragment
95
95
  // config/logging.ts
96
96
  export default LoggingConfig({
97
97
  console: { level: "info", format: "json" }, // or false
@@ -107,7 +107,7 @@ point: you decide what to _watch_ without deciding what to _keep_.
107
107
  Turning the file off is reasonable in a container that ships stdout to a
108
108
  collector:
109
109
 
110
- ```ts
110
+ ```ts fragment
111
111
  // config/logging.ts
112
112
  export default LoggingConfig({ file: false });
113
113
  ```
@@ -117,7 +117,7 @@ export default LoggingConfig({ file: false });
117
117
  Channels are _extra_ destinations layered on top of the two sinks, for routing a
118
118
  subsystem somewhere specific:
119
119
 
120
- ```ts
120
+ ```ts fragment
121
121
  // config/logging.ts
122
122
  export default LoggingConfig({
123
123
  channels: {
@@ -159,7 +159,7 @@ Each channel is a `{ driver: … }` entry under `channels`, discriminated by its
159
159
 
160
160
  The two sinks already cover the common case, so this is for the extras:
161
161
 
162
- ```ts
162
+ ```ts fragment
163
163
  // config/logging.ts
164
164
  export default LoggingConfig({
165
165
  // Machine-readable terminal output in production, human-readable locally.
@@ -176,7 +176,7 @@ export default LoggingConfig({
176
176
  `Log` is a static proxy over the `LogManager` singleton. Use it anywhere —
177
177
  controllers, services, commands, event listeners:
178
178
 
179
- ```ts
179
+ ```ts fragment
180
180
  // in a controller or service
181
181
  import { Log } from "zerotal/logger";
182
182
 
@@ -189,7 +189,7 @@ Log.fatal("Database unreachable", { host: dbHost }, err);
189
189
 
190
190
  Every level shares the same signature:
191
191
 
192
- ```ts
192
+ ```ts fragment
193
193
  // signature — all five levels
194
194
  Log.info(message: string, context?: Record<string, unknown>, err?: unknown): void
195
195
  ```
@@ -202,7 +202,7 @@ Log.info(message: string, context?: Record<string, unknown>, err?: unknown): voi
202
202
  `Log.channel(name)` returns a `BoundLogger` that writes to the named channel
203
203
  instead of the configured default:
204
204
 
205
- ```ts
205
+ ```ts fragment
206
206
  // in a controller or service
207
207
  import { Log } from "zerotal/logger";
208
208
 
@@ -219,7 +219,7 @@ This lets specific modules write to a dedicated channel without changing
219
219
  that merges `extra` into every entry. Useful in long-running jobs or to tag a
220
220
  group of log lines with a shared identifier:
221
221
 
222
- ```ts
222
+ ```ts fragment
223
223
  // in a queue job
224
224
  import { Log } from "zerotal/logger";
225
225
 
@@ -304,7 +304,7 @@ Past three or four keys, inline context stops being readable — `{"compiled":0,
304
304
  is a wall the eye slides off. `table()` logs the same data and asks the console
305
305
  to draw it in columns:
306
306
 
307
- ```ts
307
+ ```ts fragment
308
308
  Log.table("Compile summary", { compiled: 0, cached: 2, injected: 3, runtime: 8, ms: 124 });
309
309
  ```
310
310
 
@@ -322,7 +322,7 @@ Log.table("Compile summary", { compiled: 0, cached: 2, injected: 3, runtime: 8,
322
322
  Pass a list of objects instead and each key becomes a column, with a header. A
323
323
  third argument sets the level, which defaults to `info`:
324
324
 
325
- ```ts
325
+ ```ts fragment
326
326
  Log.table(
327
327
  "Pages rendering through the runtime",
328
328
  [
@@ -452,7 +452,7 @@ gives you a tap.
452
452
  **`LogManager.tap()` receives every entry** after enrichment and before it
453
453
  reaches a channel, and returns an unsubscribe function:
454
454
 
455
- ```typescript
455
+ ```typescript fragment
456
456
  // tests/logging/AuditTrail.test.ts
457
457
  import { test, expect, afterEach } from "bun:test";
458
458
  import { LogManager, type LogEntry } from "zerotal/logger";
@@ -480,7 +480,7 @@ starts matching something from a different test.
480
480
  **Silence the logger in the suite** so a passing run stays readable. The `null`
481
481
  driver discards everything:
482
482
 
483
- ```typescript
483
+ ```typescript fragment
484
484
  // tests/helpers.ts
485
485
  .useConfig({
486
486
  logging: { default: "null", channels: { null: { driver: "null" } } },
package/docs/media.md CHANGED
@@ -9,7 +9,7 @@ Attach files to a model and let the framework handle the rest — where the byte
9
9
  live, the database row that points at them, the thumbnails, the URLs, the
10
10
  ordering, and the cleanup when the model goes away.
11
11
 
12
- ```ts
12
+ ```ts fragment
13
13
  const media = await product.addMedia(await ctx.file("photo")).toCollection("images");
14
14
 
15
15
  media.getUrl(); // the original
@@ -24,7 +24,7 @@ Install the package and register its provider:
24
24
  bun add @zerotal/media
25
25
  ```
26
26
 
27
- ```ts
27
+ ```ts fragment
28
28
  // bootstrap/providers.ts
29
29
  import { StorageProvider } from "zerotal/storage";
30
30
  import { MediaProvider } from "zerotal/media";
@@ -95,7 +95,7 @@ that silently creates a collection nobody ever reads from.
95
95
 
96
96
  ## Adding files
97
97
 
98
- ```ts
98
+ ```ts fragment
99
99
  // From an upload
100
100
  await product.addMedia(await ctx.file("photo")).toCollection("images");
101
101
 
@@ -127,7 +127,7 @@ derived the same way.
127
127
 
128
128
  ## Reading
129
129
 
130
- ```ts
130
+ ```ts fragment
131
131
  await product.getMedia("images"); // MediaItem[], in order
132
132
  await product.getFirstMedia("images"); // MediaItem | null
133
133
  await product.getFirstMediaUrl("images"); // or the collection's fallbackUrl
@@ -145,7 +145,7 @@ the original rather than to nothing.
145
145
 
146
146
  Conversions are declared per collection and generated when a file is added:
147
147
 
148
- ```ts
148
+ ```ts fragment
149
149
  conversions: {
150
150
  thumb: { width: 200, height: 200, format: "webp", quality: 80 },
151
151
  hero: { width: 1600, queued: true },
@@ -187,7 +187,7 @@ runtime, no native module, nothing to install.
187
187
  held to the same output dimensions by a shared parity suite, so switching is a
188
188
  performance decision rather than a feature one:
189
189
 
190
- | | `bun` (default) | `sharp` |
190
+ | Criterion | `bun` (default) | `sharp` |
191
191
  | --------------------------- | ----------------------- | -------------------------------- |
192
192
  | Install | Nothing | `bun add sharp`, a native module |
193
193
  | `fit: "cover"` | Yes | Yes |
@@ -220,7 +220,7 @@ conversion runs inline — late is better than a thumbnail that never appears.
220
220
  Set `responsive: true` on a collection to generate a width ladder plus an inline
221
221
  blur placeholder:
222
222
 
223
- ```tsx
223
+ ```tsx fragment
224
224
  <img
225
225
  src={media.getUrl()}
226
226
  srcset={media.srcset()}
@@ -237,7 +237,7 @@ needed.
237
237
 
238
238
  Any JSON you attach travels with the row:
239
239
 
240
- ```ts
240
+ ```ts fragment
241
241
  media.getCustomProperty("alt");
242
242
  media.setCustomProperty("alt", "A steel kettle");
243
243
  await media.save();
@@ -247,7 +247,7 @@ await media.save();
247
247
 
248
248
  Items carry an `orderColumn`, assigned in insertion order:
249
249
 
250
- ```ts
250
+ ```ts fragment
251
251
  await product.setMediaOrder([third.id, first.id], "images");
252
252
  ```
253
253
 
@@ -256,7 +256,7 @@ in only the items a drag-and-drop UI moved does what it looks like.
256
256
 
257
257
  ## Deleting
258
258
 
259
- ```ts
259
+ ```ts fragment
260
260
  await media.delete(); // row + original + every derivative
261
261
  await product.clearMediaCollection("images");
262
262
  await product.clearAllMedia();
@@ -271,7 +271,7 @@ that if the images went with it. Those files go on `forceDelete()`.
271
271
  Media inherits whatever the disk does. Put a collection on a private disk and
272
272
  hand out signed, expiring links instead of public URLs:
273
273
 
274
- ```ts
274
+ ```ts fragment
275
275
  await media.getTemporaryUrl(300); // the original, for 5 minutes
276
276
  await media.getTemporaryUrl(300, "thumb"); // a conversion
277
277
  ```
@@ -308,7 +308,7 @@ Pair `Storage.fake()` with `MediaFake`. The first asserts bytes landed, the
308
308
  second asserts a row points at them — a media row with no file and a file with
309
309
  no row are different bugs.
310
310
 
311
- ```ts
311
+ ```ts fragment
312
312
  import { Storage } from "zerotal/storage";
313
313
  import { MediaFake } from "zerotal/media";
314
314
 
@@ -350,7 +350,7 @@ Signatures below are the ones `packages/media/api-surface.md` records, which CI
350
350
 
351
351
  `Media` is the mixin — it reads as `Model.using(Media)`, and it declares the static `mediaCollections` field. `MediaItem` is one stored file: a row in the `media` table, and an ordinary model, so every query-builder method is available on it too.
352
352
 
353
- ```ts
353
+ ```ts fragment
354
354
  function Media<TBase extends Constructor>(
355
355
  Base: TBase,
356
356
  ): TBase & { mediaCollections: MediaCollections };
@@ -382,7 +382,7 @@ Columns: `uuid`, `name`, `fileName`, `mimeType`, `size`, `disk`, `conversionsDis
382
382
 
383
383
  `addMedia(source)` returns a `MediaAdder`. Every method chains; `toCollection()` is what actually stores the file, and it returns the `MediaItem`.
384
384
 
385
- ```ts
385
+ ```ts fragment
386
386
  class MediaAdder {
387
387
  usingName(name: string): MediaAdder;
388
388
  usingFileName(fileName: string): MediaAdder;
@@ -395,7 +395,7 @@ class MediaAdder {
395
395
 
396
396
  `MediaSource` is what a source may be:
397
397
 
398
- ```ts
398
+ ```ts fragment
399
399
  type MediaSource = ArrayBuffer | Blob | UploadedFile | File | Uint8Array;
400
400
  ```
401
401
 
@@ -407,7 +407,7 @@ type MediaSource = ArrayBuffer | Blob | UploadedFile | File | Uint8Array;
407
407
 
408
408
  `MediaLibrary` is the facade; `MediaManager` is the class behind it. They are named differently because `Media` is already the mixin, and an app importing both would otherwise have to rename one at every call site.
409
409
 
410
- ```ts
410
+ ```ts fragment
411
411
  class MediaManager {
412
412
  readonly config: MediaConfigShape;
413
413
  readonly driver: ImageDriver;
@@ -431,7 +431,7 @@ interface CleanReport {
431
431
 
432
432
  ### Collections and conversions
433
433
 
434
- ```ts
434
+ ```ts fragment
435
435
  type MediaCollections = Record<string, CollectionDefinition | (() => CollectionDefinition)>;
436
436
  type ConversionMap = Record<string, ConversionDefinition>;
437
437
  ```
@@ -477,7 +477,7 @@ interface ResponsiveImageSet {
477
477
 
478
478
  `ImageDriver` is the seam between this package and whatever actually manipulates pixels. Two implementations ship — `BunImageDriver` (the default, no dependencies) and `SharpImageDriver` (opt-in, a native module) — and a shared parity suite holds them to the same output dimensions.
479
479
 
480
- ```ts
480
+ ```ts fragment
481
481
  interface ImageDriver {
482
482
  readonly name: string;
483
483
  /** Whether `fit: "cover"` is available. Both shipped drivers report `true`. */
@@ -519,7 +519,7 @@ Writing your own driver is supported, and the interface may grow only in ways th
519
519
 
520
520
  `BunImageDriver` takes pixel ceilings, so a decompression bomb fails as a refusal rather than as memory exhaustion. `SharpImageDriver` takes no arguments — libvips streams tiles rather than materialising the full bitmap, so the ceiling has nothing to protect.
521
521
 
522
- ```ts
522
+ ```ts fragment
523
523
  new BunImageDriver(maxPixels?, maxCropPixels?);
524
524
  new SharpImageDriver();
525
525
  ```
@@ -528,7 +528,7 @@ new SharpImageDriver();
528
528
 
529
529
  Three lookup tables are exported for reading — to label a download, or to check a type before offering an upload. They are frozen, because they are shared module state: an app that mutated one would change how conversions behave for every other caller in the process, including ones it does not own.
530
530
 
531
- ```ts
531
+ ```ts fragment
532
532
  const FORMAT_MIME: Readonly<Record<ConversionFormat, string>>;
533
533
  const FORMAT_EXTENSION: Readonly<Record<ConversionFormat, string>>;
534
534
  const CONVERTIBLE_MIME_TYPES: ReadonlySet<string>;
@@ -541,7 +541,7 @@ function isConvertible(mimeType: string | null | undefined): boolean;
541
541
 
542
542
  Supply a `PathGenerator` to change the on-disk layout described under [Where files live](#where-files-live). `DefaultPathGenerator` is the shipped one, and `setPathGenerator` installs yours. It is process-global, so a provider's `register()` is the place for it.
543
543
 
544
- ```ts
544
+ ```ts fragment
545
545
  interface PathGenerator {
546
546
  forOriginal(media: MediaItem): string;
547
547
  forConversions(media: MediaItem): string;
@@ -555,7 +555,7 @@ class DefaultPathGenerator implements PathGenerator {
555
555
  function setPathGenerator(generator: PathGenerator): void;
556
556
  ```
557
557
 
558
- ```ts
558
+ ```ts fragment
559
559
  // A provider's register()
560
560
  setPathGenerator(new DefaultPathGenerator("uploads"));
561
561
  ```
@@ -116,7 +116,7 @@ For middleware that must run on _every_ request, see [Global middleware](#global
116
116
  Pass an array of middleware classes as the fourth argument to any route
117
117
  registration method:
118
118
 
119
- ```ts
119
+ ```ts fragment
120
120
  // routes/index.ts
121
121
  Router.get("/dashboard", DashboardController, "index", [AuthMiddleware]);
122
122
  Router.post("/posts", PostController, "store", [AuthMiddleware, ThrottleMiddleware]);
@@ -124,7 +124,7 @@ Router.post("/posts", PostController, "store", [AuthMiddleware, ThrottleMiddlewa
124
124
 
125
125
  ### Route groups
126
126
 
127
- ```ts
127
+ ```ts fragment
128
128
  // routes/index.ts
129
129
  Router.group({ middleware: AuthMiddleware }, () => {
130
130
  Router.get("/dashboard", DashboardController, "index");
@@ -137,7 +137,7 @@ Router.group({ middleware: AuthMiddleware }, () => {
137
137
  Middleware classes under `app/middleware/` are auto-registered at boot as a **named group under
138
138
  their class name** — reference them by string in routes without importing:
139
139
 
140
- ```ts
140
+ ```ts fragment
141
141
  // app/middleware/EnsureSubscribed.ts → referenceable as "EnsureSubscribed"
142
142
  Router.group({ middleware: ["EnsureSubscribed"] }, () => {
143
143
  /* … */
@@ -151,13 +151,13 @@ global pipeline. See [Conventions](/docs/conventions#middleware-appmiddleware).
151
151
 
152
152
  Define a group once, reference it by name everywhere:
153
153
 
154
- ```ts
154
+ ```ts fragment
155
155
  // in a ServiceProvider.onRegister()
156
156
  Router.middlewareGroup("api", [ThrottleMiddleware, BearerTokenMiddleware]);
157
157
  Router.middlewareGroup("web", [SessionMiddleware, CsrfMiddleware]);
158
158
  ```
159
159
 
160
- ```ts
160
+ ```ts fragment
161
161
  // routes/index.ts
162
162
  Router.group({ prefix: "/api/v1", middleware: "api" }, () => {
163
163
  Router.resource("posts", PostController);
@@ -173,7 +173,7 @@ Router.group({ middleware: ["web", AuthMiddleware] }, () => {
173
173
  Drop a `_middleware.ts` file into any directory under your file routes folder.
174
174
  It applies to every route file in that directory and all subdirectories:
175
175
 
176
- ```ts
176
+ ```ts fragment
177
177
  // app/routes/admin/_middleware.ts
178
178
  import { AuthMiddleware } from "@zerotal/auth";
179
179
  import { AdminMiddleware } from "../../middleware/AdminMiddleware.ts";
@@ -188,7 +188,7 @@ you get `root/_middleware → admin/_middleware → route handler` in one pipeli
188
188
 
189
189
  Register middleware that runs on every request in `bootstrap/app.ts`:
190
190
 
191
- ```ts
191
+ ```ts fragment
192
192
  // bootstrap/app.ts
193
193
  export default Application.create({ providers }).use([
194
194
  DevtoolsInjectionMiddleware,
@@ -208,7 +208,7 @@ bakes options into a zero-argument class.
208
208
 
209
209
  ### CorsMiddleware
210
210
 
211
- ```ts
211
+ ```ts fragment
212
212
  // bootstrap/app.ts
213
213
  import { CorsMiddleware } from "zerotal";
214
214
 
@@ -240,7 +240,7 @@ app.use([CorsMiddleware.with({ origin: (o) => o.endsWith(".mycompany.com") })]);
240
240
  Rate-limits requests with an in-memory sliding window counter. Returns `429`
241
241
  with `Retry-After` and `X-RateLimit-*` headers when the limit is exceeded.
242
242
 
243
- ```ts
243
+ ```ts fragment
244
244
  // bootstrap/app.ts (global) and routes/index.ts (per-route)
245
245
  import { ThrottleMiddleware } from "zerotal";
246
246
 
@@ -295,7 +295,7 @@ RateLimiter.for("upload")
295
295
 
296
296
  Apply as route middleware with `RateLimiter.middleware(name)`:
297
297
 
298
- ```ts
298
+ ```ts fragment
299
299
  // routes/index.ts
300
300
  Router.post("/login", AuthController, "login", [RateLimiter.middleware("login")]);
301
301
 
@@ -314,7 +314,7 @@ Router.group({ prefix: "/api", middleware: [RateLimiter.middleware("api")] }, ()
314
314
  Check or reset a limiter manually — e.g. clear failed login attempts after a
315
315
  successful sign-in:
316
316
 
317
- ```ts
317
+ ```ts fragment
318
318
  // in a controller action — `ctx` is the HttpContext the action receives
319
319
  if (await RateLimiter.tooManyAttempts("login", ctx)) {
320
320
  return ctx.json({ message: "Too Many Requests" }, 429);
@@ -333,7 +333,7 @@ RateLimiter.resetFor("login", ctx); // clear the counter for this actor
333
333
  Adds `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, and a
334
334
  basic `Content-Security-Policy` to every response:
335
335
 
336
- ```ts
336
+ ```ts fragment
337
337
  // bootstrap/app.ts
338
338
  import { SecureHeadersMiddleware } from "zerotal";
339
339
  app.use([SecureHeadersMiddleware]);
@@ -358,7 +358,7 @@ embeddable stays that way.
358
358
 
359
359
  Verifies HMAC-SHA256 signatures on incoming webhook requests:
360
360
 
361
- ```ts
361
+ ```ts fragment
362
362
  // routes/index.ts
363
363
  import { WebhookMiddleware } from "zerotal";
364
364
 
@@ -379,7 +379,7 @@ Router.post("/webhooks/stripe", StripeController, "handle", [
379
379
  Decorate the class with `@inject(...)`, listing its dependency tokens in
380
380
  constructor order; the container resolves them and passes them in:
381
381
 
382
- ```ts
382
+ ```ts fragment
383
383
  // app/middleware/AuditMiddleware.ts
384
384
  import { inject } from "zerotal";
385
385
  import type { Pipe, NextFn, HttpContext } from "zerotal";
@@ -423,7 +423,7 @@ lets a request **through**, and what it does when it **stops** one.
423
423
 
424
424
  **Test the stop first**, because it is the reason the middleware exists:
425
425
 
426
- ```typescript
426
+ ```typescript fragment
427
427
  // tests/http/middleware.test.ts
428
428
  import { test } from "bun:test";
429
429
  import { createApp } from "../helpers.ts";
@@ -441,7 +441,7 @@ test("a guest is turned away from a protected route", async () => {
441
441
  **Then prove it lets the right request through**, otherwise a middleware that
442
442
  rejects everything passes the first test perfectly:
443
443
 
444
- ```typescript
444
+ ```typescript fragment
445
445
  // tests/http/middleware.test.ts
446
446
  const res = await app.actingAs(user).get("/dashboard");
447
447
 
@@ -452,7 +452,7 @@ res.assertOk();
452
452
  Register a probe route in the `setup` callback — `createTestApp(bootstrap, setup)`
453
453
  runs it before the server starts, so the route compiles into the router:
454
454
 
455
- ```typescript
455
+ ```typescript fragment
456
456
  // tests/http/middleware.test.ts
457
457
  const app = await createApp(() => {
458
458
  Router.get("/probe", () => ({ locale: Context.get("locale") })).middleware([LocaleMiddleware]);
@@ -191,7 +191,7 @@ Additive sync leaves columns in place even after you delete them from a model, s
191
191
  database accumulates orphaned columns over time. To let Zerotal also **drop** columns that
192
192
  no model declares anymore, opt in explicitly with the object form:
193
193
 
194
- ```typescript
194
+ ```typescript fragment
195
195
  // config/database.ts
196
196
  export default DatabaseConfig({
197
197
  url: env("DATABASE_URL", "./database/db.sqlite"),
@@ -222,7 +222,7 @@ connection. Call them from inside `up()` / `down()`.
222
222
 
223
223
  ### Creating tables
224
224
 
225
- ```typescript
225
+ ```typescript fragment
226
226
  // inside a migration's up()
227
227
  await Schema.create("users", (table) => {
228
228
  table.increments("id"); // INTEGER PRIMARY KEY AUTOINCREMENT
@@ -244,7 +244,7 @@ await Schema.createIfNotExists("settings", (table) => {
244
244
 
245
245
  ### Modifying tables
246
246
 
247
- ```typescript
247
+ ```typescript fragment
248
248
  // inside a migration's up()
249
249
  await Schema.table("users", (table) => {
250
250
  table.string("role").default("user"); // ADD COLUMN
@@ -257,7 +257,7 @@ await Schema.table("users", (table) => {
257
257
 
258
258
  ### Other Schema methods
259
259
 
260
- ```typescript
260
+ ```typescript fragment
261
261
  // inside a migration
262
262
  await Schema.drop("users");
263
263
  await Schema.dropIfExists("temp_table");
@@ -310,7 +310,7 @@ column; SQL types reflect the SQLite mappings (other drivers use their native ty
310
310
 
311
311
  Chain modifiers on any column:
312
312
 
313
- ```typescript
313
+ ```typescript fragment
314
314
  // inside a Blueprint callback
315
315
  t.string("bio").nullable();
316
316
  t.string("role").default("user");
@@ -336,7 +336,7 @@ t.dateTime("published_at").nullable().useCurrent();
336
336
 
337
337
  ## Indexes
338
338
 
339
- ```typescript
339
+ ```typescript fragment
340
340
  // inside a migration's up()
341
341
  await Schema.create("posts", (table) => {
342
342
  table.increments("id");
@@ -363,7 +363,7 @@ await Schema.create("posts", (table) => {
363
363
 
364
364
  ## Foreign keys
365
365
 
366
- ```typescript
366
+ ```typescript fragment
367
367
  // inside a migration's up()
368
368
  await Schema.create("comments", (table) => {
369
369
  table.increments("id");
@@ -379,7 +379,7 @@ await Schema.create("comments", (table) => {
379
379
  `foreignId()` is shorthand for an unsigned integer column; chain `.constrained()` to
380
380
  add the foreign-key constraint, inferring the referenced table from the column name:
381
381
 
382
- ```typescript
382
+ ```typescript fragment
383
383
  // inside a Blueprint callback
384
384
  t.foreignId("user_id").constrained(); // references users.id
385
385
  t.foreignId("post_id").constrained("posts"); // explicit table
@@ -392,7 +392,7 @@ read more fluently.
392
392
 
393
393
  ## Soft deletes
394
394
 
395
- ```typescript
395
+ ```typescript fragment
396
396
  // inside a Blueprint callback
397
397
  t.softDeletes(); // adds nullable deleted_at TEXT column
398
398
  t.softDeletes("removed_at"); // custom column name
@@ -403,7 +403,7 @@ non-null `deleted_at` are excluded from queries automatically.
403
403
 
404
404
  ## Pivot / join tables
405
405
 
406
- ```typescript
406
+ ```typescript fragment
407
407
  // inside a migration's up()
408
408
  await Schema.create("post_tags", (table) => {
409
409
  table.integer("post_id");
@@ -418,7 +418,7 @@ await Schema.create("post_tags", (table) => {
418
418
 
419
419
  Check schema state before making changes to keep migrations idempotent:
420
420
 
421
- ```typescript
421
+ ```typescript fragment
422
422
  // inside a migration
423
423
  async up(): Promise<void> {
424
424
  if (await Schema.hasColumn("users", "role")) return;
@@ -456,7 +456,7 @@ free coverage, and it is why a broken migration rarely reaches production.
456
456
  moment. A migration is only reversible if you have run it backwards at least
457
457
  once:
458
458
 
459
- ```typescript
459
+ ```typescript fragment
460
460
  // tests/migrations/AddArchivedToPosts.test.ts
461
461
  import { test, expect } from "bun:test";
462
462
  import { Schema } from "@zerotal/orm";
@@ -480,7 +480,7 @@ second `up()` should either succeed or fail for a reason you have chosen.
480
480
  mistakes are unrecoverable. Arrange rows in the old shape, run the migration,
481
481
  assert the new shape:
482
482
 
483
- ```typescript
483
+ ```typescript fragment
484
484
  // tests/migrations/BackfillSlugs.test.ts
485
485
  test("backfills a slug for every existing post", async () => {
486
486
  await DB.table("posts").insert({ title: "Hello World", slug: null });