@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/tenancy.md CHANGED
@@ -32,7 +32,7 @@ The package owns the `tenants` registry and the `tenant_members` pivot tables
32
32
 
33
33
  Supply the config with `TenancyProvider.withConfig(...)`, then add the provider to the array in `bootstrap/providers.ts`:
34
34
 
35
- ```typescript
35
+ ```typescript fragment
36
36
  // bootstrap/providers.ts
37
37
  import { TenancyProvider } from "@zerotal/tenancy";
38
38
  import tenancyConfig from "../config/tenancy.ts";
@@ -141,7 +141,7 @@ import { RouteParamResolver } from "@zerotal/tenancy";
141
141
  new RouteParamResolver({ param: "tenancy" }); // default param name
142
142
  ```
143
143
 
144
- ```typescript
144
+ ```typescript fragment
145
145
  // bootstrap/app.ts
146
146
  .fileBasedRouting({
147
147
  dir: basePath("app/routes"),
@@ -178,7 +178,7 @@ Requests with no authenticated user — login, signup, a public marketing page
178
178
  resolve to `null` here, and with `strict: false` (the default) fall through to
179
179
  the default connection rather than failing.
180
180
 
181
- ```typescript
181
+ ```typescript fragment
182
182
  // config/tenancy.ts — a non-default column
183
183
  new AuthResolver({ column: "organisationId" });
184
184
  ```
@@ -208,28 +208,21 @@ export class CookieResolver implements TenantResolver {
208
208
 
209
209
  `TenancyMiddleware` resolves the tenant and opens the [TenantContext](#tenantcontext-access-the-tenant-anywhere) boundary. Apply it globally or to specific route groups.
210
210
 
211
- ### Global
212
-
213
- With `TenancyProvider` already registered, add the middleware to the global pipeline with `.use()` in `bootstrap/app.ts`:
211
+ Registering `TenancyProvider` is the whole of it — the provider adds the resolving
212
+ middleware to the global pipeline itself, so every request has a tenant boundary
213
+ opened before it reaches a route:
214
214
 
215
- ```typescript
215
+ ```typescript fragment
216
216
  // bootstrap/app.ts
217
- import { TenancyMiddleware } from "@zerotal/tenancy";
217
+ import { TenancyProvider } from "@zerotal/tenancy";
218
218
 
219
- Application.create({ providers }).use([TenancyMiddleware]);
219
+ Application.create({ providers: [TenancyProvider] });
220
220
  ```
221
221
 
222
- ### Route group only
223
-
224
- ```typescript
225
- // routes/web.ts
226
- import { TenancyMiddleware } from "@zerotal/tenancy";
227
-
228
- Router.group({ domain: "{tenant}.myapp.com" }, () => {
229
- Router.get("/dashboard", DashboardController, "index");
230
- Router.get("/settings", SettingsController, "index");
231
- }).use([TenancyMiddleware]);
232
- ```
222
+ `TenancyMiddleware` is deliberately not exported: there is no arrangement in which
223
+ an app registers it by hand, and one registered twice would resolve the tenant
224
+ twice. To _require_ a tenant on particular routes, reach for
225
+ `EnsureTenancyMiddleware` below.
233
226
 
234
227
  The middleware throws an [error](/docs/errors) the framework's exception handler renders by content negotiation:
235
228
 
@@ -247,21 +240,22 @@ one. Use it on routes that make no sense without a tenant — it throws
247
240
  `TenantNotFoundError` when the context is empty rather than letting a query run
248
241
  unscoped:
249
242
 
250
- ```typescript
243
+ ```typescript fragment
251
244
  // routes/web.ts
252
- import { TenancyMiddleware, EnsureTenancyMiddleware } from "@zerotal/tenancy";
245
+ import { EnsureTenancyMiddleware } from "@zerotal/tenancy";
253
246
 
254
- Router.group({ middleware: [TenancyMiddleware, EnsureTenancyMiddleware] }, () => {
247
+ Router.group({ middleware: [EnsureTenancyMiddleware] }, () => {
255
248
  Router.get("/billing", BillingController, "index");
256
249
  });
257
250
  ```
258
251
 
259
- Register it after `TenancyMiddleware`, never instead of it: on its own there is
260
- nothing to resolve, so every request fails.
252
+ It runs after the global resolving middleware, which the provider has already
253
+ registered, so by the time this asserts there is a tenant the attempt to find one
254
+ has been made.
261
255
 
262
256
  ## ORM scoping — Tenantable
263
257
 
264
- Compose the `Tenantable` mixin via [`Model.using`](/docs/orm/index#composing-model-mixins) onto any model that belongs to a tenant. Two things happen automatically:
258
+ Compose the `Tenantable` mixin via [`Model.using`](/docs/orm#composing-model-mixins) onto any model that belongs to a tenant. Two things happen automatically:
265
259
 
266
260
  1. Every query on the model receives `WHERE tenant_id = <current tenant id>` (skipped outside a tenant context).
267
261
  2. `create()` / `save()` inject `tenant_id` on new records, so you never accidentally write cross-tenant data.
@@ -278,7 +272,7 @@ export class Project extends Model.using(Tenantable) {
278
272
  }
279
273
  ```
280
274
 
281
- ```typescript
275
+ ```typescript fragment
282
276
  // in a controller — inside a TenancyMiddleware boundary (tenant id = 7):
283
277
  const projects = await Project.all();
284
278
  // → SELECT * FROM projects WHERE tenant_id = 7
@@ -312,14 +306,14 @@ export class Invoice extends Model.using(Tenantable) {
312
306
 
313
307
  ### Single query
314
308
 
315
- ```typescript
309
+ ```typescript fragment
316
310
  // in an admin controller — load every project across all tenants:
317
311
  const all = await Project.query().withoutTenancy().get();
318
312
  ```
319
313
 
320
314
  `withoutTenancy()` removes only the tenant scope. To strip every global scope instead:
321
315
 
322
- ```typescript
316
+ ```typescript fragment
323
317
  // in a controller
324
318
  const all = await Project.query().withoutGlobalScopes().get();
325
319
  ```
@@ -328,7 +322,7 @@ const all = await Project.query().withoutGlobalScopes().get();
328
322
 
329
323
  Wrap the work in a tenant boundary with `TenantContext.run()` (or `Tenant.run()` via the facade) to scope every query inside it to a specific tenant:
330
324
 
331
- ```typescript
325
+ ```typescript fragment
332
326
  // app/jobs/SendReminders.ts
333
327
  import { TenantContext, Tenant } from "@zerotal/tenancy";
334
328
 
@@ -375,7 +369,7 @@ if (Tenant.check()) {
375
369
 
376
370
  Controllers also receive the resolved tenant directly on the request context:
377
371
 
378
- ```typescript
372
+ ```typescript fragment
379
373
  // in a controller
380
374
  async index(ctx: HttpContext): Promise<void> {
381
375
  const tenant = (ctx as any).tenant as Tenant;
@@ -387,7 +381,7 @@ async index(ctx: HttpContext): Promise<void> {
387
381
 
388
382
  The package maintains a `tenant_members` pivot linking your authenticated [User](/docs/authentication) model to tenants, with an admin flag. `Tenancy.create()` makes the current user the first admin; `update()` and `delete()` are admin-gated (use the `force*` variants to bypass the check from trusted server code).
389
383
 
390
- ```typescript
384
+ ```typescript fragment
391
385
  // in a controller
392
386
  import { Tenant } from "@zerotal/tenancy";
393
387
 
@@ -403,7 +397,7 @@ await Tenant.update({ name: "Acme Corp" }); // throws TenantForbiddenError unles
403
397
 
404
398
  `tenantDisk()` wraps any [storage](/docs/storage) disk and prefixes every path with `tenants/<slug>/` for the active tenant:
405
399
 
406
- ```typescript
400
+ ```typescript fragment
407
401
  // in a controller — inside a tenant boundary (slug = 'acme')
408
402
  import { tenantDisk } from "@zerotal/tenancy";
409
403
 
@@ -422,7 +416,7 @@ await tenantDisk("s3").put("reports/q4.pdf", pdf);
422
416
  A key that tries to climb out of its tenant's folder is rejected with
423
417
  `TenantStoragePathError` before it reaches the driver:
424
418
 
425
- ```typescript
419
+ ```typescript fragment
426
420
  await tenantDisk().put("../other-tenant/secrets.txt", buffer);
427
421
  // throws TenantStoragePathError
428
422
  ```
@@ -437,7 +431,7 @@ the exception handler renders it like any other.
437
431
 
438
432
  `tenantCache()` wraps the [cache](/docs/cache) and prefixes every key with `tenant:<slug>:`:
439
433
 
440
- ```typescript
434
+ ```typescript fragment
441
435
  // in a controller — inside a tenant boundary (slug = 'acme')
442
436
  import { tenantCache } from "@zerotal/tenancy";
443
437
 
@@ -467,7 +461,7 @@ export default TenancyConfig({
467
461
  });
468
462
  ```
469
463
 
470
- ```typescript
464
+ ```typescript fragment
471
465
  // in a controller — Project.all() automatically uses the active tenant's connection:
472
466
  const projects = await Project.all();
473
467
  ```
@@ -494,7 +488,7 @@ Set your suite up once as described in [Testing](/docs/testing). Tenancy has one
494
488
  test that matters more than all the others: **a tenant must not see another
495
489
  tenant's rows.** Write it first, and write it as a negative.
496
490
 
497
- ```typescript
491
+ ```typescript fragment
498
492
  // tests/tenancy/isolation.test.ts
499
493
  import { test, expect } from "bun:test";
500
494
  import { Tenancy, TenantContext } from "@zerotal/tenancy";
@@ -523,7 +517,7 @@ when the query returns everything but that one title.
523
517
  broken resolver serves the wrong tenant's data correctly, which no isolation test
524
518
  catches:
525
519
 
526
- ```typescript
520
+ ```typescript fragment
527
521
  // tests/tenancy/resolver.test.ts
528
522
  const res = await app.get("/dashboard", { Host: "acme.example.test" });
529
523
 
@@ -534,7 +528,7 @@ expect(res.json().tenant).toBe("acme");
534
528
  tenant and throws leaves it set, and the next test passes or fails for reasons
535
529
  that have nothing to do with it:
536
530
 
537
- ```typescript
531
+ ```typescript fragment
538
532
  // tests/tenancy/isolation.test.ts
539
533
  afterEach(() => {
540
534
  expect(TenantContext.tryGet()).toBeUndefined(); // catches a leak at its source
@@ -601,7 +595,7 @@ Resolved from the container binding `"tenancy"`; the `Tenant` value is the facad
601
595
 
602
596
  ## Next steps
603
597
 
604
- - [ORM](/docs/orm/index) — how `Tenantable` composes onto your models via `Model.using`.
598
+ - [ORM](/docs/orm) — how `Tenantable` composes onto your models via `Model.using`.
605
599
  - [Storage](/docs/storage) — the disks `tenantDisk()` wraps.
606
600
  - [Cache](/docs/cache) — the stores `tenantCache()` wraps.
607
601
  - [Middleware](/docs/middleware) — where `TenancyMiddleware` runs in the pipeline.
@@ -37,7 +37,7 @@ bunx playwright install --with-deps
37
37
  Add a `playwright.config.ts` that boots your app with the `webServer` option, so the
38
38
  server starts once for the whole run and is torn down after:
39
39
 
40
- ```typescript
40
+ ```typescript fragment
41
41
  // playwright.config.ts
42
42
  import { defineConfig, devices } from "@playwright/test";
43
43
 
@@ -81,18 +81,18 @@ Wire up scripts in `package.json`:
81
81
  Specs live in `e2e/` and use Playwright's `test`/`expect`. Drive the page through
82
82
  roles and assert on what the user sees:
83
83
 
84
- ```typescript
84
+ ```typescript fragment
85
85
  // e2e/navigate.e2e.ts
86
86
  import { test, expect } from "@playwright/test";
87
87
 
88
88
  test.describe("SPA navigation", () => {
89
89
  test("swaps content without a full page reload", async ({ page }) => {
90
- await page.goto("/pulse/counter");
90
+ await page.goto("/demo/counter");
91
91
  // A full reload would clear this marker; an SPA swap keeps it.
92
92
  await page.evaluate(() => ((window as any).__spa = true));
93
93
 
94
94
  await page.getByRole("link", { name: "Components" }).click();
95
- await expect(page).toHaveURL(/\/pulse\/components/);
95
+ await expect(page).toHaveURL(/\/demo\/components/);
96
96
  await expect(page.getByRole("heading", { name: "Native Components" })).toBeVisible();
97
97
 
98
98
  const kept = await page.evaluate(() => (window as any).__spa);
@@ -112,7 +112,7 @@ Flow pages finish wiring once Alpine has walked the DOM and fired
112
112
  `alpine:initialized`. A fast click can land before that and silently no-op (a flaky,
113
113
  browser-dependent failure). Wait for readiness before interacting:
114
114
 
115
- ```typescript
115
+ ```typescript fragment
116
116
  // e2e/support/gotoReady.ts
117
117
  import type { Page } from "@playwright/test";
118
118
 
@@ -128,7 +128,7 @@ export async function gotoReady(page: Page, url: string): Promise<void> {
128
128
  }
129
129
  ```
130
130
 
131
- Then `await gotoReady(page, "/pulse/counter")` instead of `page.goto(...)` for
131
+ Then `await gotoReady(page, "/demo/counter")` instead of `page.goto(...)` for
132
132
  interactive Flow pages.
133
133
 
134
134
  > **Warning** — Clicking before `alpine:initialized` fires is the most common source
@@ -41,7 +41,7 @@ describe("cache:clear", () => {
41
41
  Pass arguments and flags as a parameter map. Booleans become bare flags when `true`;
42
42
  everything else becomes a `key=value` token, mirroring how the CLI parses argv:
43
43
 
44
- ```typescript
44
+ ```typescript fragment
45
45
  // in a test file
46
46
  await Artisan.call("migrate", { "--fresh": true });
47
47
  await Artisan.call("make:model", { name: "Post", "--migration": true });
@@ -55,7 +55,7 @@ Commands usually do work — touch the database, queue a job, write a file. Asse
55
55
  _effect_, not just the output, using the [database](/docs/testing/database) and
56
56
  [mocking](/docs/testing/mocking) helpers:
57
57
 
58
- ```typescript
58
+ ```typescript fragment
59
59
  // src/tests/PruneUsersTest.ts
60
60
  import { Artisan } from "zerotal";
61
61
  import { assertDatabaseCount, QueueFake } from "@zerotal/testing";
@@ -79,7 +79,7 @@ For a focused test of a single command's logic, instantiate it directly and inje
79
79
  `BufferWriter` to capture output. The `_writer`, `args`, and `flags` fields are
80
80
  internal — the runner normally sets them — so a unit test reaches in with a cast:
81
81
 
82
- ```typescript
82
+ ```typescript fragment
83
83
  // src/tests/GreetCommandTest.ts
84
84
  import { expect } from "bun:test";
85
85
  import { BufferWriter } from "zerotal";
@@ -21,7 +21,7 @@ import {
21
21
  } from "@zerotal/testing";
22
22
  ```
23
23
 
24
- > **Note** — These helpers are part of the [testing toolkit](/docs/testing/index) and
24
+ > **Note** — These helpers are part of the [testing toolkit](/docs/testing) and
25
25
  > only run inside `bun test`. Install the package as a dev dependency if it isn't
26
26
  > already: `bun add -d @zerotal/testing`.
27
27
 
@@ -30,7 +30,7 @@ import {
30
30
  A test needs tables before it needs rows. Build them by running the project's own
31
31
  [migrations](/docs/migrations) rather than by writing the schema a second time:
32
32
 
33
- ```typescript
33
+ ```typescript fragment
34
34
  // tests/Feature/PostTest.ts
35
35
  import { migrateDatabase } from "@zerotal/testing";
36
36
 
@@ -46,7 +46,7 @@ until something fails for a reason that has nothing to do with the change you
46
46
  made. Running the migrations themselves means the schema under test is the schema
47
47
  that ships.
48
48
 
49
- ```typescript
49
+ ```typescript fragment
50
50
  // signature
51
51
  function migrateDatabase(options?: MigrateDatabaseOptions): Promise<string[]>;
52
52
  ```
@@ -61,7 +61,7 @@ It returns the names of the migrations it applied, and is idempotent — a secon
61
61
  call applies nothing. `refreshDatabase({ migrate: true })` runs it for you, which
62
62
  is usually what you want:
63
63
 
64
- ```typescript
64
+ ```typescript fragment
65
65
  // in a test file
66
66
  describe("Post", () => {
67
67
  refreshDatabase({ connection: db, migrate: true });
@@ -92,7 +92,7 @@ Call it once inside a `describe` block. Every `it()` in that block runs inside a
92
92
  transaction that rolls back when the test ends, so each test starts from the same
93
93
  clean baseline:
94
94
 
95
- ```typescript
95
+ ```typescript fragment
96
96
  // tests/Feature/UserTest.ts
97
97
  import { describe, it } from "bun:test";
98
98
  import { refreshDatabase, assertDatabaseHas, assertDatabaseMissing } from "@zerotal/testing";
@@ -115,7 +115,7 @@ describe("User", () => {
115
115
  });
116
116
  ```
117
117
 
118
- ```typescript
118
+ ```typescript fragment
119
119
  // signature
120
120
  function refreshDatabase(options?: RefreshDatabaseOptions): void;
121
121
  ```
@@ -137,7 +137,7 @@ fixtures), while each test's own writes happen inside the rolled-back transactio
137
137
 
138
138
  When you want rollback for a single test rather than a whole block, wrap the test body:
139
139
 
140
- ```typescript
140
+ ```typescript fragment
141
141
  // in a test file
142
142
  import { withDatabase } from "@zerotal/testing";
143
143
 
@@ -150,7 +150,7 @@ it(
150
150
  ); // ← transaction rolled back here
151
151
  ```
152
152
 
153
- ```typescript
153
+ ```typescript fragment
154
154
  // signature
155
155
  function withDatabase(fn: () => Promise<void>): () => Promise<void>;
156
156
  ```
@@ -201,7 +201,7 @@ await assertMissingFile("local", "uploads/old.jpg");
201
201
  Use [factories](/docs/orm/factories) to build the records a test acts on, and
202
202
  [seeders](/docs/seeding) for shared fixtures:
203
203
 
204
- ```typescript
204
+ ```typescript fragment
205
205
  // in a test file
206
206
  import { UserFactory } from "../database/factories/UserFactory.ts";
207
207
  import { PostFactory } from "../database/factories/PostFactory.ts";
@@ -231,7 +231,7 @@ jobs — call `.dispatchEvents()` when a test needs the full lifecycle.
231
231
 
232
232
  ## Next steps
233
233
 
234
- - [Testing overview](/docs/testing/index) — the full toolkit and a first test.
234
+ - [Testing overview](/docs/testing) — the full toolkit and a first test.
235
235
  - [Factories](/docs/orm/factories) — generating model records for tests.
236
236
  - [Seeding](/docs/seeding) — reusable fixtures across dev and tests.
237
237
  - [Database](/docs/database) — transactions and the raw `DB` layer being rolled back.
@@ -8,7 +8,7 @@ description: Drive a real page against a real server inside bun test, and assert
8
8
  `FlowBrowser` runs a headless browser against your app inside `bun test`, so a test
9
9
  can assert on the thing `FlowTest` cannot reach: the WebSocket bridge.
10
10
 
11
- ```ts
11
+ ```ts fragment
12
12
  import { FlowBrowser } from "@zerotal/testing/browser";
13
13
 
14
14
  const browser = await FlowBrowser.serve(() => import("../bootstrap/app.ts").then((m) => m.default));
@@ -70,7 +70,7 @@ The two environments differ deliberately. **A browser suite that skips itself in
70
70
  is worse than no suite**, because it reports green for exactly the failures it exists
71
71
  to catch. So pair the skip with a guard that runs regardless:
72
72
 
73
- ```ts
73
+ ```ts fragment
74
74
  import { FlowBrowser, browserAvailability, browserRequired } from "@zerotal/testing/browser";
75
75
 
76
76
  const availability = await browserAvailability();
@@ -94,7 +94,7 @@ that sets it does not have to hard-code the string.
94
94
 
95
95
  ## Serving the app
96
96
 
97
- ```ts
97
+ ```ts fragment
98
98
  const browser = await FlowBrowser.serve(bootstrap, options?);
99
99
  ```
100
100
 
@@ -112,7 +112,7 @@ through a second bootstrap that can drift. The server binds an OS-assigned port.
112
112
  Call `await browser.stop()` in `afterAll`: it closes every page it opened, then stops
113
113
  the server.
114
114
 
115
- ```ts
115
+ ```ts fragment
116
116
  let browser: FlowBrowser;
117
117
  beforeAll(async () => {
118
118
  browser = await FlowBrowser.serve(bootstrap);
@@ -168,7 +168,7 @@ suite gets deleted.
168
168
  `page.transport()` reports what Chrome saw on the wire — which the page cannot lie
169
169
  about. A client that degraded silently still shows zero upgraded sockets here.
170
170
 
171
- ```ts
171
+ ```ts fragment
172
172
  expect(page.socketUpgraded()).toBe(true);
173
173
  expect(page.transport().statuses).toContain(101);
174
174
  expect(await page.connection()).toBe("online");
@@ -192,7 +192,7 @@ It returns a `TransportReport`:
192
192
  `frames` carries the payloads, which is how you assert on what actually crossed the
193
193
  wire rather than on what the DOM ended up showing:
194
194
 
195
- ```ts
195
+ ```ts fragment
196
196
  const sent = page.transport().frames.filter((f) => f.direction === "sent");
197
197
  expect(sent.some((f) => f.payload.includes(`"method":"save"`))).toBe(true);
198
198
  ```
@@ -28,7 +28,7 @@ import { createTestApp, type TestApp } from "@zerotal/testing";
28
28
 
29
29
  Boot the app once per suite and close it when the suite finishes:
30
30
 
31
- ```typescript
31
+ ```typescript fragment
32
32
  // src/tests/PostTest.ts
33
33
  import { describe, it, beforeAll, afterAll } from "bun:test";
34
34
  import { createTestApp, type TestApp } from "@zerotal/testing";
@@ -45,7 +45,7 @@ afterAll(() => testApp.close());
45
45
 
46
46
  `createTestApp()` boots the app and starts it on an OS-assigned port (port `0`):
47
47
 
48
- ```typescript
48
+ ```typescript fragment
49
49
  function createTestApp(
50
50
  bootstrap: () => Application | Promise<Application>,
51
51
  setup?: () => void,
@@ -82,7 +82,7 @@ call `closeSharedTestApps()`.
82
82
 
83
83
  ## Sending requests
84
84
 
85
- ```typescript
85
+ ```typescript fragment
86
86
  // in a test
87
87
  await testApp.get("/path");
88
88
  await testApp.post("/path", body); // body serialized as JSON
@@ -104,7 +104,7 @@ A route meant for a browser should be tested the way a browser reaches it. A JSO
104
104
  redirect-back-with-errors branch of validation, the CSRF check, and any middleware
105
105
  that reads `application/x-www-form-urlencoded`.
106
106
 
107
- ```typescript
107
+ ```typescript fragment
108
108
  // in a test
109
109
  const res = await testApp.postForm("/posts", { title: "Hello", published: true });
110
110
 
@@ -120,7 +120,7 @@ strings `"null"` and `"undefined"`.
120
120
  `multipart()` is the only way to exercise a route that reads an uploaded file.
121
121
  Build the attachments with `fakeFile`:
122
122
 
123
- ```typescript
123
+ ```typescript fragment
124
124
  // in a test
125
125
  import { fakeFile } from "@zerotal/testing";
126
126
 
@@ -154,7 +154,7 @@ without a request, build one with `UploadedFile.fake('a.png', { type, size })`.
154
154
 
155
155
  ### Authentication
156
156
 
157
- ```typescript
157
+ ```typescript fragment
158
158
  // in a test
159
159
 
160
160
  // Forge a signed session cookie so the request is treated as authenticated
@@ -173,7 +173,7 @@ any `user_id` already set by `actingAs()`.
173
173
 
174
174
  ### Headers and redirects
175
175
 
176
- ```typescript
176
+ ```typescript fragment
177
177
  // in a test
178
178
  testApp.withHeaders({ "X-App-Version": "2" });
179
179
  testApp.withCookie("theme", "dark");
@@ -203,7 +203,7 @@ An exception inside a route is converted to a response before the test sees it,
203
203
  so the assertion reports `500` and the body is an error page. `withoutExceptionHandling()`
204
204
  hands you the original instead:
205
205
 
206
- ```typescript
206
+ ```typescript fragment
207
207
  // in a test
208
208
  const res = await testApp.withoutExceptionHandling().get("/checkout");
209
209
 
@@ -226,7 +226,7 @@ into the test output. Call `withExceptionHandling()` to restore the default.
226
226
  Every request returns a `TestResponse`. Assertions are chainable and throw
227
227
  descriptive errors on failure.
228
228
 
229
- ```typescript
229
+ ```typescript fragment
230
230
  // in a test
231
231
 
232
232
  // Status
@@ -300,7 +300,7 @@ A failed validation reaches the client two different ways: an API client gets
300
300
  flashed to the session. `assertInvalid` reads both, so the assertion is the same
301
301
  either way:
302
302
 
303
- ```typescript
303
+ ```typescript fragment
304
304
  // in a test
305
305
 
306
306
  // API client
@@ -331,7 +331,7 @@ reporting the key as absent.
331
331
 
332
332
  Reading the body directly:
333
333
 
334
- ```typescript
334
+ ```typescript fragment
335
335
  // in a test
336
336
  const data = res.json<{ id: number }>();
337
337
  const html = res.text();
@@ -343,7 +343,7 @@ const ok = res.ok; // boolean
343
343
 
344
344
  Skip HTTP entirely for fast, focused controller tests by faking the context:
345
345
 
346
- ```typescript
346
+ ```typescript fragment
347
347
  // src/tests/PostControllerTest.ts
348
348
  import { HttpContext } from "zerotal";
349
349
 
@@ -64,7 +64,7 @@ add it here too — that one edit keeps every test in the suite honest.
64
64
 
65
65
  From then on a test is two lines of setup:
66
66
 
67
- ```typescript
67
+ ```typescript fragment
68
68
  // tests/http/posts.test.ts
69
69
  import { test } from "bun:test";
70
70
  import { createApp } from "../helpers.ts";
@@ -149,7 +149,7 @@ building its own.
149
149
  `createTestApp()` boots your application, starts it on a random port, and returns a
150
150
  `TestApp` client. Pair it with a [factory](/docs/orm/factories) to arrange data:
151
151
 
152
- ```typescript
152
+ ```typescript fragment
153
153
  // tests/Feature/PostTest.ts
154
154
  import { describe, it, beforeAll, afterAll } from "bun:test";
155
155
  import { createTestApp, migrateDatabase, type TestApp, assertDatabaseHas } from "@zerotal/testing";
@@ -197,7 +197,7 @@ See [HTTP Tests](/docs/testing/http) for the full `TestApp` and `TestResponse` A
197
197
 
198
198
  ## Resetting framework state
199
199
 
200
- ```typescript
200
+ ```typescript fragment
201
201
  // tests/Feature/SomeTest.ts
202
202
  import { resetTestState } from "@zerotal/testing";
203
203
 
@@ -46,7 +46,7 @@ Everything except `Broadcast.fake()` and `Social.fake()` is re-exported from
46
46
  `EventFake.install()` replaces the `events` binding, so emitted events are
47
47
  recorded and their listeners never run.
48
48
 
49
- ```typescript
49
+ ```typescript fragment
50
50
  // src/tests/PostPublishTest.ts
51
51
  import { EventFake } from "@zerotal/testing";
52
52
 
@@ -80,7 +80,7 @@ search indexing, cache invalidation.
80
80
  `QueueFake.install()` replaces the `queue` binding so dispatched jobs are
81
81
  captured instead of persisted or executed.
82
82
 
83
- ```typescript
83
+ ```typescript fragment
84
84
  // src/tests/PostPublishTest.ts
85
85
  import { QueueFake } from "@zerotal/testing"; // re-exported from @zerotal/queue
86
86
 
@@ -111,7 +111,7 @@ it("queues a job when a post is published", async () => {
111
111
  `Notify.send()` and `Notify.queue()` are captured as "sent". The `mail` channel
112
112
  runs through `Notify`, so faking notifications also covers email side effects.
113
113
 
114
- ```typescript
114
+ ```typescript fragment
115
115
  // src/tests/InvoiceTest.ts
116
116
  import { NotificationFake } from "@zerotal/testing"; // re-exported from @zerotal/notifications
117
117
  import { Notify } from "@zerotal/notifications";
@@ -142,7 +142,7 @@ The `Broadcast` facade has its own recorder. Call `Broadcast.fake()` to install
142
142
  it and `Broadcast.resetFake()` to restore — the returned fake exposes the
143
143
  assertions:
144
144
 
145
- ```typescript
145
+ ```typescript fragment
146
146
  // src/tests/PostBroadcastTest.ts
147
147
  import { Broadcast } from "@zerotal/broadcasting";
148
148
 
@@ -168,7 +168,7 @@ touches the filesystem, so each test starts empty by construction rather than by
168
168
  remembering to clean up — and a suite that forgets to clean up passes the second
169
169
  time for the wrong reason.
170
170
 
171
- ```typescript
171
+ ```typescript fragment
172
172
  // src/tests/AvatarTest.ts
173
173
  import { Storage } from "zerotal/storage";
174
174
  import { fakeFile } from "@zerotal/testing";
@@ -201,7 +201,7 @@ it directly.
201
201
  client, so a test that exercises a service integration makes no real network
202
202
  call — and does not fail when that service is down or rate-limits you.
203
203
 
204
- ```typescript
204
+ ```typescript fragment
205
205
  // src/tests/PaymentTest.ts
206
206
  import { Http } from "@zerotal/testing";
207
207
 
@@ -231,9 +231,9 @@ Behaviour that depends on the passage of time — a token that expires in seven
231
231
  days, a reminder that only fires after 24 hours — has no way to be tested except
232
232
  by waiting. Freeze the clock instead:
233
233
 
234
- ```typescript
234
+ ```typescript fragment
235
235
  // src/tests/InvitationTest.ts
236
- import { Carbon } from "zerotal";
236
+ import { Carbon } from "zerotal/carbon";
237
237
 
238
238
  afterEach(() => Carbon.release());
239
239
 
package/docs/upgrade.md CHANGED
@@ -18,7 +18,7 @@ version line:
18
18
  - **Major** (`X.y.z`) — breaking changes; read the version's section in the
19
19
  [Release Notes](/docs/changelog) before upgrading.
20
20
 
21
- > **Warning** — while the 1.x line is young, a breaking change may also land in a minor or a patch. It is always labelled **BREAKING** in the [Release Notes](/docs/changelog) with migration steps, and two have shipped so far (1.3.0 and 1.7.2). Read the notes for every version you cross, not only the majors. See [Releases and versioning](/docs/support-policy#releases-and-versioning) for when this carve-out ends.
21
+ > **Warning** — while the 1.x line is young, a breaking change may also land in a minor or a patch. It is always labelled **BREAKING** in the [Release Notes](/docs/changelog) with migration steps. Read the notes for every version you cross, not only the majors. See [Releases and versioning](/docs/support-policy#releases-and-versioning) for which ones have shipped and when this carve-out ends.
22
22
 
23
23
  > **Warning** — always upgrade the `@zerotal/*` packages together. Mixing versions across core, ORM, and feature packages leads to type and runtime mismatches.
24
24