@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
@@ -25,13 +25,13 @@ carrying the rows plus the metadata, cursors, and URL helpers a UI needs.
25
25
  Pagination ships with `@zerotal/orm` as query-builder methods — nothing to
26
26
  install or register. Call them at the end of any query:
27
27
 
28
- ```typescript
28
+ ```typescript fragment
29
29
  const page = await Post.query().latest().paginate();
30
30
  ```
31
31
 
32
32
  ## paginate — full numbered pages
33
33
 
34
- ```typescript
34
+ ```typescript fragment
35
35
  // in a controller
36
36
  const page = await Post.query()
37
37
  .where("status", "published")
@@ -48,7 +48,7 @@ complete picture — at the cost of the extra count query. `perPage` defaults to
48
48
  omit `page` and it reads the request's current page (`?page=`, or the page a Flow
49
49
  component registered).
50
50
 
51
- ```typescript
51
+ ```typescript fragment
52
52
  async paginate<T>(perPage?: number, page?: number): Promise<PaginateResult<T>>
53
53
  ```
54
54
 
@@ -77,7 +77,7 @@ interface PaginateResult<T> {
77
77
 
78
78
  ### URL & state helpers
79
79
 
80
- ```typescript
80
+ ```typescript fragment
81
81
  // using a PaginateResult `page`
82
82
  page.hasMorePages; // boolean
83
83
  page.onFirstPage; // boolean
@@ -101,7 +101,7 @@ and extra query params, which are preserved across links so filters survive
101
101
  Skips the `COUNT` entirely by fetching `perPage + 1` rows to detect whether
102
102
  another page follows. Use it when you don't need a total or page numbers:
103
103
 
104
- ```typescript
104
+ ```typescript fragment
105
105
  // in a controller
106
106
  const page = await Post.query().latest().simplePaginate(20);
107
107
 
@@ -111,7 +111,7 @@ page.nextPageUrl();
111
111
  page.onFirstPage;
112
112
  ```
113
113
 
114
- ```typescript
114
+ ```typescript fragment
115
115
  async simplePaginate<T>(perPage?: number, page?: number): Promise<SimplePaginateResult<T>>
116
116
  ```
117
117
 
@@ -128,7 +128,7 @@ BY id ASC`, fetching `limit + 1` rows to detect a next page. It has **no
128
128
  `COUNT`** and stays fast no matter how deep you scroll — ideal for
129
129
  infinite-scroll feeds and very large tables:
130
130
 
131
- ```typescript
131
+ ```typescript fragment
132
132
  // in a controller
133
133
  let result = await Post.query().cursorPaginate({ limit: 20 });
134
134
 
@@ -141,7 +141,7 @@ result.hasMore; // boolean
141
141
  const more = await Post.query().cursorPaginate({ cursor: result.nextCursor!, limit: 20 });
142
142
  ```
143
143
 
144
- ```typescript
144
+ ```typescript fragment
145
145
  async cursorPaginate<T>(options?: { cursor?: number; limit?: number }): Promise<CursorPaginateResult<T>>
146
146
  ```
147
147
 
@@ -157,7 +157,7 @@ that encodes the last row's sort value so clients cannot interpret or tamper wit
157
157
  it. A secondary `id ASC` tiebreaker keeps page boundaries stable when the sort
158
158
  column isn't unique.
159
159
 
160
- ```typescript
160
+ ```typescript fragment
161
161
  // in a controller
162
162
  // First page, newest first
163
163
  const p1 = await Post.query().keysetPaginate({ column: "created_at", direction: "desc" });
@@ -173,7 +173,7 @@ const p2 = await Post.query().keysetPaginate({
173
173
  });
174
174
  ```
175
175
 
176
- ```typescript
176
+ ```typescript fragment
177
177
  async keysetPaginate<T>(options?: KeysetOptions): Promise<KeysetPaginateResult<T>>
178
178
 
179
179
  interface KeysetOptions {
@@ -213,7 +213,7 @@ Paginate _after_ applying filters and sorting, then pass the current filters to
213
213
  `links()` so they ride along on every page URL — without that, clicking "page 2" would
214
214
  silently drop the user's search:
215
215
 
216
- ```typescript
216
+ ```typescript fragment
217
217
  // in a controller — GET /search?q=bun&page=2
218
218
  const q = http.query("q", "");
219
219
 
@@ -235,7 +235,7 @@ return view("search", {
235
235
  For a feed the client scrolls forever, return the rows plus the next cursor and nothing
236
236
  else — no count, no page numbers. The client sends the cursor back to fetch more:
237
237
 
238
- ```typescript
238
+ ```typescript fragment
239
239
  // in a controller — GET /api/posts?cursor=128
240
240
  const result = await Post.query()
241
241
  .with("author")
@@ -258,7 +258,7 @@ live at the edges, so test the boundaries rather than the happy page.
258
258
  A `paginate()` result carries `data` plus the counts, so one call proves several
259
259
  things at once:
260
260
 
261
- ```typescript
261
+ ```typescript fragment
262
262
  // tests/pagination/Posts.test.ts
263
263
  import { test, expect } from "bun:test";
264
264
  import { PostFactory } from "../../database/factories/PostFactory.ts";
@@ -280,7 +280,7 @@ test("reports the right totals on the last page", async () => {
280
280
  **The three cases worth pinning down** are the ones that produce a broken UI
281
281
  rather than an exception:
282
282
 
283
- ```typescript
283
+ ```typescript fragment
284
284
  // tests/pagination/Posts.test.ts
285
285
  test("an empty result reports null bounds, not zero", async () => {
286
286
  const page = await Post.query().where("status", "nothing").paginate();
@@ -35,7 +35,7 @@ The broker reads and writes through your query functions, so it needs a table to
35
35
  back them. The default examples below use a `password_reset_tokens` table keyed by
36
36
  email:
37
37
 
38
- ```typescript
38
+ ```typescript fragment
39
39
  // database/migrations/xxxx_create_password_reset_tokens.ts
40
40
  await Schema.create("password_reset_tokens", (table) => {
41
41
  table.string("email").primary();
@@ -54,7 +54,7 @@ Create the broker once and export it so controllers can import it. Pass an objec
54
54
  implementing `PasswordBrokerOptions` — each query function maps the broker onto your
55
55
  storage:
56
56
 
57
- ```typescript
57
+ ```typescript fragment
58
58
  // app/auth/passwords.ts
59
59
  import { PasswordBroker, Hash } from "@zerotal/auth";
60
60
  import { Notify } from "@zerotal/notifications";
@@ -136,7 +136,7 @@ The controller wires the broker into request handlers: one pair for requesting a
136
136
  link, one pair for submitting the new password. Compare the broker result against the
137
137
  [`PASSWORDS` constants](#passwords-constants) rather than raw strings.
138
138
 
139
- ```typescript
139
+ ```typescript fragment
140
140
  // app/controllers/PasswordResetController.ts
141
141
  import { broker } from "#app/auth/passwords.ts";
142
142
  import { Auth, PASSWORDS } from "@zerotal/auth";
@@ -199,7 +199,7 @@ export class PasswordResetController {
199
199
 
200
200
  ## Routes
201
201
 
202
- ```typescript
202
+ ```typescript fragment
203
203
  // routes/web.ts
204
204
  import { GuestMiddleware } from "@zerotal/auth";
205
205
  import { PasswordResetController } from "#app/controllers/PasswordResetController.ts";
@@ -220,7 +220,7 @@ Expired rows accumulate because `reset()` only deletes a token when it's used or
220
220
  found expired on lookup. Call `broker.prune()` on a schedule to clear the rest — it
221
221
  delegates to your `pruneTokens` function with the cutoff date:
222
222
 
223
- ```typescript
223
+ ```typescript fragment
224
224
  // app/schedules/PrunePasswordTokens.ts
225
225
  import { Schedule } from "@zerotal/scheduler";
226
226
  import { broker } from "#app/auth/passwords.ts";
@@ -292,7 +292,7 @@ earn a test: a wrong token, an expired one, and the replay above.
292
292
  On the HTTP side, the case worth pinning down is that a miss is indistinguishable
293
293
  from a hit:
294
294
 
295
- ```typescript
295
+ ```typescript fragment
296
296
  // tests/http/password-reset.test.ts
297
297
  const res = await app.post("/forgot-password", { email: "nobody@example.com" });
298
298
 
package/docs/providers.md CHANGED
@@ -53,7 +53,7 @@ import { ServiceProvider } from "zerotal";
53
53
 
54
54
  ## Anatomy of a provider
55
55
 
56
- ```typescript
56
+ ```typescript fragment
57
57
  // app/providers/AppServiceProvider.ts
58
58
  import { ServiceProvider } from "zerotal";
59
59
  import { PaymentGateway } from "../services/PaymentGateway.ts";
@@ -107,7 +107,7 @@ one provider must be fully prepared before another even registers. There are als
107
107
  List providers in `bootstrap/providers.ts`. Order matters — a provider can
108
108
  only resolve bindings registered by providers that appear earlier in the list:
109
109
 
110
- ```typescript
110
+ ```typescript fragment
111
111
  // bootstrap/providers.ts
112
112
  import { DatabaseProvider } from "@zerotal/orm";
113
113
  import { CacheProvider } from "@zerotal/cache";
@@ -157,7 +157,7 @@ type-checked and survives renames. It does two jobs at once:
157
157
 
158
158
  So an app that uses the admin panel only needs the panel itself:
159
159
 
160
- ```typescript
160
+ ```typescript fragment
161
161
  // bootstrap/providers.ts — FlowProvider arrives via AdminProvider.dependsOn
162
162
  const providers = [AdminProvider];
163
163
 
@@ -180,7 +180,7 @@ defaults to `0`), then to registration order. It's a coarse knob — useful for
180
180
  framework-core provider that should generally boot ahead of everything else,
181
181
  without every other provider having to name it explicitly:
182
182
 
183
- ```typescript
183
+ ```typescript fragment
184
184
  export class CoreProvider extends ServiceProvider {
185
185
  static priority = -100; // boots before ordinary (priority 0) providers
186
186
  }
@@ -214,7 +214,7 @@ is **synchronous** — no `await` — and runs before any binding is resolved, s
214
214
  treat it as pure wiring. The factory closures you pass don't run yet; they run
215
215
  _later_, when the binding is first resolved.
216
216
 
217
- ```typescript
217
+ ```typescript fragment
218
218
  // inside a ServiceProvider
219
219
  onRegister(): void {
220
220
  // Singleton — one shared instance per app lifetime
@@ -243,7 +243,7 @@ booted. Reach for it only when one provider must finish preparing before the nex
243
243
  one even registers — most providers skip it entirely. It's async, so you can
244
244
  `await`:
245
245
 
246
- ```typescript
246
+ ```typescript fragment
247
247
  // inside a ServiceProvider
248
248
  async onBooting(): Promise<void> {
249
249
  // e.g. open a connection pool that a provider listed after this one
@@ -259,7 +259,7 @@ By `onBooted()` every provider has registered, so this is the safe place to
259
259
  _resolve_ bindings (including from other providers), start background work, and
260
260
  register event listeners. It runs in parallel across all providers:
261
261
 
262
- ```typescript
262
+ ```typescript fragment
263
263
  // inside a ServiceProvider
264
264
  async onBooted(): Promise<void> {
265
265
  // Resolve a binding from another provider
@@ -299,7 +299,7 @@ A provider can add its own auto-discovered directory by registering a concern de
299
299
  `onRegister()`/`onBooting()`. The framework scans the directory at boot and calls `register()`
300
300
  for each file's exports:
301
301
 
302
- ```typescript
302
+ ```typescript fragment
303
303
  // inside a ServiceProvider
304
304
  onRegister(): void {
305
305
  this.app.registerConcern({
@@ -346,7 +346,7 @@ Second — and this is the part that actually defers it — register it with
346
346
  `app.defer()` instead of the normal providers array. `static provides` on its own
347
347
  is just metadata; it's `defer()` that wires the lazy boot:
348
348
 
349
- ```typescript
349
+ ```typescript fragment
350
350
  // bootstrap/app.ts
351
351
  const app = Application.create({ providers });
352
352
  app.defer([SearchProvider]); // array form reads each provider's `static provides`
@@ -366,7 +366,7 @@ A provider runs in every runtime by default. Branch on `this.app.environment` to
366
366
  bind a different implementation per environment — a fake mailer under `test`, the
367
367
  real one everywhere else:
368
368
 
369
- ```typescript
369
+ ```typescript fragment
370
370
  // inside a ServiceProvider
371
371
  onRegister(): void {
372
372
  if (this.app.environment === 'test') {
@@ -387,7 +387,7 @@ Providers can push middleware into the global pipeline via `this.app.useOnce()`
387
387
  the framework guarantees it's added exactly once, even if the same middleware is
388
388
  registered by several providers:
389
389
 
390
- ```typescript
390
+ ```typescript fragment
391
391
  // inside a ServiceProvider
392
392
  onBooting(): Promise<void> {
393
393
  this.app.useOnce(SessionMiddleware);
@@ -53,7 +53,7 @@ just a description of a query until you `await` it.
53
53
 
54
54
  ## Selecting columns
55
55
 
56
- ```typescript
56
+ ```typescript fragment
57
57
  // in a controller or service
58
58
  DB.table("posts").select("id", "title", "created_at");
59
59
  DB.table("posts").distinct().select("status");
@@ -65,7 +65,7 @@ so build it only from trusted constants.
65
65
 
66
66
  ## Where clauses
67
67
 
68
- ```typescript
68
+ ```typescript fragment
69
69
  // in a controller or service
70
70
  DB.table("posts").where("status", "published"); // column = value
71
71
  DB.table("posts").where("views", ">", 1000); // explicit operator
@@ -106,7 +106,7 @@ parameterized.
106
106
 
107
107
  ## Joins
108
108
 
109
- ```typescript
109
+ ```typescript fragment
110
110
  // in a controller or service
111
111
  DB.table("posts")
112
112
  .join("users", "posts.user_id", "=", "users.id")
@@ -122,7 +122,7 @@ alias, first, operator, second)`.
122
122
 
123
123
  ## Ordering, grouping, limits
124
124
 
125
- ```typescript
125
+ ```typescript fragment
126
126
  // in a controller or service
127
127
  DB.table("posts").orderBy("created_at", "desc");
128
128
  DB.table("posts").orderByDesc("created_at"); // shorthand for the line above
@@ -145,7 +145,7 @@ clauses and optionally applies a fresh one.
145
145
  Apply clauses only when a condition is truthy — handy for optional filters without
146
146
  breaking the chain:
147
147
 
148
- ```typescript
148
+ ```typescript fragment
149
149
  // in a controller or service
150
150
  DB.table("posts")
151
151
  .when(status, (q, value) => q.where("status", value))
@@ -157,7 +157,7 @@ The callback receives the builder and the (truthy) condition value.
157
157
 
158
158
  ## Retrieving results
159
159
 
160
- ```typescript
160
+ ```typescript fragment
161
161
  // in a controller or service
162
162
  const rows = await DB.table("posts").where("status", "published").get();
163
163
  const row = await DB.table("posts").where("id", 1).first(); // first row or null
@@ -171,7 +171,7 @@ const has = await DB.table("posts").where("user_id", userId).exists(); // boolea
171
171
 
172
172
  Pass a row type to `get<T>()` / `first<T>()` for typed results:
173
173
 
174
- ```typescript
174
+ ```typescript fragment
175
175
  // in a controller or service
176
176
  const rows = await DB.table("posts").get<{ id: number; title: string }>();
177
177
  ```
@@ -181,7 +181,7 @@ const rows = await DB.table("posts").get<{ id: number; title: string }>();
181
181
 
182
182
  ### Aggregates
183
183
 
184
- ```typescript
184
+ ```typescript fragment
185
185
  // in a controller or service
186
186
  await DB.table("posts").count();
187
187
  await DB.table("posts").where("status", "published").sum("views");
@@ -197,7 +197,7 @@ For paged result sets, see [Pagination](/docs/pagination).
197
197
  For result sets too large to hold in memory, page through them instead of calling
198
198
  `get()`:
199
199
 
200
- ```typescript
200
+ ```typescript fragment
201
201
  // in a command or job
202
202
  await DB.table("posts").chunk(500, async (rows, page) => {
203
203
  for (const row of rows) await archive(row);
@@ -215,7 +215,7 @@ for await (const row of DB.table("posts").lazy()) {
215
215
 
216
216
  ## Writing rows
217
217
 
218
- ```typescript
218
+ ```typescript fragment
219
219
  // in a controller or service
220
220
  // INSERT
221
221
  await DB.table("post_tags").insert({ post_id: 1, tag_id: 3 });
@@ -242,7 +242,7 @@ one if none exists; it returns `true` when a row was inserted.
242
242
 
243
243
  Inside a [transaction](/docs/database#transactions), lock the selected rows:
244
244
 
245
- ```typescript
245
+ ```typescript fragment
246
246
  // in a controller or service
247
247
  await DB.transaction(async (trx) => {
248
248
  const row = await trx.table("accounts").where("id", 1).lockForUpdate().first();
@@ -260,7 +260,7 @@ await DB.transaction(async (trx) => {
260
260
 
261
261
  ## Debugging
262
262
 
263
- ```typescript
263
+ ```typescript fragment
264
264
  // in a controller or service
265
265
  DB.table("posts").where("status", "published").toSql(); // SQL with ? placeholders
266
266
  DB.table("posts").where("status", "published").toRawSql(); // values inlined (logging only)
@@ -282,7 +282,7 @@ The bread and butter of any index page or list endpoint. `when()` lets every fil
282
282
  be optional without a tangle of `if` statements, and `paginate()` returns the rows
283
283
  plus the page metadata in one call:
284
284
 
285
- ```typescript
285
+ ```typescript fragment
286
286
  // in a controller — req.query holds the optional filters
287
287
  const posts = await DB.table("posts")
288
288
  .when(req.query.status, (q, status) => q.where("status", status))
@@ -301,7 +301,7 @@ For join tables and key/value rows, `updateOrInsert` avoids the "check then inse
301
301
  race — it updates the row matching the first argument, or inserts the two merged if
302
302
  none exists:
303
303
 
304
- ```typescript
304
+ ```typescript fragment
305
305
  await DB.table("user_settings").updateOrInsert(
306
306
  { user_id: userId, key: "theme" }, // how to find the row
307
307
  { value: "dark" }, // what to set
@@ -313,7 +313,7 @@ await DB.table("user_settings").updateOrInsert(
313
313
  When you only need numbers, skip model hydration entirely and let the database do the
314
314
  aggregation:
315
315
 
316
- ```typescript
316
+ ```typescript fragment
317
317
  const byAuthor = await DB.table("posts")
318
318
  .select("user_id")
319
319
  .selectRaw("COUNT(*) AS post_count")
@@ -329,7 +329,7 @@ const byAuthor = await DB.table("posts")
329
329
  Never load a big table with `get()`. Page through it with `chunkById`, which walks an
330
330
  incrementing key so concurrent inserts or deletes can't make it skip or repeat rows:
331
331
 
332
- ```typescript
332
+ ```typescript fragment
333
333
  // in a command or job
334
334
  await DB.table("posts")
335
335
  .whereNull("slug")
@@ -347,7 +347,7 @@ await DB.table("posts")
347
347
  When exactly one row should match — looking a user up by email, say — `sole()` turns
348
348
  "zero or many matches" into a thrown error instead of a silent bug:
349
349
 
350
- ```typescript
350
+ ```typescript fragment
351
351
  const user = await DB.table("users").where("email", email).sole();
352
352
  ```
353
353
 
@@ -383,7 +383,7 @@ silently dropped by a mis-chained `orWhere` — without needing rows to prove it
383
383
  **Assert the rows when the result is the point.** Arrange with factories and run
384
384
  the query for real:
385
385
 
386
- ```typescript
386
+ ```typescript fragment
387
387
  // tests/queries/ActiveSubscribers.test.ts
388
388
  test("excludes users still in trial", async () => {
389
389
  await UserFactory.create({ status: "active", trialEndsAt: null });
package/docs/queue.md CHANGED
@@ -56,7 +56,9 @@ import { QueueConfig } from "@zerotal/queue";
56
56
  import { env } from "zerotal";
57
57
 
58
58
  export default QueueConfig({
59
- driver: env("QUEUE_DRIVER", "sqlite"),
59
+ // One of 'sqlite' | 'redis' | 'sync', written literally: the field is that union
60
+ // and `env()` returns a plain string.
61
+ driver: "sqlite",
60
62
  pollInterval: env("QUEUE_POLL_INTERVAL", 500),
61
63
  queues: ["default"],
62
64
  workers: env("QUEUE_WORKERS", 0),
@@ -81,7 +83,7 @@ A job is a class that extends `Job` and implements `handle()`. Constructor
81
83
  arguments are the job's state — serialise them in `payload()` and restore them in
82
84
  a static `fromPayload()`:
83
85
 
84
- ```typescript
86
+ ```typescript fragment
85
87
  // app/jobs/NotifyFollowersJob.ts
86
88
  import { Job, JobRegistry } from "@zerotal/queue";
87
89
 
@@ -126,7 +128,7 @@ JobRegistry.register(NotifyFollowersJob as never);
126
128
 
127
129
  A job with no constructor state needs only `handle()` plus the registration line:
128
130
 
129
- ```typescript
131
+ ```typescript fragment
130
132
  // app/jobs/PruneDeletedContentJob.ts
131
133
  import { Job, JobRegistry } from "@zerotal/queue";
132
134
 
@@ -164,7 +166,7 @@ the `JobRegistry.register(...)` line — keep it at the bottom of each job file.
164
166
 
165
167
  ## Dispatching jobs
166
168
 
167
- ```typescript
169
+ ```typescript fragment
168
170
  // in a controller
169
171
  import { Queue } from "@zerotal/queue";
170
172
 
@@ -183,7 +185,7 @@ worker loop, not the web request.
183
185
 
184
186
  A document saved eight times in a minute should rebuild its search index once, and the only rebuild anyone sees is the last one. Set `debounce` to a number of seconds and repeated dispatches collapse into a single run:
185
187
 
186
- ```typescript
188
+ ```typescript fragment
187
189
  export class ReindexDocument extends Job {
188
190
  /** Run 30s after the last dispatch, not once per dispatch. */
189
191
  override readonly debounce = 30;
@@ -202,7 +204,7 @@ export class ReindexDocument extends Job {
202
204
  }
203
205
  ```
204
206
 
205
- ```typescript
207
+ ```typescript fragment
206
208
  // Eight saves in quick succession…
207
209
  for (const _ of edits) await Queue.dispatch(new ReindexDocument(doc.id));
208
210
  // …one job, running 30s after the last one.
@@ -220,7 +222,7 @@ By default, the class name plus the serialised payload. So `ReindexDocument(1)`
220
222
 
221
223
  Override `debounceKey()` when two payloads mean the same work. A job carrying a timestamp or a request id is unique on every dispatch and would otherwise never collapse with anything:
222
224
 
223
- ```typescript
225
+ ```typescript fragment
224
226
  export class ReindexDocument extends Job {
225
227
  override readonly debounce = 30;
226
228
 
@@ -258,7 +260,7 @@ This is the behaviour you want for the reindex case: a save that lands while the
258
260
  Batch a set of jobs and react when they all finish. Batching uses the
259
261
  `zerotal_job_batches` table (auto-created by `SqliteDriver`).
260
262
 
261
- ```typescript
263
+ ```typescript fragment
262
264
  // in a controller
263
265
  import { Bus } from "@zerotal/queue";
264
266
 
@@ -283,7 +285,7 @@ const batch = await Bus.batch(rows.map((row) => new ImportCsvRowJob(row)))
283
285
 
284
286
  `Bus.batch(...).dispatch()` resolves to a `Batch` instance:
285
287
 
286
- ```typescript
288
+ ```typescript fragment
287
289
  // after .dispatch()
288
290
  batch.id; // UUID string
289
291
  batch.name; // label from .name()
@@ -304,7 +306,7 @@ batch.progress(); // 0.0 → 1.0
304
306
  Run jobs sequentially: each job dispatches the next one only after it succeeds. If
305
307
  any job fails, the rest of the chain is abandoned.
306
308
 
307
- ```typescript
309
+ ```typescript fragment
308
310
  // in a controller
309
311
  import { Bus } from "@zerotal/queue";
310
312
 
@@ -437,7 +439,7 @@ which stops every thread. Any in-flight or queued work is resolved with
437
439
  `QueueFake` swaps the `queue` binding for a fake that captures dispatched jobs
438
440
  instead of running them, so you can assert on them:
439
441
 
440
- ```typescript
442
+ ```typescript fragment
441
443
  // in a test
442
444
  import { QueueFake } from "@zerotal/queue";
443
445
 
@@ -530,7 +532,7 @@ while leaving unrelated failures alone.
530
532
  | `QueueShuttingDownError` | `E_QUEUE_SHUTTING_DOWN` | Dispatching during a graceful shutdown — the manager is draining. |
531
533
  | `QueueBatchingUnsupportedError` | `E_QUEUE_BATCHING_UNSUPPORTED` | Using batches on a driver that has no batch support. |
532
534
 
533
- ```typescript
535
+ ```typescript fragment
534
536
  // in a controller or service
535
537
  import { QueueError, QueueShuttingDownError } from "@zerotal/queue";
536
538
 
@@ -38,7 +38,7 @@ import { ThrottleMiddleware } from "zerotal";
38
38
  Attach it directly with the static `.with(options)` factory, which returns a
39
39
  ready-to-use middleware class:
40
40
 
41
- ```typescript
41
+ ```typescript fragment
42
42
  // in routes/web.ts (or wherever you register routes)
43
43
  import { ThrottleMiddleware } from "zerotal";
44
44
 
@@ -117,7 +117,7 @@ is the client IP.
117
117
  `RateLimiter.middleware(name)` returns the middleware instance for a registered
118
118
  limiter, ready to drop into a route or group:
119
119
 
120
- ```typescript
120
+ ```typescript fragment
121
121
  // in routes/web.ts
122
122
  import { RateLimiter } from "zerotal";
123
123
 
@@ -133,7 +133,7 @@ Router.group({ prefix: "/api", middleware: [RateLimiter.middleware("api")] }, ()
133
133
  Check or clear a limiter imperatively — e.g. reset failed-login counts after a
134
134
  successful sign-in:
135
135
 
136
- ```typescript
136
+ ```typescript fragment
137
137
  // in a controller
138
138
  import { RateLimiter } from "zerotal";
139
139
 
@@ -171,7 +171,7 @@ line for CLI requests.
171
171
  Set your suite up once as described in [Testing](/docs/testing). A rate limiter
172
172
  is only proven by the request that gets refused, so the test has to exhaust it.
173
173
 
174
- ```typescript
174
+ ```typescript fragment
175
175
  // tests/http/throttle.test.ts
176
176
  import { test, expect } from "bun:test";
177
177
  import { createApp } from "../helpers.ts";
@@ -196,7 +196,7 @@ test("the sixth attempt in a minute is refused", async () => {
196
196
  reads to back off. A limiter that returns `429` without them still fails the
197
197
  clients it was meant to protect you from:
198
198
 
199
- ```typescript
199
+ ```typescript fragment
200
200
  // tests/http/throttle.test.ts
201
201
  blocked.assertHeader("X-RateLimit-Limit", "5");
202
202
  blocked.assertHeader("X-RateLimit-Remaining", "0");
@@ -206,7 +206,7 @@ blocked.assertHeader("X-RateLimit-Remaining", "0");
206
206
  next in the same window. Give each test a distinct key — a different route, IP
207
207
  header, or user — rather than relying on ordering:
208
208
 
209
- ```typescript
209
+ ```typescript fragment
210
210
  // tests/http/throttle.test.ts
211
211
  await app.post("/login", { email: "a@b.c" }, { "X-Forwarded-For": "10.0.0.7" });
212
212
  ```