@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/monitor.md CHANGED
@@ -136,7 +136,7 @@ Everything is driven by core's `FrameworkEvents` bus, the same synchronous subst
136
136
 
137
137
  For anything that does not flow through the bus — a deploy marker, or a third-party call you make without the `Http` client — the `Monitor` facade records it directly and takes precedence. Every `Monitor.*` call is a safe no-op until the provider boots, so you can call it from anywhere without guarding.
138
138
 
139
- ```ts
139
+ ```ts fragment
140
140
  import { Monitor } from "@zerotal/monitor";
141
141
 
142
142
  Monitor.recordDeploy(gitSha);
@@ -187,7 +187,7 @@ export default MonitorConfig({
187
187
 
188
188
  The `auth` callback receives the authenticated user (or `undefined`) and returns whether to allow access. The default permits access only outside production, which is safe for local development but means **you must set an `auth` gate before deploying** — otherwise the panel, which exposes request payloads, user identities, and logs, is unreachable in production by default but wide open the moment you flip the environment. Gate it to an admin role:
189
189
 
190
- ```ts
190
+ ```ts fragment
191
191
  auth: (user) => user?.role === "admin",
192
192
  ```
193
193
 
@@ -197,7 +197,7 @@ The same gate protects the JSON export route. The Prometheus endpoint is deliber
197
197
 
198
198
  The panel evaluates threshold alerts every fifteen seconds against the live snapshot. Out of the box it watches four signals: the 5xx error rate, p95 latency, total pending jobs across queues, and transaction rollbacks. Tune the limits through `alertThresholds`:
199
199
 
200
- ```ts
200
+ ```ts fragment
201
201
  alertThresholds: { errorRatePct: 5, p95Ms: 2000, queuePending: 500, rolledBackInWindow: 10 },
202
202
  ```
203
203
 
@@ -205,7 +205,7 @@ Alerts are edge-triggered — each one fires once when it crosses its threshold
205
205
 
206
206
  When an alert fires it is logged, recorded so the Alerts tab can show it with its full context, and dispatched. There are two ways to be paged. The simplest is a webhook — set `alertWebhook` to a Slack-compatible URL and each firing is POSTed as JSON, no extra dependency. For richer routing, register a handler and wire it to notifications, PagerDuty, or anything else:
207
207
 
208
- ```ts
208
+ ```ts fragment
209
209
  import { onAlert } from "@zerotal/monitor";
210
210
 
211
211
  onAlert((alert) => {
@@ -268,7 +268,7 @@ The panel owns the shell, the time-range selector, the storage and the retention
268
268
 
269
269
  A section is _described_, not rendered. The contributor returns stats and tables; the panel draws them. That keeps the panel coherent no matter who contributed a section, and means the contributing package needs no JSX and no dependency on `@zerotal/monitor`:
270
270
 
271
- ```ts
271
+ ```ts fragment
272
272
  // In a contributing provider's onBooting()
273
273
  interface MonitorHost {
274
274
  enabled(id: string): boolean;
@@ -311,7 +311,7 @@ This is the other half of how the recorders already work. A package writes its m
311
311
 
312
312
  To keep a contributor installed but drop its section, name it in `sections`:
313
313
 
314
- ```ts
314
+ ```ts fragment
315
315
  // config/monitor.ts
316
316
  export default MonitorConfig({
317
317
  sections: { scheduler: false },
@@ -137,7 +137,7 @@ sends one real message and prints whatever the server said.
137
137
  Extend `Notification`, declare `channels()`, then implement a `to*()` method for
138
138
  each declared channel:
139
139
 
140
- ```ts
140
+ ```ts fragment
141
141
  // app/notifications/OrderShippedNotification.ts
142
142
  import { Notification, MailMessage } from "@zerotal/notifications";
143
143
  import type { Notifiable } from "@zerotal/notifications";
@@ -188,7 +188,7 @@ export class OrderShippedNotification extends Notification {
188
188
  `channels()` receives the recipient, so a single notification can respect each
189
189
  person's preferences instead of forcing every recipient down the same path:
190
190
 
191
- ```ts
191
+ ```ts fragment
192
192
  // app/notifications/OrderShippedNotification.ts
193
193
  channels(user: Notifiable) {
194
194
  // Everyone gets the inbox copy; how they're alerted is their choice.
@@ -202,7 +202,7 @@ common case, and `channels()` with no arguments stays valid.
202
202
  A recipient can also redirect an individual channel without any notification
203
203
  knowing about it, by implementing `routeNotificationFor`:
204
204
 
205
- ```ts
205
+ ```ts fragment
206
206
  // app/models/User.ts
207
207
  routeNotificationFor(channel: string) {
208
208
  // Invoices go to the billing contact; everything else to the usual address.
@@ -221,7 +221,7 @@ afterwards as a `NotificationDispatchError`. A Slack webhook returning a 500 doe
221
221
  not cost the recipient the email and the stored row that were declared alongside
222
222
  it.
223
223
 
224
- ```ts
224
+ ```ts fragment
225
225
  // in a controller or service
226
226
  try {
227
227
  await Notify.send(user, new OrderShippedNotification(order));
@@ -289,7 +289,7 @@ export class User extends AuthUser.using(Notifiable) {
289
289
  }
290
290
  ```
291
291
 
292
- ```ts
292
+ ```ts fragment
293
293
  // in a controller or service
294
294
  await user.notify(new OrderShippedNotification(order)); // send now
295
295
  await user.notifyLater(new OrderShippedNotification(order)); // queue for background
@@ -301,7 +301,7 @@ await user.markNotificationsAsRead();
301
301
 
302
302
  ### Via the Notify facade
303
303
 
304
- ```ts
304
+ ```ts fragment
305
305
  // in a controller or service
306
306
  import { Notify } from "@zerotal/notifications";
307
307
 
@@ -315,7 +315,7 @@ await Notify.queue(user, new OrderShippedNotification(order)); // queue for back
315
315
  independent: one failing does not stop the rest, and the errors are reported
316
316
  together at the end.
317
317
 
318
- ```ts
318
+ ```ts fragment
319
319
  // in a controller or service
320
320
  const admins = await User.where("role", "admin").get();
321
321
  await Notify.sendMany(admins, new LowStockNotification(product));
@@ -326,7 +326,7 @@ await Notify.sendMany(admins, new LowStockNotification(product));
326
326
  Some notifications go to a destination rather than a user — an on-call address, a
327
327
  webhook, a number typed into a form. `route()` takes the destinations directly:
328
328
 
329
- ```ts
329
+ ```ts fragment
330
330
  // in a controller or service
331
331
  await Notify.route({ mail: "ops@acme.test" }).notify(new DeployFinished(build));
332
332
 
@@ -343,7 +343,7 @@ normally declares only transport channels.
343
343
 
344
344
  ### Via the NotificationManager directly
345
345
 
346
- ```ts
346
+ ```ts fragment
347
347
  // in a service
348
348
  import { NotificationManager } from "@zerotal/notifications";
349
349
  import { Application } from "zerotal";
@@ -361,7 +361,7 @@ to the notifiable's `email`, so you rarely set `to()`. The mail channel is
361
361
  built-in and works on the `log` driver out of the box; switch to `smtp` or
362
362
  `resend` in `config/notifications.ts`.
363
363
 
364
- ```ts
364
+ ```ts fragment
365
365
  // in a Notification
366
366
  import { MailMessage } from "@zerotal/notifications";
367
367
  import type { Notifiable } from "@zerotal/notifications";
@@ -379,7 +379,7 @@ Attach files with `attach()` when you already have the bytes, or `attachFile()`
379
379
  to read one from disk. `embed()` places an image in the body rather than listing
380
380
  it as a download — reference it from your HTML as `cid:the-id`:
381
381
 
382
- ```ts
382
+ ```ts fragment
383
383
  // in a Notification
384
384
  async toMail(_n: Notifiable): Promise<MailMessage> {
385
385
  return (await new MailMessage()
@@ -416,7 +416,7 @@ Read and mark stored notifications through the [mixin helpers](#via-the-notifiab
416
416
  Inbox reads return the 100 most recent rows unless told otherwise. Pass a limit
417
417
  and offset to page, or `limit: 0` for everything:
418
418
 
419
- ```ts
419
+ ```ts fragment
420
420
  // in a controller
421
421
  const page = await user.notifications({ limit: 20, offset: 40 });
422
422
  const badge = await user.unreadNotificationCount(); // counts without loading rows
@@ -430,7 +430,7 @@ past an age threshold; add `--all` to include unread ones. Schedule it.
430
430
 
431
431
  Implement `toSlack(notifiable)` returning a `SlackMessage`:
432
432
 
433
- ```ts
433
+ ```ts fragment
434
434
  // in a Notification
435
435
  import type { SlackMessage } from "@zerotal/notifications";
436
436
 
@@ -453,7 +453,7 @@ channel raises a `NotificationChannelNotConfiguredError` naming the notification
453
453
  Implement `toSms(notifiable)` returning an `SmsMessage`. Supported drivers:
454
454
  `twilio` and `vonage`.
455
455
 
456
- ```ts
456
+ ```ts fragment
457
457
  // in a Notification
458
458
  import type { SmsMessage } from "@zerotal/notifications";
459
459
 
@@ -474,7 +474,7 @@ Push a notification to a connected client in real time via
474
474
  returning a `BroadcastMessage` (or a plain data object), and optionally
475
475
  `broadcastType()` to set the wire `type`:
476
476
 
477
- ```ts
477
+ ```ts fragment
478
478
  // app/notifications/InvoicePaid.ts
479
479
  import { Notification, BroadcastMessage } from "@zerotal/notifications";
480
480
  import type { Notifiable } from "@zerotal/notifications";
@@ -504,7 +504,7 @@ The broadcast channel works like this:
504
504
  `private-notifications.{id}` by default. Override per notifiable with
505
505
  `receivesBroadcastNotificationsOn(): string`:
506
506
 
507
- ```ts
507
+ ```ts fragment
508
508
  // app/models/User.ts
509
509
  class User extends Model {
510
510
  receivesBroadcastNotificationsOn() {
@@ -521,14 +521,14 @@ The broadcast channel works like this:
521
521
  stored notification.
522
522
  - **Authorize** the per-user channel in `routes/channels.ts`:
523
523
 
524
- ```ts
524
+ ```ts fragment
525
525
  // routes/channels.ts
526
526
  Broadcast.channel("notifications.[id]", (user, id) => String(user.id) === id);
527
527
  ```
528
528
 
529
529
  - **Client** (any Pusher-compatible client):
530
530
 
531
- ```ts
531
+ ```ts fragment
532
532
  // in your frontend
533
533
  Socket.private(`notifications.${userId}`).listen("notification", (n) => {
534
534
  console.log(n.type, n);
@@ -541,7 +541,7 @@ A broadcast goes out inline, which is the point of the channel. When one fans ou
541
541
  widely enough that the request should not wait for it, `.onQueue(name)` hands it
542
542
  to a worker instead — at the cost of arriving whenever that worker picks it up.
543
543
 
544
- ```ts
544
+ ```ts fragment
545
545
  // in a Notification
546
546
  toBroadcast(_n: Notifiable) {
547
547
  return new BroadcastMessage({ id: this.report.id }).onQueue("broadcasts");
@@ -554,7 +554,7 @@ The five built-in channels are registered the same way yours are, so adding a
554
554
  channel is not a special case. Register a factory under a name, and any
555
555
  notification can declare it:
556
556
 
557
- ```ts
557
+ ```ts fragment
558
558
  // app/providers/DiscordChannelProvider.ts
559
559
  import type { NotificationChannel, Notifiable } from "@zerotal/notifications";
560
560
 
@@ -576,7 +576,7 @@ const notifications = this.app.container.makeSync("notifications");
576
576
  notifications.extend("discord", () => new DiscordChannel());
577
577
  ```
578
578
 
579
- ```ts
579
+ ```ts fragment
580
580
  // app/notifications/DeployFinished.ts
581
581
  channels() {
582
582
  return ["database", "discord"];
@@ -612,7 +612,7 @@ notification you queue.
612
612
  `app/notifications/` are found automatically. One that lives elsewhere must
613
613
  register itself:
614
614
 
615
- ```ts
615
+ ```ts fragment
616
616
  // app/domain/billing/InvoiceOverdue.ts
617
617
  import { NotificationRegistry } from "@zerotal/notifications";
618
618
 
@@ -627,7 +627,7 @@ By default a notification's own enumerable fields are serialized, which covers a
627
627
  constructor that assigns plain values. When it holds something JSON cannot carry
628
628
  — a model instance, a `Map`, a closure — say how to shrink and rebuild it:
629
629
 
630
- ```ts
630
+ ```ts fragment
631
631
  // app/notifications/InvoicePaid.ts
632
632
  export class InvoicePaid extends Notification {
633
633
  constructor(private invoice: Invoice) {
@@ -649,7 +649,7 @@ export class InvoicePaid extends Notification {
649
649
  `NotificationFake` swaps the `"notifications"` container binding for an
650
650
  in-memory recorder, so assertions run without hitting any real channel:
651
651
 
652
- ```ts
652
+ ```ts fragment
653
653
  // tests/orders.test.ts
654
654
  import { NotificationFake } from "@zerotal/notifications";
655
655
  import { describe, it, beforeEach, afterEach } from "bun:test";
package/docs/orm/casts.md CHANGED
@@ -58,7 +58,7 @@ export class Post extends Model {
58
58
  `json` and `array` encode on write and parse on read, in both directions, so a value
59
59
  round-trips as the type you gave it — including a bare scalar:
60
60
 
61
- ```typescript
61
+ ```typescript fragment
62
62
  setting.value = "62812345678"; // stored as "62812345678", read back as a string
63
63
  setting.value = "051001"; // a branch code keeps its leading zero
64
64
  setting.value = { plan: "pro" }; // objects and arrays as you would expect
@@ -79,7 +79,7 @@ was a numeric string may come back as a number; coerce on read where it matters.
79
79
  Reads and writes the value as a string with exactly `N` decimal places. Useful
80
80
  for currency, where you want to avoid floating-point drift:
81
81
 
82
- ```typescript
82
+ ```typescript fragment
83
83
  // app/models/Product.ts
84
84
  @column({ type: "number", cast: "decimal:2" }) price!: string;
85
85
  // DB stores "9.99" — the model reads it back as the string "9.99".
@@ -94,7 +94,7 @@ for currency, where you want to avoid floating-point drift:
94
94
  Behaves like `'datetime'` on read (constructs a [Carbon](/docs/carbon)) and
95
95
  serializes to an ISO 8601 string on write:
96
96
 
97
- ```typescript
97
+ ```typescript fragment
98
98
  // app/models/Booking.ts
99
99
  @column({ type: "datetime", cast: "immutable_datetime" }) lockedAt?: Carbon;
100
100
 
@@ -112,7 +112,7 @@ Stores and retrieves the raw enum value (the underlying string or number);
112
112
  TypeScript narrows the property type. The cast itself is a pass-through, so pair
113
113
  it with `enumValues` to document the enum:
114
114
 
115
- ```typescript
115
+ ```typescript fragment
116
116
  // app/models/Post.ts
117
117
  enum Status {
118
118
  Draft = "draft",
@@ -132,7 +132,7 @@ The column stores an opaque AES-256-GCM payload keyed by `APP_KEY`; the property
132
132
  holds the value you assigned. Nothing in between — your code, validation,
133
133
  `$dirty` — has to know:
134
134
 
135
- ```typescript
135
+ ```typescript fragment
136
136
  // app/models/Client.ts
137
137
  @column("encrypted", { nullable: true }) idNumber?: string;
138
138
 
@@ -150,7 +150,7 @@ that ciphertext outgrows its plaintext.
150
150
 
151
151
  For several columns at once, list them instead — it means exactly the same thing:
152
152
 
153
- ```typescript
153
+ ```typescript fragment
154
154
  class Client extends BaseModel {
155
155
  static encryptable = ["idNumber", "passportNumber"];
156
156
  }
@@ -160,7 +160,7 @@ A column in that list whose `@column({ type })` is `json` encrypts as
160
160
  `encrypted:json` automatically, so the structure survives the round trip rather
161
161
  than reaching the cipher as `"[object Object]"`.
162
162
 
163
- Unlike [`hashable`](/docs/orm/index), this is reversible and does not touch the
163
+ Unlike [`hashable`](/docs/orm), this is reversible and does not touch the
164
164
  instance: after `save()`, `client.idNumber` still reads as the plaintext you set.
165
165
  `$dirty` therefore compares plaintext, and an unchanged column is not rewritten
166
166
  with a fresh IV on every unrelated save.
@@ -195,7 +195,7 @@ one, and re-encrypt it on the next save, losing the original for good.
195
195
  Pass an object with `get` and `set` functions for complete control over
196
196
  serialization:
197
197
 
198
- ```typescript
198
+ ```typescript fragment
199
199
  // app/models/Place.ts
200
200
  interface GeoPoint { lat: number; lng: number }
201
201
 
@@ -232,7 +232,7 @@ export class MoneyCast extends Cast<number> {
232
232
  }
233
233
  ```
234
234
 
235
- ```typescript
235
+ ```typescript fragment
236
236
  // app/models/Invoice.ts
237
237
  import { column } from "@zerotal/orm";
238
238
  import { MoneyCast } from "../casts/MoneyCast.ts";
@@ -243,7 +243,7 @@ import { MoneyCast } from "../casts/MoneyCast.ts";
243
243
  For JSON columns the ORM ships ready-made helpers that optionally hydrate the
244
244
  parsed value into a class:
245
245
 
246
- ```typescript
246
+ ```typescript fragment
247
247
  // app/models/Customer.ts
248
248
  import { column } from "@zerotal/orm";
249
249
  import { json, objectOf, arrayOf } from "@zerotal/orm";
@@ -263,7 +263,7 @@ import { Address } from "../value-objects/Address.ts";
263
263
  An alternative to `@column()` for columns you don't declare directly (e.g. from
264
264
  an external schema, a view, or a generated table):
265
265
 
266
- ```typescript
266
+ ```typescript fragment
267
267
  // app/models/Post.ts
268
268
  @table("posts")
269
269
  export class Post extends Model {
@@ -300,7 +300,7 @@ does not mark the column dirty and won't be persisted on the next `save()`.
300
300
  Enable `reactiveCasts` to make `json` and `array` columns use a reactive proxy
301
301
  that tracks deep mutations:
302
302
 
303
- ```typescript
303
+ ```typescript fragment
304
304
  // app/models/Post.ts
305
305
  @table("posts")
306
306
  export class Post extends Model {
@@ -318,7 +318,7 @@ await post.save(); // persists the updated meta
318
318
 
319
319
  Without `reactiveCasts`, replace the whole value to ensure dirty tracking:
320
320
 
321
- ```typescript
321
+ ```typescript fragment
322
322
  // in a controller
323
323
  post.meta = { ...post.meta, views: (post.meta.views as number) + 1 };
324
324
  await post.save();
@@ -359,7 +359,7 @@ Cast options accepted by `@column()`:
359
359
 
360
360
  ## Next steps
361
361
 
362
- - [ORM](/docs/orm/index) — defining models and columns.
362
+ - [ORM](/docs/orm) — defining models and columns.
363
363
  - [Queries](/docs/orm/queries) — the query builder and scopes.
364
364
  - [Serialization](/docs/orm/serialization) — control JSON output.
365
365
  - [Carbon](/docs/carbon) — the date type behind `datetime` casts.
@@ -37,7 +37,7 @@ This writes `database/factories/PostFactory.ts`. A factory is `Factory.define(Mo
37
37
  definition)`, where the definition callback receives the built-in [`fake`](#the-fake-helper)
38
38
  helper and returns the model's default attributes:
39
39
 
40
- ```typescript
40
+ ```typescript fragment
41
41
  // database/factories/PostFactory.ts
42
42
  import { Factory } from "@zerotal/testing";
43
43
  import { Post } from "../../app/models/Post.ts";
@@ -56,7 +56,7 @@ supply them at create time with [`.for()`](#relating-models) or an override.
56
56
 
57
57
  ## Creating records
58
58
 
59
- ```typescript
59
+ ```typescript fragment
60
60
  // in a test or seeder
61
61
  // Persist one record → Promise<Post>
62
62
  const post = await PostFactory.create();
@@ -93,7 +93,7 @@ wins).
93
93
  `.for(parent)` injects the parent's primary key as a foreign key, derived from the
94
94
  parent's class name (`User` → `userId`):
95
95
 
96
- ```typescript
96
+ ```typescript fragment
97
97
  // in a test or seeder
98
98
  const user = await UserFactory.create();
99
99
 
@@ -114,7 +114,7 @@ base factory stays reusable.
114
114
  Force the created instance into a model state via `forceState()` (bypassing guards
115
115
  and transition callbacks — see [Lifecycle & Events](/docs/orm/lifecycle)):
116
116
 
117
- ```typescript
117
+ ```typescript fragment
118
118
  // in a test or seeder
119
119
  const expired = await SubscriptionFactory.state("expired").create();
120
120
  ```
@@ -123,7 +123,7 @@ const expired = await SubscriptionFactory.state("expired").create();
123
123
 
124
124
  Run logic after each instance is saved — e.g. attaching related records:
125
125
 
126
- ```typescript
126
+ ```typescript fragment
127
127
  // in a test or seeder
128
128
  const user = await UserFactory.afterCreate(async (u) => {
129
129
  await PostFactory.for(u).count(3).create();
@@ -138,7 +138,7 @@ needs the full lifecycle to fire:
138
138
  > **Note** — Suppressing hooks keeps seeding side-effect free (no logs, emails, or
139
139
  > queued jobs). Call `dispatchEvents()` to let observers and hooks run.
140
140
 
141
- ```typescript
141
+ ```typescript fragment
142
142
  // in a test
143
143
  // Silent — no "user registered" side effects:
144
144
  await UserFactory.count(20).create();
@@ -150,7 +150,7 @@ Queue.assertDispatched(WelcomeEmailJob);
150
150
 
151
151
  ## In seeders
152
152
 
153
- ```typescript
153
+ ```typescript fragment
154
154
  // database/seeders/DatabaseSeeder.ts
155
155
  import { Seeder } from "@zerotal/orm";
156
156
  import { UserFactory } from "../factories/UserFactory.ts";
@@ -172,7 +172,7 @@ See [Seeding](/docs/seeding) for running seeders.
172
172
 
173
173
  Factories are the standard way to arrange database state in a test:
174
174
 
175
- ```typescript
175
+ ```typescript fragment
176
176
  // in a test
177
177
  import { UserFactory } from "../../database/factories/UserFactory.ts";
178
178
 
package/docs/orm/index.md CHANGED
@@ -59,7 +59,9 @@ import { DatabaseConfig } from "@zerotal/orm";
59
59
  import { env } from "zerotal";
60
60
 
61
61
  export default DatabaseConfig({
62
- driver: env("DB_DRIVER", "sqlite"), // 'sqlite' | 'postgres' | 'mysql'
62
+ // One of 'sqlite' | 'postgres' | 'mysql', written literally: the field is that
63
+ // union, and `env()` returns a plain string. Scaffolding writes your choice here.
64
+ driver: "sqlite",
63
65
  url: env("DATABASE_URL", "./database/db.sqlite"),
64
66
 
65
67
  // PostgreSQL: 'postgres://user:pass@localhost:5432/mydb'
@@ -101,7 +103,7 @@ export default DatabaseConfig({
101
103
 
102
104
  Every model is configured with the `@table()` decorator. It accepts a fluent chain or an options object — both styles are equivalent:
103
105
 
104
- ```typescript
106
+ ```typescript fragment
105
107
  // app/models/Post.ts
106
108
  import { Model, column, table } from "@zerotal/orm";
107
109
 
@@ -143,7 +145,7 @@ Timestamps are **on by default**, so `@table("ledger")` alone still writes `crea
143
145
  `updated_at`. For an append-only table whose migration creates neither column, say so —
144
146
  otherwise the first save fails with `table ledger has no column named updated_at`:
145
147
 
146
- ```typescript
148
+ ```typescript fragment
147
149
  @(table("ledger").withoutTimestamps())
148
150
  export class LedgerEntry extends Model {
149
151
  /* … */
@@ -173,7 +175,7 @@ export class LedgerEntry extends Model {
173
175
 
174
176
  Declare typed columns. Accepts a shorthand cast string or a full options object:
175
177
 
176
- ```typescript
178
+ ```typescript fragment
177
179
  // in a model class body
178
180
  import { column } from "@zerotal/orm";
179
181
 
@@ -220,7 +222,7 @@ fails at the first `.diffForHumans()` or arithmetic.
220
222
 
221
223
  Declare constraints on the column and schema generation emits them, so `migrate:generate` produces a schema with the guarantees your application depends on rather than a bare set of columns:
222
224
 
223
- ```typescript
225
+ ```typescript fragment
224
226
  @column({ unique: true }) idempotencyKey!: string; // unique index
225
227
  @column({ index: true }) status!: string; // plain index
226
228
  ```
@@ -233,7 +235,7 @@ Reusable model behaviour ships as **mixins** — soft deletes, state machines, t
233
235
  roles, permissions, notifications, tenancy, auditing. A model opts into the ones it wants with the
234
236
  `Model.using(...)` static, so a model that does not use a feature does not carry its API:
235
237
 
236
- ```typescript
238
+ ```typescript fragment
237
239
  import { Model, SoftDeletes } from "@zerotal/orm";
238
240
  import { Authenticatable, Roles, Permissions } from "@zerotal/auth";
239
241
 
@@ -274,7 +276,7 @@ The mixins the framework ships:
274
276
  `using` composes onto whatever class you call it on, not onto `Model` specifically, so an
275
277
  app-level base model can carry its own configuration and still take mixins:
276
278
 
277
- ```typescript
279
+ ```typescript fragment
278
280
  class AppModel extends Model {
279
281
  static override primaryKey = "uuid";
280
282
  }
@@ -324,7 +326,7 @@ in `ctx.body()`. To allow columns through, you opt in explicitly.
324
326
  Any attribute not permitted by the active list throws `MassAssignmentError` (it is
325
327
  **not** silently dropped), so a mistake surfaces loudly instead of quietly failing:
326
328
 
327
- ```typescript
329
+ ```typescript fragment
328
330
  // app/models/Post.ts
329
331
  @table("posts")
330
332
  export class Post extends Model {
@@ -342,7 +344,7 @@ export class Post extends Model {
342
344
 
343
345
  Use the `Columns<T>` utility for compile-time safety against typos:
344
346
 
345
- ```typescript
347
+ ```typescript fragment
346
348
  // app/models/Post.ts
347
349
  import type { Columns } from "@zerotal/orm";
348
350
 
@@ -353,7 +355,7 @@ static fillable: Columns<Post>[] = ["title", "body", "status"];
353
355
  Declare `fillable` as a literal tuple (`as const`) and `create()` narrows its payload to
354
356
  exactly those columns:
355
357
 
356
- ```typescript
358
+ ```typescript fragment
357
359
  @table("customers")
358
360
  export class Customer extends Model {
359
361
  static fillable = ["name", "email"] as const;
@@ -378,7 +380,7 @@ mistake from a runtime `MassAssignmentError` to a compile error.
378
380
  For data you construct yourself (seeders, factories, framework-internal writes)
379
381
  the guard is just friction. Bypass it deliberately:
380
382
 
381
- ```typescript
383
+ ```typescript fragment
382
384
  // Per call — skip the guard for one write:
383
385
  role.forceFill({ name, guard });
384
386
  await Role.forceCreate({ name, guard });
@@ -407,7 +409,7 @@ An explicit `fillable` / `guarded` list is always honoured, even under a global
407
409
 
408
410
  ### fill instance method
409
411
 
410
- ```typescript
412
+ ```typescript fragment
411
413
  // in a controller
412
414
  post.fill({ title: "New title", body: "Updated body" }); // throws if a key isn't fillable
413
415
  await post.save();
@@ -421,7 +423,7 @@ await post.save();
421
423
 
422
424
  Fields listed in `hashable` are automatically hashed with `Bun.password.hash()` (bcrypt) before every `INSERT`, and on `UPDATE` only when the value has changed since the last load:
423
425
 
424
- ```typescript
426
+ ```typescript fragment
425
427
  // app/models/User.ts
426
428
  @table("users")
427
429
  export class User extends Model {
@@ -443,14 +445,14 @@ const ok = await Bun.password.verify(candidate, user.password);
443
445
 
444
446
  `dispatchesEvents` connects ORM lifecycle hooks to the application event bus without wiring every observer manually. Declare a map from lifecycle event names to event classes:
445
447
 
446
- ```typescript
448
+ ```typescript fragment
447
449
  // app/events/UserCreated.ts
448
450
  export class UserCreated {
449
451
  constructor(public user: User) {}
450
452
  }
451
453
  ```
452
454
 
453
- ```typescript
455
+ ```typescript fragment
454
456
  // app/models/User.ts
455
457
  @table("users")
456
458
  export class User extends Model {
@@ -466,7 +468,7 @@ export class User extends Model {
466
468
 
467
469
  Each event class is constructed with the model instance as its first argument and emitted on the container's event bus (a no-op when no bus is bound, so it's safe in standalone ORM use). Subscribe anywhere:
468
470
 
469
- ```typescript
471
+ ```typescript fragment
470
472
  // app/listeners/sendWelcome.ts
471
473
  import { Events } from "zerotal";
472
474
  import { UserCreated } from "#app/events/UserCreated.ts";
@@ -506,7 +508,7 @@ export class Post extends Model {
506
508
 
507
509
  ## Full model example
508
510
 
509
- ```typescript
511
+ ```typescript fragment
510
512
  // app/models/Post.ts
511
513
  import { Model, column, table, hasMany, belongsTo } from "@zerotal/orm";
512
514
  import type { Columns } from "@zerotal/orm";
@@ -557,7 +559,7 @@ worth knowing: `UserFactory.create()` writes the row without firing `creating`,
557
559
  jobs. A test asserting on a side-effect of creation therefore sees nothing —
558
560
  and reads as a bug in your observer rather than in the test.
559
561
 
560
- ```typescript
562
+ ```typescript fragment
561
563
  // tests/models/User.test.ts
562
564
  import { test } from "bun:test";
563
565
  import { QueueFake } from "@zerotal/queue";
@@ -578,7 +580,7 @@ test("creating a user queues the welcome email", async () => {
578
580
  them against the model directly — it is faster and the failure points at the
579
581
  right line:
580
582
 
581
- ```typescript
583
+ ```typescript fragment
582
584
  // tests/models/Post.test.ts
583
585
  import { test, expect } from "bun:test";
584
586
  import { PostFactory } from "../../database/factories/PostFactory.ts";
@@ -598,7 +600,7 @@ test("the published scope excludes drafts", async () => {
598
600
  ambiguous. Assert on both sides — gone from the normal query, present with
599
601
  `withTrashed()`:
600
602
 
601
- ```typescript
603
+ ```typescript fragment
602
604
  // tests/models/Post.test.ts
603
605
  await post.delete();
604
606