@zerotal/arch 1.7.3 → 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.
- package/CHANGELOG.md +34 -2
- package/README.md +4 -2
- package/api-surface.md +4 -4
- package/docs/about.md +16 -16
- package/docs/admin/actions.md +9 -9
- package/docs/admin/auth.md +2 -2
- package/docs/admin/dashboard.md +5 -5
- package/docs/admin/extending-ui.md +12 -12
- package/docs/admin/extending.md +16 -7
- package/docs/admin/forms.md +4 -4
- package/docs/admin/index.md +2 -2
- package/docs/admin/operations.md +9 -9
- package/docs/admin/resources.md +3 -3
- package/docs/admin/structure.md +6 -6
- package/docs/admin/tables.md +8 -8
- package/docs/admin/testing.md +4 -4
- package/docs/ai.md +118 -11
- package/docs/application.md +12 -12
- package/docs/arch.md +25 -3
- package/docs/assets.md +11 -11
- package/docs/audit.md +13 -13
- package/docs/authentication.md +39 -39
- package/docs/authorization.md +13 -13
- package/docs/broadcasting/channels.md +2 -2
- package/docs/broadcasting/client.md +2 -2
- package/docs/broadcasting/events.md +9 -10
- package/docs/broadcasting/index.md +1 -1
- package/docs/broadcasting/testing.md +5 -5
- package/docs/cache.md +15 -13
- package/docs/carbon.md +35 -35
- package/docs/changelog.md +165 -1
- package/docs/client/index.md +277 -70
- package/docs/commands.md +4 -4
- package/docs/components.md +116 -115
- package/docs/config-system.md +4 -4
- package/docs/container.md +27 -27
- package/docs/context.md +27 -27
- package/docs/contributing.md +46 -0
- package/docs/controllers.md +10 -10
- package/docs/conventions.md +23 -15
- package/docs/cookies.md +3 -3
- package/docs/csrf.md +6 -6
- package/docs/database.md +22 -17
- package/docs/deployment.md +2 -2
- package/docs/devtools.md +16 -16
- package/docs/email-verification.md +8 -8
- package/docs/encryption.md +7 -7
- package/docs/errors.md +6 -6
- package/docs/events.md +12 -11
- package/docs/flow/components.md +32 -32
- package/docs/flow/decorators.md +18 -18
- package/docs/flow/events.md +16 -16
- package/docs/flow/forms.md +17 -17
- package/docs/flow/icons.md +199 -0
- package/docs/flow/index.md +20 -20
- package/docs/flow/layouts.md +35 -35
- package/docs/flow/lifecycle.md +16 -16
- package/docs/flow/models.md +9 -9
- package/docs/flow/pagination.md +8 -8
- package/docs/flow/performance.md +7 -7
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +26 -26
- package/docs/flow/testing.md +20 -20
- package/docs/getting-started.md +18 -13
- package/docs/health.md +17 -8
- package/docs/helpers.md +17 -17
- package/docs/i18n.md +10 -10
- package/docs/inertia/devtools.md +4 -4
- package/docs/inertia/index.md +12 -7
- package/docs/inertia/props.md +22 -22
- package/docs/inertia/rendering.md +86 -7
- package/docs/inertia/ssr.md +2 -2
- package/docs/lifecycle.md +2 -2
- package/docs/lock.md +11 -11
- package/docs/logger.md +12 -12
- package/docs/media.md +22 -22
- package/docs/middleware.md +17 -17
- package/docs/migrations.md +13 -13
- package/docs/monitor.md +6 -6
- package/docs/notifications.md +24 -24
- package/docs/orm/casts.md +14 -14
- package/docs/orm/factories.md +8 -8
- package/docs/orm/index.md +22 -20
- package/docs/orm/lifecycle.md +13 -13
- package/docs/orm/queries.md +28 -28
- package/docs/orm/relationships.md +16 -16
- package/docs/orm/serialization.md +11 -11
- package/docs/package-development.md +10 -10
- package/docs/pagination.md +14 -14
- package/docs/password-reset.md +6 -6
- package/docs/providers.md +11 -11
- package/docs/query-builder.md +18 -18
- package/docs/queue.md +14 -12
- package/docs/rate-limiting.md +6 -6
- package/docs/responses.md +14 -14
- package/docs/roles-and-2fa.md +9 -9
- package/docs/routing.md +44 -44
- package/docs/scaffolding.md +1 -1
- package/docs/scheduler.md +10 -10
- package/docs/seeding.md +6 -6
- package/docs/session.md +16 -14
- package/docs/social.md +14 -14
- package/docs/storage.md +25 -13
- package/docs/structure.md +2 -2
- package/docs/support-policy.md +36 -8
- package/docs/telemetry.md +12 -12
- package/docs/tenancy.md +34 -40
- package/docs/testing/browser.md +6 -6
- package/docs/testing/console.md +3 -3
- package/docs/testing/database.md +10 -10
- package/docs/testing/flow-browser.md +6 -6
- package/docs/testing/http.md +12 -12
- package/docs/testing/index.md +3 -3
- package/docs/testing/mocking.md +8 -8
- package/docs/upgrade.md +1 -1
- package/docs/validator.md +19 -19
- package/docs/view.md +14 -14
- package/package.json +4 -4
- package/src/index.ts +25 -1
- package/src/tools/searchDocs.ts +7 -0
- package/docs/client/auth.md +0 -113
- package/docs/client/errors.md +0 -139
- package/docs/client/files.md +0 -118
- package/docs/client/references.md +0 -58
- package/docs/client/requests.md +0 -131
- package/docs/client/resilience.md +0 -141
- package/docs/client/testing.md +0 -146
package/docs/config-system.md
CHANGED
|
@@ -94,7 +94,7 @@ Dot-notation maps to the config file path and the key within it. `config('databa
|
|
|
94
94
|
|
|
95
95
|
`config()` is type-aware. Each path resolves to the type declared in the owning package's `*ConfigShape`, with autocomplete on the path string:
|
|
96
96
|
|
|
97
|
-
```typescript
|
|
97
|
+
```typescript fragment
|
|
98
98
|
// in application code
|
|
99
99
|
config("app.name"); // string
|
|
100
100
|
config("app.port"); // number
|
|
@@ -105,7 +105,7 @@ config.set("app.debug", "yes"); // type error: expected boolean
|
|
|
105
105
|
|
|
106
106
|
This works exactly like the container's `ContainerBindings`: there's a `ConfigRegistry` interface that every config-owning package augments by namespace. Core registers `app` and `health`; each package registers its own next to its `*ConfigShape`:
|
|
107
107
|
|
|
108
|
-
```typescript
|
|
108
|
+
```typescript fragment
|
|
109
109
|
// in a package's config registry declaration
|
|
110
110
|
declare module "zerotal" {
|
|
111
111
|
interface ConfigRegistry {
|
|
@@ -140,7 +140,7 @@ Because every file is loaded as a normal module, a config file may `import` and
|
|
|
140
140
|
|
|
141
141
|
## Overriding config in tests
|
|
142
142
|
|
|
143
|
-
```typescript
|
|
143
|
+
```typescript fragment
|
|
144
144
|
// in a test setup file
|
|
145
145
|
import { config } from "zerotal";
|
|
146
146
|
|
|
@@ -227,7 +227,7 @@ SESSION_SECRET=super-secret
|
|
|
227
227
|
|
|
228
228
|
Use `APP_ENV` to differentiate behaviour:
|
|
229
229
|
|
|
230
|
-
```typescript
|
|
230
|
+
```typescript fragment
|
|
231
231
|
// in application code
|
|
232
232
|
if (config("app.env") === "production") {
|
|
233
233
|
// production-only logic
|
package/docs/container.md
CHANGED
|
@@ -34,7 +34,7 @@ one line) and decoupling (a class depends on a `Mailer` contract, not on
|
|
|
34
34
|
|
|
35
35
|
Inside a `ServiceProvider`, the container lives at `this.app.container`:
|
|
36
36
|
|
|
37
|
-
```typescript
|
|
37
|
+
```typescript fragment
|
|
38
38
|
// in a ServiceProvider method
|
|
39
39
|
this.app.container.singleton(CacheManager, () => new CacheManager());
|
|
40
40
|
const cache = await this.app.container.make(CacheManager);
|
|
@@ -42,7 +42,7 @@ const cache = await this.app.container.make(CacheManager);
|
|
|
42
42
|
|
|
43
43
|
Anywhere else, reach it through the application singleton:
|
|
44
44
|
|
|
45
|
-
```typescript
|
|
45
|
+
```typescript fragment
|
|
46
46
|
// in application code outside a provider
|
|
47
47
|
import { Application } from "zerotal";
|
|
48
48
|
|
|
@@ -97,7 +97,7 @@ Created once, then cached for the lifetime of the application. Every caller gets
|
|
|
97
97
|
the same instance. The factory receives the container so it can resolve its own
|
|
98
98
|
dependencies:
|
|
99
99
|
|
|
100
|
-
```typescript
|
|
100
|
+
```typescript fragment
|
|
101
101
|
// in a ServiceProvider's onRegister()
|
|
102
102
|
this.app.container.singleton(CacheManager, async (c) => {
|
|
103
103
|
const cfg = await c.make("config");
|
|
@@ -115,7 +115,7 @@ Created once per HTTP request and isolated between concurrent requests via
|
|
|
115
115
|
`AsyncLocalStorage`. Resolving a scoped binding outside of a request context
|
|
116
116
|
throws `ScopedOutsideRequestError`:
|
|
117
117
|
|
|
118
|
-
```typescript
|
|
118
|
+
```typescript fragment
|
|
119
119
|
// in a ServiceProvider's onRegister()
|
|
120
120
|
this.app.container.scoped(UserSession, (c) => new UserSession());
|
|
121
121
|
```
|
|
@@ -124,7 +124,7 @@ this.app.container.scoped(UserSession, (c) => new UserSession());
|
|
|
124
124
|
|
|
125
125
|
A fresh instance on every `make()`:
|
|
126
126
|
|
|
127
|
-
```typescript
|
|
127
|
+
```typescript fragment
|
|
128
128
|
// in a ServiceProvider's onRegister()
|
|
129
129
|
this.app.container.bind(ReportGenerator, () => new ReportGenerator());
|
|
130
130
|
```
|
|
@@ -133,7 +133,7 @@ this.app.container.bind(ReportGenerator, () => new ReportGenerator());
|
|
|
133
133
|
|
|
134
134
|
Bind a pre-built instance directly — no factory, no lazy construction:
|
|
135
135
|
|
|
136
|
-
```typescript
|
|
136
|
+
```typescript fragment
|
|
137
137
|
// in a ServiceProvider's onRegister()
|
|
138
138
|
this.app.container.value("config", configObject);
|
|
139
139
|
```
|
|
@@ -142,7 +142,7 @@ this.app.container.value("config", configObject);
|
|
|
142
142
|
|
|
143
143
|
Bindings are registered in a `ServiceProvider`'s `onRegister()`:
|
|
144
144
|
|
|
145
|
-
```typescript
|
|
145
|
+
```typescript fragment
|
|
146
146
|
// app/providers/AppServiceProvider.ts
|
|
147
147
|
import { ServiceProvider } from "zerotal";
|
|
148
148
|
import { CacheManager } from "../services/CacheManager.ts";
|
|
@@ -197,7 +197,7 @@ can't register itself, and only reach for a provider when you need lifecycle hoo
|
|
|
197
197
|
`App` is the canonical surface for application code — resolve from anywhere
|
|
198
198
|
(pages, controllers, jobs) without importing `Application`:
|
|
199
199
|
|
|
200
|
-
```typescript
|
|
200
|
+
```typescript fragment
|
|
201
201
|
// in a controller, page, or job
|
|
202
202
|
import { App } from "zerotal";
|
|
203
203
|
import { UsersService } from "@app/services/users-service.ts";
|
|
@@ -211,7 +211,7 @@ It also exposes registration (`bind`, `singleton`, `scoped`, `value`, `alias`,
|
|
|
211
211
|
helpers (`environment()`, `isProduction()`, `isLocal()`). The `make()` and
|
|
212
212
|
`app()` global helpers are shorthands:
|
|
213
213
|
|
|
214
|
-
```typescript
|
|
214
|
+
```typescript fragment
|
|
215
215
|
// in application code
|
|
216
216
|
import { make, app } from "zerotal";
|
|
217
217
|
|
|
@@ -225,7 +225,7 @@ const same = await app(UsersService); // = make(UsersService)
|
|
|
225
225
|
Register bindings in `bootstrap/app.ts` without a provider. The callback runs at
|
|
226
226
|
boot, before any provider's `onRegister()`, so providers can still override:
|
|
227
227
|
|
|
228
|
-
```typescript
|
|
228
|
+
```typescript fragment
|
|
229
229
|
// bootstrap/app.ts — the callback receives the live container
|
|
230
230
|
Application.create({ providers })
|
|
231
231
|
.bind((container) => {
|
|
@@ -238,7 +238,7 @@ Application.create({ providers })
|
|
|
238
238
|
This is the place for **interface → implementation** bindings, since a class
|
|
239
239
|
can't register itself against a different token:
|
|
240
240
|
|
|
241
|
-
```typescript
|
|
241
|
+
```typescript fragment
|
|
242
242
|
// bootstrap/app.ts
|
|
243
243
|
Application.create({ providers }).bind((container) =>
|
|
244
244
|
container.singleton(Mailer, () => new SendGridMailer(env("SENDGRID_KEY"))),
|
|
@@ -250,7 +250,7 @@ Application.create({ providers }).bind((container) =>
|
|
|
250
250
|
Any class under `app/services/` is auto-discovered at boot. Declare its lifetime
|
|
251
251
|
next to the class with a `static lifetime` flag and it's registered for you:
|
|
252
252
|
|
|
253
|
-
```typescript
|
|
253
|
+
```typescript fragment
|
|
254
254
|
// app/services/users-service.ts
|
|
255
255
|
@inject(Auth)
|
|
256
256
|
export class UsersService {
|
|
@@ -288,7 +288,7 @@ binding at boot and the container hands each request its own instance.
|
|
|
288
288
|
`make()` is the primary way to resolve a binding. It's async because factories
|
|
289
289
|
may be async and deferred providers may need to boot first:
|
|
290
290
|
|
|
291
|
-
```typescript
|
|
291
|
+
```typescript fragment
|
|
292
292
|
// in application code
|
|
293
293
|
const cache = await container.make(CacheManager);
|
|
294
294
|
const cfg = await container.make("config");
|
|
@@ -305,7 +305,7 @@ Anything else throws `SyncResolutionError`. This is what [facades](#facades) use
|
|
|
305
305
|
internally — which is why providers pre-resolve (warm) their singleton in
|
|
306
306
|
`onBooted()` before any facade call happens:
|
|
307
307
|
|
|
308
|
-
```typescript
|
|
308
|
+
```typescript fragment
|
|
309
309
|
// in application code
|
|
310
310
|
const cache = container.makeSync(CacheManager); // throws if not yet resolved
|
|
311
311
|
```
|
|
@@ -316,7 +316,7 @@ const cache = container.makeSync(CacheManager); // throws if not yet resolved
|
|
|
316
316
|
instead of throwing when the token isn't registered. Useful for optional
|
|
317
317
|
services that exist only in certain runtime modes:
|
|
318
318
|
|
|
319
|
-
```typescript
|
|
319
|
+
```typescript fragment
|
|
320
320
|
// in a ServiceProvider — CommandRunner is only bound in console mode
|
|
321
321
|
const runner = container.tryMake("commandRunner");
|
|
322
322
|
if (runner) runner.register(MyCommand);
|
|
@@ -334,7 +334,7 @@ Pass a class's dependency tokens straight to `@inject(...)`. The container
|
|
|
334
334
|
resolves each token — recursively, and **in parallel** — before constructing the
|
|
335
335
|
class:
|
|
336
336
|
|
|
337
|
-
```typescript
|
|
337
|
+
```typescript fragment
|
|
338
338
|
// app/repositories/PostRepository.ts
|
|
339
339
|
import { inject } from "zerotal";
|
|
340
340
|
import { CacheManager } from "../services/CacheManager.ts";
|
|
@@ -351,7 +351,7 @@ export class PostRepository {
|
|
|
351
351
|
|
|
352
352
|
No registration needed — the container auto-wires on first `make()`:
|
|
353
353
|
|
|
354
|
-
```typescript
|
|
354
|
+
```typescript fragment
|
|
355
355
|
// in application code
|
|
356
356
|
const repo = await container.make(PostRepository);
|
|
357
357
|
```
|
|
@@ -365,7 +365,7 @@ classes, or string keys from `ContainerBindings`.
|
|
|
365
365
|
A token is the key the container resolves against — a class constructor, an
|
|
366
366
|
abstract class, or a string key declared in `ContainerBindings`:
|
|
367
367
|
|
|
368
|
-
```typescript
|
|
368
|
+
```typescript fragment
|
|
369
369
|
// in application code
|
|
370
370
|
// Class token (most common)
|
|
371
371
|
container.singleton(CacheManager, factory);
|
|
@@ -379,7 +379,7 @@ const cfg = await container.make("config");
|
|
|
379
379
|
Packages extend the `ContainerBindings` interface via declaration merging so
|
|
380
380
|
string tokens stay fully type-safe:
|
|
381
381
|
|
|
382
|
-
```typescript
|
|
382
|
+
```typescript fragment
|
|
383
383
|
// in a package's types.ts
|
|
384
384
|
declare module "zerotal" {
|
|
385
385
|
interface ContainerBindings {
|
|
@@ -409,7 +409,7 @@ After construction, any [`resolving()`](#resolving-hooks) hooks for the token fi
|
|
|
409
409
|
|
|
410
410
|
Hand a different implementation of the same dependency to different consumers:
|
|
411
411
|
|
|
412
|
-
```typescript
|
|
412
|
+
```typescript fragment
|
|
413
413
|
// in a ServiceProvider or bootstrap bind() callback
|
|
414
414
|
// PostController gets the Redis cache; ReportController gets the file cache
|
|
415
415
|
container.for(PostController).give(CacheDriver, () => new RedisCache());
|
|
@@ -434,7 +434,7 @@ of its `@inject` tokens.
|
|
|
434
434
|
Bind one token as an alias for another. Resolving the alias returns the target's
|
|
435
435
|
instance — handy for binding an interface/contract token to a concrete class:
|
|
436
436
|
|
|
437
|
-
```typescript
|
|
437
|
+
```typescript fragment
|
|
438
438
|
// in a ServiceProvider or bootstrap bind() callback
|
|
439
439
|
container.alias(CacheContract, CacheManager);
|
|
440
440
|
const cache = await container.make(CacheContract); // → the CacheManager singleton
|
|
@@ -448,7 +448,7 @@ alias cycles.
|
|
|
448
448
|
Run a callback every time a token resolves — for post-construction setup without
|
|
449
449
|
subclassing or wrapping the factory:
|
|
450
450
|
|
|
451
|
-
```typescript
|
|
451
|
+
```typescript fragment
|
|
452
452
|
// in a ServiceProvider or bootstrap bind() callback
|
|
453
453
|
container.resolving(Logger, (logger) => {
|
|
454
454
|
logger.setChannel("app");
|
|
@@ -463,7 +463,7 @@ instance is constructed.
|
|
|
463
463
|
Register a provider so it boots only when one of its tokens is first resolved —
|
|
464
464
|
keeping cold-start fast for services not used on every request:
|
|
465
465
|
|
|
466
|
-
```typescript
|
|
466
|
+
```typescript fragment
|
|
467
467
|
// in a ServiceProvider or bootstrap bind() callback
|
|
468
468
|
container.defer(SearchClient, SearchServiceProvider);
|
|
469
469
|
```
|
|
@@ -542,7 +542,7 @@ synchronously via `makeSync`. That's why providers pre-resolve their singleton i
|
|
|
542
542
|
|
|
543
543
|
Core ships a few, importable from `zerotal`:
|
|
544
544
|
|
|
545
|
-
```typescript
|
|
545
|
+
```typescript fragment
|
|
546
546
|
// in application code (after boot)
|
|
547
547
|
import { Config, Events, Artisan } from "zerotal";
|
|
548
548
|
|
|
@@ -560,12 +560,12 @@ await Artisan.call("migrate");
|
|
|
560
560
|
Packages ship their own — for example `Cache` from `@zerotal/cache`, `Auth` from
|
|
561
561
|
`@zerotal/auth`:
|
|
562
562
|
|
|
563
|
-
```typescript
|
|
563
|
+
```typescript fragment
|
|
564
564
|
// in application code (after boot)
|
|
565
565
|
import { Cache } from "@zerotal/cache";
|
|
566
566
|
import { Auth } from "@zerotal/auth";
|
|
567
567
|
|
|
568
|
-
await Cache.
|
|
568
|
+
await Cache.set("key", value, 300);
|
|
569
569
|
Auth.check(); // → boolean
|
|
570
570
|
Auth.user(); // → AuthenticatedUser (throws if guest)
|
|
571
571
|
Auth.userOrNull(); // → AuthenticatedUser | undefined
|
|
@@ -575,7 +575,7 @@ Auth.userOrNull(); // → AuthenticatedUser | undefined
|
|
|
575
575
|
|
|
576
576
|
Build one with `createFacade<T>(token)`, passing the same token the provider binds:
|
|
577
577
|
|
|
578
|
-
```typescript
|
|
578
|
+
```typescript fragment
|
|
579
579
|
// src/facades/Cache.ts
|
|
580
580
|
import { createFacade } from "zerotal";
|
|
581
581
|
import type { CacheManager } from "../CacheManager.ts";
|
package/docs/context.md
CHANGED
|
@@ -32,7 +32,7 @@ tests, build one with [`HttpContext.fake()`](#testing).
|
|
|
32
32
|
Every method on the context is bound to its instance, so you can destructure
|
|
33
33
|
exactly what a handler needs — methods included — without losing `this`:
|
|
34
34
|
|
|
35
|
-
```typescript
|
|
35
|
+
```typescript fragment
|
|
36
36
|
// routes: Router.get('/posts/:post/:tab', PostController, 'show')
|
|
37
37
|
|
|
38
38
|
async show({ view, params: { post, tab } }: HttpContext<{ post: Post; tab: string }>) {
|
|
@@ -55,7 +55,7 @@ instead (`async show(ctx: HttpContext)`) when you prefer `ctx.view(...)` /
|
|
|
55
55
|
`ctx.params` is `Record<string, string>` — the raw matched segments. Pass a type
|
|
56
56
|
argument to describe what the route resolves, including model bindings:
|
|
57
57
|
|
|
58
|
-
```typescript
|
|
58
|
+
```typescript fragment
|
|
59
59
|
// raw params only — every value is a string
|
|
60
60
|
async index(ctx: HttpContext) {
|
|
61
61
|
const page = ctx.params.page; // string | undefined
|
|
@@ -81,7 +81,7 @@ name (and exposes it via `ctx.model<T>()`).
|
|
|
81
81
|
Usually there is nothing to declare: a param whose name matches an auto-registered
|
|
82
82
|
model binds on its own, so `:post` resolves through `Post` by primary key.
|
|
83
83
|
|
|
84
|
-
```typescript
|
|
84
|
+
```typescript fragment
|
|
85
85
|
// routes/web.ts — :post is already bound to Post
|
|
86
86
|
Router.get("/posts/:post", PostController, "show");
|
|
87
87
|
|
|
@@ -91,7 +91,7 @@ Router.get("/posts/:post", PostController, "show").bind("post", (slug) =>
|
|
|
91
91
|
);
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
```typescript
|
|
94
|
+
```typescript fragment
|
|
95
95
|
// in PostController — ctx.params.post is the resolved Post, not the raw id
|
|
96
96
|
async show(ctx: HttpContext<{ post: Post }>) {
|
|
97
97
|
return ctx.json(ctx.params.post);
|
|
@@ -114,7 +114,7 @@ custom resolvers, scoped bindings — lives in [Routing](/docs/routing).
|
|
|
114
114
|
The scalar helpers check route params first, then the query string, and coerce to
|
|
115
115
|
the type the method name promises. Each takes an optional fallback.
|
|
116
116
|
|
|
117
|
-
```typescript
|
|
117
|
+
```typescript fragment
|
|
118
118
|
// in a controller
|
|
119
119
|
ctx.query("page", "1"); // string | undefined — query string only
|
|
120
120
|
ctx.string("sort", "asc"); // string | undefined — param then query
|
|
@@ -131,7 +131,7 @@ is absent, or the value is not a valid integer (`NaN`). Because parsing is base-
|
|
|
131
131
|
validate with a [form request](/docs/validator) when you need to reject malformed
|
|
132
132
|
input rather than coerce it.
|
|
133
133
|
|
|
134
|
-
```typescript
|
|
134
|
+
```typescript fragment
|
|
135
135
|
// in a controller — GET /posts?page=3
|
|
136
136
|
ctx.integer("page"); // 3
|
|
137
137
|
ctx.integer("page", 1); // 3 (fallback unused)
|
|
@@ -147,7 +147,7 @@ of `'1'`, `'true'`, `'yes'`, or `'on'` (compared case-insensitively). Any other
|
|
|
147
147
|
present value is `false`; an absent value returns the fallback, which defaults to
|
|
148
148
|
`false`.
|
|
149
149
|
|
|
150
|
-
```typescript
|
|
150
|
+
```typescript fragment
|
|
151
151
|
// in a controller
|
|
152
152
|
ctx.boolean("active"); // ?active=true / ?active=1 / ?active=ON → true
|
|
153
153
|
ctx.boolean("active"); // ?active=0 / ?active=no / ?active= → false
|
|
@@ -164,14 +164,14 @@ ctx.boolean("active", true); // absent → true (custom fallback)
|
|
|
164
164
|
`body()` parses and caches the body (JSON, form-urlencoded, or multipart fields) so
|
|
165
165
|
repeated calls are free, and returns `{}` on an absent or invalid body:
|
|
166
166
|
|
|
167
|
-
```typescript
|
|
167
|
+
```typescript fragment
|
|
168
168
|
const data = await ctx.body<{ title: string; body: string }>();
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
`input()` reads a single merged value in priority order — route params → cached
|
|
172
172
|
body → query string — without awaiting:
|
|
173
173
|
|
|
174
|
-
```typescript
|
|
174
|
+
```typescript fragment
|
|
175
175
|
ctx.input("id"); // route :id or ?id=
|
|
176
176
|
ctx.input("q", "all"); // with fallback
|
|
177
177
|
```
|
|
@@ -183,7 +183,7 @@ ctx.input("q", "all"); // with fallback
|
|
|
183
183
|
Read a header with `header()` (case-insensitive) or pull a Bearer token with
|
|
184
184
|
`bearerToken()`:
|
|
185
185
|
|
|
186
|
-
```typescript
|
|
186
|
+
```typescript fragment
|
|
187
187
|
ctx.header("x-forwarded-for"); // string | null
|
|
188
188
|
ctx.bearerToken(); // string | null — strips the "Bearer " prefix
|
|
189
189
|
```
|
|
@@ -193,7 +193,7 @@ ctx.bearerToken(); // string | null — strips the "Bearer " prefix
|
|
|
193
193
|
`file()` returns the first `UploadedFile` for a form field (or `null`); `files()`
|
|
194
194
|
returns all of them. Both parse and cache the multipart body on first call.
|
|
195
195
|
|
|
196
|
-
```typescript
|
|
196
|
+
```typescript fragment
|
|
197
197
|
const avatar = await ctx.file("avatar"); // UploadedFile | null
|
|
198
198
|
const attachments = await ctx.files("attachments"); // UploadedFile[]
|
|
199
199
|
```
|
|
@@ -201,7 +201,7 @@ const attachments = await ctx.files("attachments"); // UploadedFile[]
|
|
|
201
201
|
An `UploadedFile` describes the upload and knows how to persist itself. Validate
|
|
202
202
|
before storing, then hand it a disk — `store()` returns the stored path:
|
|
203
203
|
|
|
204
|
-
```typescript
|
|
204
|
+
```typescript fragment
|
|
205
205
|
// in a controller — single file
|
|
206
206
|
const avatar = await ctx.file("avatar");
|
|
207
207
|
|
|
@@ -220,7 +220,7 @@ if (avatar) {
|
|
|
220
220
|
}
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
```typescript
|
|
223
|
+
```typescript fragment
|
|
224
224
|
// in a controller — multiple files
|
|
225
225
|
for (const file of await ctx.files("attachments")) {
|
|
226
226
|
await file.store("uploads", Storage.disk());
|
|
@@ -239,7 +239,7 @@ The response helpers set `ctx.response` for you and return nothing — they are
|
|
|
239
239
|
terminal. You can also `return` a value from a controller; see
|
|
240
240
|
[Responses](/docs/responses) for the full set.
|
|
241
241
|
|
|
242
|
-
```typescript
|
|
242
|
+
```typescript fragment
|
|
243
243
|
ctx.json({ user }); // 200 application/json
|
|
244
244
|
ctx.json({ errors }, 422); // custom status
|
|
245
245
|
ctx.view(WelcomeView, { name }); // full HTML document (prepends <!DOCTYPE html>)
|
|
@@ -253,7 +253,7 @@ ctx.back(303); // same, with a 303 status
|
|
|
253
253
|
`view()` accepts either pre-rendered markup, or a **view component plus its props**.
|
|
254
254
|
A view component receives the request `HttpContext` first and your props second:
|
|
255
255
|
|
|
256
|
-
```typescript
|
|
256
|
+
```typescript fragment
|
|
257
257
|
// resources/views/Welcome.tsx
|
|
258
258
|
export default function Welcome(ctx: HttpContext, { title }: { title: string }) {
|
|
259
259
|
return (
|
|
@@ -307,7 +307,7 @@ the pipeline, ordering, and registration.
|
|
|
307
307
|
the matching record, and assigns it to `ctx.user`; `AuthMiddleware` reads it to guard
|
|
308
308
|
routes. It is `undefined` for guests.
|
|
309
309
|
|
|
310
|
-
```typescript
|
|
310
|
+
```typescript fragment
|
|
311
311
|
if (!ctx.user) throw new UnauthorizedError();
|
|
312
312
|
return ctx.json({ email: ctx.user.email });
|
|
313
313
|
```
|
|
@@ -317,7 +317,7 @@ registered auth model — see [Authentication](/docs/authentication).
|
|
|
317
317
|
|
|
318
318
|
## Client IP
|
|
319
319
|
|
|
320
|
-
```typescript
|
|
320
|
+
```typescript fragment
|
|
321
321
|
ctx.ip(); // string | null — socket-level IP from Bun's server.requestIP()
|
|
322
322
|
```
|
|
323
323
|
|
|
@@ -331,7 +331,7 @@ with `HttpContext.fake()`).
|
|
|
331
331
|
|
|
332
332
|
## URL & matching helpers
|
|
333
333
|
|
|
334
|
-
```typescript
|
|
334
|
+
```typescript fragment
|
|
335
335
|
ctx.path(); // "/posts" — pathname only
|
|
336
336
|
ctx.fullUrl(); // "https://app.test/posts?page=2"
|
|
337
337
|
ctx.host(); // "app.test"
|
|
@@ -343,7 +343,7 @@ ctx.subdomains; // { tenant: "acme" }
|
|
|
343
343
|
`isJson()` / `wantsJson()` inspect the `Content-Type` / `Accept` headers — handy in
|
|
344
344
|
an exception handler, or anywhere one route serves both browsers and API clients:
|
|
345
345
|
|
|
346
|
-
```typescript
|
|
346
|
+
```typescript fragment
|
|
347
347
|
// in a controller or middleware
|
|
348
348
|
if (ctx.wantsJson()) {
|
|
349
349
|
ctx.json({ message: "Unauthorized" }, 401);
|
|
@@ -358,13 +358,13 @@ Flash writes a value to the session for the **next** request only — ideal for
|
|
|
358
358
|
post-redirect success and error messages. It requires `SessionMiddleware` and
|
|
359
359
|
silently no-ops without it.
|
|
360
360
|
|
|
361
|
-
```typescript
|
|
361
|
+
```typescript fragment
|
|
362
362
|
// before redirecting
|
|
363
363
|
ctx.flash("success", "Post saved!");
|
|
364
364
|
return ctx.redirect("/posts", 303);
|
|
365
365
|
```
|
|
366
366
|
|
|
367
|
-
```typescript
|
|
367
|
+
```typescript fragment
|
|
368
368
|
// on the next request
|
|
369
369
|
const msg = ctx.flashed<string>("success"); // 'Post saved!'
|
|
370
370
|
```
|
|
@@ -378,7 +378,7 @@ Register work to run **after** the response has been sent — fire-and-forget si
|
|
|
378
378
|
effects that shouldn't delay the client. `afterResponse()` returns `this`, so calls
|
|
379
379
|
chain.
|
|
380
380
|
|
|
381
|
-
```typescript
|
|
381
|
+
```typescript fragment
|
|
382
382
|
ctx.afterResponse(async () => {
|
|
383
383
|
await analytics.track(ctx.requestId, ctx.url.pathname);
|
|
384
384
|
});
|
|
@@ -413,7 +413,7 @@ request — a service shared with CLI commands or queue workers — uses
|
|
|
413
413
|
`RequestContext.remember(key, factory)` runs `factory` at most once per request
|
|
414
414
|
for a given key and hands every later caller the same answer:
|
|
415
415
|
|
|
416
|
-
```typescript
|
|
416
|
+
```typescript fragment
|
|
417
417
|
import { RequestContext } from "zerotal";
|
|
418
418
|
|
|
419
419
|
const settings = await RequestContext.remember(`household:${id}:settings`, () =>
|
|
@@ -446,7 +446,7 @@ earlier in the same request.
|
|
|
446
446
|
`HttpContext.fake()` builds a context without a live server — perfect for unit
|
|
447
447
|
testing controllers and middleware:
|
|
448
448
|
|
|
449
|
-
```typescript
|
|
449
|
+
```typescript fragment
|
|
450
450
|
const ctx = HttpContext.fake("http://localhost/posts?page=2", {
|
|
451
451
|
method: "GET",
|
|
452
452
|
headers: { Authorization: "Bearer token" },
|
|
@@ -459,7 +459,7 @@ expect(ctx.response?.status).toBe(200);
|
|
|
459
459
|
Pass a `body` to exercise handlers that read one, and assign `ctx.params` directly
|
|
460
460
|
when the handler expects route params a real match would have provided:
|
|
461
461
|
|
|
462
|
-
```typescript
|
|
462
|
+
```typescript fragment
|
|
463
463
|
// in a test — a POST with a JSON body
|
|
464
464
|
const ctx = HttpContext.fake("http://localhost/posts", {
|
|
465
465
|
method: "POST",
|
|
@@ -470,14 +470,14 @@ ctx.params = { id: "42" };
|
|
|
470
470
|
```
|
|
471
471
|
|
|
472
472
|
The signature is `HttpContext.fake(url?, init?, container?)`; all three are optional.
|
|
473
|
-
See [Testing](/docs/testing
|
|
473
|
+
See [Testing](/docs/testing) for the full harness, and
|
|
474
474
|
[HTTP testing](/docs/testing/http) for the higher-level request helpers.
|
|
475
475
|
|
|
476
476
|
## References
|
|
477
477
|
|
|
478
478
|
### `HttpContext<TParams>`
|
|
479
479
|
|
|
480
|
-
```typescript
|
|
480
|
+
```typescript fragment
|
|
481
481
|
class HttpContext<TParams extends Record<string, unknown> = Record<string, string>> {
|
|
482
482
|
constructor(request: Request, container: ScopedResolver);
|
|
483
483
|
}
|
package/docs/contributing.md
CHANGED
|
@@ -149,6 +149,52 @@ that keeps it complete.
|
|
|
149
149
|
The same rule governs a page's own length: a section that has grown into a second
|
|
150
150
|
subject is a sign it wants to be its own page, or to move to the one that owns it.
|
|
151
151
|
|
|
152
|
+
### Code examples are compiled
|
|
153
|
+
|
|
154
|
+
Every TypeScript block in `docs/` is compiled against the real packages by
|
|
155
|
+
`bun run docs:examples:check`, which CI runs on every pull request. An example that
|
|
156
|
+
names a member the API no longer has fails the build rather than waiting for a reader
|
|
157
|
+
to find it.
|
|
158
|
+
|
|
159
|
+
Plenty of blocks are not whole programs on purpose — a class body, a set of columns,
|
|
160
|
+
a method shown without the class around it. Say so in the fence:
|
|
161
|
+
|
|
162
|
+
````text
|
|
163
|
+
```ts fragment
|
|
164
|
+
@column("string") name!: string;
|
|
165
|
+
@column("boolean") active!: boolean;
|
|
166
|
+
```
|
|
167
|
+
````
|
|
168
|
+
|
|
169
|
+
`fragment` works on `ts`, `tsx` and `typescript`, and readers never see it: a fence's
|
|
170
|
+
info string carries only its first word into the rendered page. It is never guessed —
|
|
171
|
+
a block is a fragment because you wrote that it is, and a run of
|
|
172
|
+
`bun run docs:examples` records it so a new one is a deliberate change rather than a
|
|
173
|
+
silent exemption.
|
|
174
|
+
|
|
175
|
+
Two lists back that up. `fragments` in `docs-examples-baseline.json` are the
|
|
176
|
+
intentional ones. `drift` is the other kind: a block that resolves every import and
|
|
177
|
+
still fails, which means the page is wrong. Neither list may grow, and `drift` is
|
|
178
|
+
meant to reach zero.
|
|
179
|
+
|
|
180
|
+
## Changelogs
|
|
181
|
+
|
|
182
|
+
Each package keeps its own `CHANGELOG.md`. Add what you changed under
|
|
183
|
+
`## [Unreleased]` in the package you changed — that is where in-flight work
|
|
184
|
+
belongs, and the entry travels with the code rather than being reconstructed later.
|
|
185
|
+
|
|
186
|
+
Cutting a release moves those entries under the version, which is a step and not an
|
|
187
|
+
automatic consequence:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
bun run changelog:release 1.7.4
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Skipping it is how `[Unreleased]` came to hold four releases of shipped work —
|
|
194
|
+
`@zerotal/flow-ui`'s newest heading read `[1.5.0]` while 1.7.3 was on npm. Someone
|
|
195
|
+
upgrading reads "Unreleased" and concludes none of it is in the version they are
|
|
196
|
+
taking, when all of it is.
|
|
197
|
+
|
|
152
198
|
## Pull requests
|
|
153
199
|
|
|
154
200
|
- Keep PRs focused — one logical change per PR.
|
package/docs/controllers.md
CHANGED
|
@@ -16,7 +16,7 @@ A controller is a plain class. Each action receives the request `HttpContext`
|
|
|
16
16
|
directly, reads input from it, and sets the response by calling a helper on it —
|
|
17
17
|
actions return `void`, not a value.
|
|
18
18
|
|
|
19
|
-
```typescript
|
|
19
|
+
```typescript fragment
|
|
20
20
|
// app/controllers/PostController.ts
|
|
21
21
|
import type { HttpContext } from "zerotal";
|
|
22
22
|
import { Post } from "../models/Post.ts";
|
|
@@ -48,7 +48,7 @@ export class PostController {
|
|
|
48
48
|
|
|
49
49
|
Map routes to actions by passing the controller class and the action name:
|
|
50
50
|
|
|
51
|
-
```typescript
|
|
51
|
+
```typescript fragment
|
|
52
52
|
// routes/index.ts
|
|
53
53
|
import { Router } from "zerotal";
|
|
54
54
|
import { PostController } from "../app/controllers/PostController.ts";
|
|
@@ -71,7 +71,7 @@ dependencies. Decorate the **class** with `@inject(...)`, listing the tokens in
|
|
|
71
71
|
constructor order — the container resolves each token and passes it to the
|
|
72
72
|
constructor:
|
|
73
73
|
|
|
74
|
-
```typescript
|
|
74
|
+
```typescript fragment
|
|
75
75
|
// app/controllers/PostController.ts
|
|
76
76
|
import type { HttpContext } from "zerotal";
|
|
77
77
|
import { inject } from "zerotal";
|
|
@@ -127,12 +127,12 @@ declaration — the resolved instance arrives on `ctx.params` under the param's
|
|
|
127
127
|
or you can read it explicitly with `ctx.model()`. `.bind()` and the model's own
|
|
128
128
|
`resolveRouteBinding` are for overriding that default:
|
|
129
129
|
|
|
130
|
-
```typescript
|
|
130
|
+
```typescript fragment
|
|
131
131
|
// routes/index.ts — :post already resolves through Post
|
|
132
132
|
Router.get("/posts/:post", PostController, "show");
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
```typescript
|
|
135
|
+
```typescript fragment
|
|
136
136
|
// app/controllers/PostController.ts — via ctx.params:
|
|
137
137
|
async show(ctx: HttpContext<{ post: Post }>): Promise<void> {
|
|
138
138
|
ctx.json(ctx.params.post);
|
|
@@ -176,7 +176,7 @@ export class StorePostRequest extends FormRequest {
|
|
|
176
176
|
returns the validated data fully typed from your `rules()` return — it throws a
|
|
177
177
|
validation error on failure:
|
|
178
178
|
|
|
179
|
-
```typescript
|
|
179
|
+
```typescript fragment
|
|
180
180
|
// app/controllers/PostController.ts
|
|
181
181
|
import { StorePostRequest } from "../requests/posts/StorePostRequest.ts";
|
|
182
182
|
|
|
@@ -228,7 +228,7 @@ export class ArticleController {
|
|
|
228
228
|
|
|
229
229
|
Register all seven with a single call, or narrow them with `.only()` / `.except()`:
|
|
230
230
|
|
|
231
|
-
```typescript
|
|
231
|
+
```typescript fragment
|
|
232
232
|
// routes/index.ts
|
|
233
233
|
Router.resource("articles", ArticleController);
|
|
234
234
|
Router.resource("articles", ArticleController).only(["index", "show"]);
|
|
@@ -243,7 +243,7 @@ Router.resource("articles", ArticleController).except(["create", "edit"]);
|
|
|
243
243
|
Controllers set `ctx.response` via helper methods — they do not return a value
|
|
244
244
|
(the return type is `Promise<void>`):
|
|
245
245
|
|
|
246
|
-
```typescript
|
|
246
|
+
```typescript fragment
|
|
247
247
|
// in a controller action
|
|
248
248
|
ctx.json(data); // 200 JSON
|
|
249
249
|
ctx.json(data, 201); // 201 JSON
|
|
@@ -281,7 +281,7 @@ With `@zerotal/auth` installed, the authenticated user is available at
|
|
|
281
281
|
`ctx.user` after `AuthMiddleware` runs. The `Auth` facade exposes the same data
|
|
282
282
|
from anywhere in the async tree — handy inside services called by the controller:
|
|
283
283
|
|
|
284
|
-
```typescript
|
|
284
|
+
```typescript fragment
|
|
285
285
|
// in a controller action
|
|
286
286
|
import { Auth } from "@zerotal/auth";
|
|
287
287
|
|
|
@@ -304,7 +304,7 @@ Register a callback to run after the response has been sent — useful for
|
|
|
304
304
|
expensive side effects that shouldn't block the client. `afterResponse` returns
|
|
305
305
|
`this`, so it chains:
|
|
306
306
|
|
|
307
|
-
```typescript
|
|
307
|
+
```typescript fragment
|
|
308
308
|
// in a controller action
|
|
309
309
|
async store(ctx: HttpContext): Promise<void> {
|
|
310
310
|
const post = await Post.create(data);
|