@zerotal/arch 1.7.4 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -2
- package/README.md +4 -2
- package/api-surface.md +2 -0
- 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 +268 -1
- package/docs/client/index.md +13 -13
- package/docs/commands.md +36 -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 +28 -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 +99 -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 +5 -5
- package/docs/flow/index.md +21 -21
- package/docs/flow/layouts.md +99 -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 +6 -6
- package/docs/inertia/props.md +21 -21
- package/docs/inertia/rendering.md +9 -9
- 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 +23 -10
- 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 +29 -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/config.ts +8 -0
- package/src/index.ts +25 -1
- package/src/install/ArchInstallCommand.ts +46 -3
- package/src/install/guidelines.ts +84 -0
- package/src/install/shape.ts +177 -0
- package/src/install/skills.ts +271 -0
- package/src/provider/ArchProvider.ts +148 -0
- package/src/tools/searchDocs.ts +7 -0
package/docs/responses.md
CHANGED
|
@@ -27,7 +27,7 @@ import type { HttpContext } from "zerotal";
|
|
|
27
27
|
|
|
28
28
|
## JSON
|
|
29
29
|
|
|
30
|
-
```typescript
|
|
30
|
+
```typescript fragment
|
|
31
31
|
// in a controller
|
|
32
32
|
ctx.json(data); // 200 OK — Content-Type: application/json
|
|
33
33
|
ctx.json(data, 201); // 201 Created
|
|
@@ -44,7 +44,7 @@ JSON-serialisable is accepted: plain objects, arrays, ORM models, `null`.
|
|
|
44
44
|
`view()` accepts either pre-rendered markup, or a view component plus its props.
|
|
45
45
|
It prepends `<!DOCTYPE html>` and sets `Content-Type: text/html`:
|
|
46
46
|
|
|
47
|
-
```typescript
|
|
47
|
+
```typescript fragment
|
|
48
48
|
// in a controller
|
|
49
49
|
import { WelcomePage } from "../../resources/views/WelcomePage.tsx";
|
|
50
50
|
|
|
@@ -69,7 +69,7 @@ once for the whole project, and already present in scaffolded apps. See the
|
|
|
69
69
|
|
|
70
70
|
For htmx, Turbo Streams, or any partial render — no DOCTYPE prepended:
|
|
71
71
|
|
|
72
|
-
```typescript
|
|
72
|
+
```typescript fragment
|
|
73
73
|
// in a controller
|
|
74
74
|
ctx.html('<p class="alert">Saved!</p>');
|
|
75
75
|
ctx.html(renderPartial(data), 200);
|
|
@@ -81,7 +81,7 @@ Render a Markdown string to a full HTML page using Bun's built-in
|
|
|
81
81
|
`Bun.markdown.html()`. Tables, strikethrough, tasklists, autolinks, and heading
|
|
82
82
|
IDs are enabled by default:
|
|
83
83
|
|
|
84
|
-
```typescript
|
|
84
|
+
```typescript fragment
|
|
85
85
|
// in a controller
|
|
86
86
|
const content = await Bun.file("./docs/guide.md").text();
|
|
87
87
|
ctx.markdown(content);
|
|
@@ -93,7 +93,7 @@ falling back to `"Docs"`.
|
|
|
93
93
|
|
|
94
94
|
## Redirects
|
|
95
95
|
|
|
96
|
-
```typescript
|
|
96
|
+
```typescript fragment
|
|
97
97
|
// in a controller
|
|
98
98
|
ctx.redirect("/dashboard"); // 302 Found
|
|
99
99
|
ctx.redirect("/dashboard", 303); // 303 See Other ← use after POST/PUT
|
|
@@ -120,7 +120,7 @@ points to a different origin.
|
|
|
120
120
|
|
|
121
121
|
Return a `Response` directly with the appropriate headers:
|
|
122
122
|
|
|
123
|
-
```typescript
|
|
123
|
+
```typescript fragment
|
|
124
124
|
// in a controller
|
|
125
125
|
const file = Bun.file("./exports/report.csv");
|
|
126
126
|
|
|
@@ -136,7 +136,7 @@ ctx.response = new Response(file as unknown as BodyInit, {
|
|
|
136
136
|
|
|
137
137
|
Set `ctx.response` to any `Response` object — the pipeline sends it verbatim:
|
|
138
138
|
|
|
139
|
-
```typescript
|
|
139
|
+
```typescript fragment
|
|
140
140
|
// in a controller
|
|
141
141
|
ctx.response = new Response("pong", { status: 200 });
|
|
142
142
|
|
|
@@ -157,7 +157,7 @@ ctx.response = new Response(stream, {
|
|
|
157
157
|
Every helper assigns a fresh `Response` to `ctx.response`. To add a header,
|
|
158
158
|
rebuild it from the existing one:
|
|
159
159
|
|
|
160
|
-
```typescript
|
|
160
|
+
```typescript fragment
|
|
161
161
|
// in a controller, after setting ctx.response via any helper
|
|
162
162
|
const existing = ctx.response!;
|
|
163
163
|
const headers = new Headers(existing.headers);
|
|
@@ -175,7 +175,7 @@ Flash a message and redirect in one step — the flashed value is available via
|
|
|
175
175
|
`ctx.flashed()` on the next request. Flashing requires an active session
|
|
176
176
|
(see [Session](/docs/session)) and silently no-ops without one:
|
|
177
177
|
|
|
178
|
-
```typescript
|
|
178
|
+
```typescript fragment
|
|
179
179
|
// in a controller
|
|
180
180
|
ctx.flash("success", "Post created!");
|
|
181
181
|
ctx.redirect("/posts", 303);
|
|
@@ -190,7 +190,7 @@ redirects.
|
|
|
190
190
|
Use `wantsJson()` (true when the client sends `Accept: application/json`) to
|
|
191
191
|
respond differently based on what the client accepts:
|
|
192
192
|
|
|
193
|
-
```typescript
|
|
193
|
+
```typescript fragment
|
|
194
194
|
// in a controller
|
|
195
195
|
async destroy(ctx: HttpContext): Promise<void> {
|
|
196
196
|
await post.delete();
|
|
@@ -211,7 +211,7 @@ the controller entirely with `Router.view()`. A static props object is evaluated
|
|
|
211
211
|
once at registration; a factory function receives the `HttpContext` per request
|
|
212
212
|
and may be async:
|
|
213
213
|
|
|
214
|
-
```typescript
|
|
214
|
+
```typescript fragment
|
|
215
215
|
// routes/web.ts
|
|
216
216
|
import { Router } from "zerotal";
|
|
217
217
|
|
|
@@ -244,7 +244,7 @@ sentence you would have written in a comment.
|
|
|
244
244
|
**Assert the status by meaning, not by number**, so a failure says what went
|
|
245
245
|
wrong rather than what integer it saw:
|
|
246
246
|
|
|
247
|
-
```typescript
|
|
247
|
+
```typescript fragment
|
|
248
248
|
// tests/http/posts.test.ts
|
|
249
249
|
import { test } from "bun:test";
|
|
250
250
|
import { createApp } from "../helpers.ts";
|
|
@@ -268,7 +268,7 @@ reach for `assertStatus(n)` only when the code has no name.
|
|
|
268
268
|
how a broken route passes. `assertRedirect` checks the `Location` header;
|
|
269
269
|
`assertJson` parses the body:
|
|
270
270
|
|
|
271
|
-
```typescript
|
|
271
|
+
```typescript fragment
|
|
272
272
|
// tests/http/posts.test.ts
|
|
273
273
|
res.assertRedirect("/posts/hello"); // 3xx + Location
|
|
274
274
|
res.assertJson({ id: 1, title: "Hello" }); // exact body match
|
|
@@ -283,7 +283,7 @@ field is added, which trains people to update tests without reading them.
|
|
|
283
283
|
**Headers and cookies are part of the response contract** when a client depends
|
|
284
284
|
on them:
|
|
285
285
|
|
|
286
|
-
```typescript
|
|
286
|
+
```typescript fragment
|
|
287
287
|
// tests/http/downloads.test.ts
|
|
288
288
|
res.assertHeader("Content-Type", "text/csv");
|
|
289
289
|
res.assertHeader("Content-Disposition");
|
package/docs/roles-and-2fa.md
CHANGED
|
@@ -80,7 +80,7 @@ export default AuthConfig({
|
|
|
80
80
|
|
|
81
81
|
2FA stores three columns on the user. Add a migration:
|
|
82
82
|
|
|
83
|
-
```typescript
|
|
83
|
+
```typescript fragment
|
|
84
84
|
// database/migrations/003_add_two_factor_to_users.ts
|
|
85
85
|
export default class AddTwoFactorToUsers extends Migration {
|
|
86
86
|
async up(schema: Schema): Promise<void> {
|
|
@@ -102,7 +102,7 @@ export default class AddTwoFactorToUsers extends Migration {
|
|
|
102
102
|
|
|
103
103
|
Then expose them on the model:
|
|
104
104
|
|
|
105
|
-
```typescript
|
|
105
|
+
```typescript fragment
|
|
106
106
|
// app/models/User.ts
|
|
107
107
|
@(table("users").withTimestamps())
|
|
108
108
|
export class User extends AuthUser {
|
|
@@ -124,7 +124,7 @@ Enrolment is a two-step flow: generate a secret and show its QR code, then verif
|
|
|
124
124
|
the user's first code before persisting. `TwoFactor.verifyCode()` and
|
|
125
125
|
`TwoFactor.generateRecoveryCodes()` are synchronous.
|
|
126
126
|
|
|
127
|
-
```typescript
|
|
127
|
+
```typescript fragment
|
|
128
128
|
// app/controllers/TwoFactorController.ts
|
|
129
129
|
import { TwoFactor } from "@zerotal/auth";
|
|
130
130
|
|
|
@@ -191,7 +191,7 @@ After login, the challenge controller verifies a TOTP code (falling back to a
|
|
|
191
191
|
recovery code) and marks the session as confirmed. `verifyRecoveryCode()` returns
|
|
192
192
|
the remaining codes synchronously so you can persist the consumed set:
|
|
193
193
|
|
|
194
|
-
```typescript
|
|
194
|
+
```typescript fragment
|
|
195
195
|
// app/controllers/TwoFactorChallengeController.ts
|
|
196
196
|
import { TwoFactor, TWO_FACTOR_SESSION_KEY } from "@zerotal/auth";
|
|
197
197
|
|
|
@@ -242,7 +242,7 @@ export class TwoFactorChallengeController extends Controller {
|
|
|
242
242
|
Add `TwoFactorMiddleware` **after** `AuthMiddleware` in any group that should
|
|
243
243
|
require 2FA:
|
|
244
244
|
|
|
245
|
-
```typescript
|
|
245
|
+
```typescript fragment
|
|
246
246
|
// routes/web.ts
|
|
247
247
|
import { AuthMiddleware, TwoFactorMiddleware } from "@zerotal/auth";
|
|
248
248
|
|
|
@@ -276,7 +276,7 @@ TwoFactorMiddleware.challengeRoute = "/auth/2fa";
|
|
|
276
276
|
`getQrCodeSvg()` returns the scannable code as an inline `<svg>`, drawn in your
|
|
277
277
|
process. Inline it — do not fetch it, and do not log it:
|
|
278
278
|
|
|
279
|
-
```tsx
|
|
279
|
+
```tsx fragment
|
|
280
280
|
// in a Flow page
|
|
281
281
|
const qr = TwoFactor.getQrCodeSvg(user.email, secret, { size: 220 });
|
|
282
282
|
|
|
@@ -303,14 +303,14 @@ anyone enrolling on the device that holds the authenticator needs another way in
|
|
|
303
303
|
— the secret in readable blocks to type, or the `otpauth://` URI from
|
|
304
304
|
`getQrCodeUrl()` as a link, which opens the authenticator app directly:
|
|
305
305
|
|
|
306
|
-
```tsx
|
|
306
|
+
```tsx fragment
|
|
307
307
|
<a href={TwoFactor.getQrCodeUrl(user.email, secret)}>Open in my authenticator</a>
|
|
308
308
|
```
|
|
309
309
|
|
|
310
310
|
To draw the code yourself — to a canvas, a PNG, or your own markup — `encodeQr()`
|
|
311
311
|
returns the module matrix and `qrSvg()` renders one:
|
|
312
312
|
|
|
313
|
-
```ts
|
|
313
|
+
```ts fragment
|
|
314
314
|
import { encodeQr } from "@zerotal/auth";
|
|
315
315
|
|
|
316
316
|
const matrix = encodeQr(TwoFactor.getQrCodeUrl(user.email, secret));
|
|
@@ -359,7 +359,7 @@ test("a recovery code verifies once and is then consumed", () => {
|
|
|
359
359
|
what an authenticator app would show, so the test exercises the TOTP path rather
|
|
360
360
|
than working around it:
|
|
361
361
|
|
|
362
|
-
```typescript
|
|
362
|
+
```typescript fragment
|
|
363
363
|
// tests/http/two-factor.test.ts
|
|
364
364
|
import { test } from "bun:test";
|
|
365
365
|
import { createApp } from "../helpers.ts";
|
package/docs/routing.md
CHANGED
|
@@ -25,7 +25,7 @@ import { Router, route } from "zerotal";
|
|
|
25
25
|
|
|
26
26
|
Register a `GET` route to a controller action, or to an inline closure:
|
|
27
27
|
|
|
28
|
-
```typescript
|
|
28
|
+
```typescript fragment
|
|
29
29
|
// routes/index.ts
|
|
30
30
|
import { Router } from "zerotal";
|
|
31
31
|
|
|
@@ -53,7 +53,7 @@ rules.
|
|
|
53
53
|
|
|
54
54
|
### HTTP verbs
|
|
55
55
|
|
|
56
|
-
```typescript
|
|
56
|
+
```typescript fragment
|
|
57
57
|
// routes/index.ts
|
|
58
58
|
Router.get("/users", UserController, "index");
|
|
59
59
|
Router.post("/users", UserController, "store");
|
|
@@ -64,7 +64,7 @@ Router.delete("/users/:id", UserController, "destroy");
|
|
|
64
64
|
|
|
65
65
|
Each call returns a `RouteRegistration` you can chain:
|
|
66
66
|
|
|
67
|
-
```typescript
|
|
67
|
+
```typescript fragment
|
|
68
68
|
// routes/index.ts
|
|
69
69
|
Router.get("/posts/:slug", PostController, "show").name("posts.show").bind("post", Post);
|
|
70
70
|
```
|
|
@@ -73,7 +73,7 @@ Router.get("/posts/:slug", PostController, "show").name("posts.show").bind("post
|
|
|
73
73
|
|
|
74
74
|
Pass middleware classes as the fourth argument:
|
|
75
75
|
|
|
76
|
-
```typescript
|
|
76
|
+
```typescript fragment
|
|
77
77
|
// routes/index.ts
|
|
78
78
|
Router.get("/dashboard", DashboardController, "index", [AuthMiddleware]);
|
|
79
79
|
Router.post("/posts", PostController, "store", [AuthMiddleware, ThrottleMiddleware]);
|
|
@@ -102,7 +102,7 @@ Router.get("/posts/:slug", (ctx: HttpContext<{ slug: string }>) =>
|
|
|
102
102
|
Middleware is passed as the **third** argument in closure form (there's no action
|
|
103
103
|
name), and the returned `RouteRegistration` still chains `.name()` / `.bind()`:
|
|
104
104
|
|
|
105
|
-
```typescript
|
|
105
|
+
```typescript fragment
|
|
106
106
|
// routes/index.ts
|
|
107
107
|
Router.get("/admin", (ctx) => ctx.json({ ok: true }), [AuthMiddleware]);
|
|
108
108
|
Router.get("/posts/:post", (ctx: HttpContext<{ post: Post }>) =>
|
|
@@ -118,7 +118,7 @@ Router.get("/posts/:post", (ctx: HttpContext<{ post: Post }>) =>
|
|
|
118
118
|
|
|
119
119
|
`Router.resource()` registers all seven RESTful actions in one call:
|
|
120
120
|
|
|
121
|
-
```typescript
|
|
121
|
+
```typescript fragment
|
|
122
122
|
// routes/index.ts
|
|
123
123
|
Router.resource("posts", PostController);
|
|
124
124
|
```
|
|
@@ -138,7 +138,7 @@ are accepted.
|
|
|
138
138
|
|
|
139
139
|
#### Filtering actions
|
|
140
140
|
|
|
141
|
-
```typescript
|
|
141
|
+
```typescript fragment
|
|
142
142
|
// routes/index.ts
|
|
143
143
|
// Register only these actions
|
|
144
144
|
Router.resource("photos", PhotoController).only(["index", "show"]);
|
|
@@ -149,7 +149,7 @@ Router.resource("tags", TagController).except(["create", "edit"]);
|
|
|
149
149
|
|
|
150
150
|
#### Resource middleware
|
|
151
151
|
|
|
152
|
-
```typescript
|
|
152
|
+
```typescript fragment
|
|
153
153
|
// routes/index.ts
|
|
154
154
|
Router.resource("comments", CommentController, [AuthMiddleware]);
|
|
155
155
|
```
|
|
@@ -159,7 +159,7 @@ Router.resource("comments", CommentController, [AuthMiddleware]);
|
|
|
159
159
|
Register a GET route that renders a `@zerotal/core` JSX component directly —
|
|
160
160
|
no controller class needed for simple pages:
|
|
161
161
|
|
|
162
|
-
```typescript
|
|
162
|
+
```typescript fragment
|
|
163
163
|
// routes/index.ts
|
|
164
164
|
import { AboutPage } from "../resources/views/AboutPage.tsx";
|
|
165
165
|
import { DashboardPage } from "../resources/views/DashboardPage.tsx";
|
|
@@ -182,7 +182,7 @@ Router.view("/settings", SettingsPage, (ctx) => ({ user: ctx.user }), [AuthMiddl
|
|
|
182
182
|
|
|
183
183
|
The route chains `.name()` and `.withLayout()`:
|
|
184
184
|
|
|
185
|
-
```typescript
|
|
185
|
+
```typescript fragment
|
|
186
186
|
// routes/index.ts
|
|
187
187
|
Router.view("/about", AboutPage).name("about").withLayout(AppLayout);
|
|
188
188
|
```
|
|
@@ -191,7 +191,7 @@ Router.view("/about", AboutPage).name("about").withLayout(AppLayout);
|
|
|
191
191
|
|
|
192
192
|
Serve a local directory under a URL prefix:
|
|
193
193
|
|
|
194
|
-
```typescript
|
|
194
|
+
```typescript fragment
|
|
195
195
|
// routes/index.ts
|
|
196
196
|
Router.static("/assets", "./public/assets");
|
|
197
197
|
Router.static("/uploads", "public/uploads");
|
|
@@ -211,7 +211,7 @@ is pre-registered at boot as a static `Response` (zero JS per request); pass
|
|
|
211
211
|
|
|
212
212
|
Serve a directory of `.md` files as rendered HTML pages:
|
|
213
213
|
|
|
214
|
-
```typescript
|
|
214
|
+
```typescript fragment
|
|
215
215
|
// routes/index.ts
|
|
216
216
|
Router.markdown("/docs", "./docs");
|
|
217
217
|
|
|
@@ -232,7 +232,7 @@ Register a handler that receives the raw `Request` and bypasses the entire globa
|
|
|
232
232
|
middleware pipeline — no `HttpContext`, no session, no auth. Useful for internal
|
|
233
233
|
health checks or asset endpoints:
|
|
234
234
|
|
|
235
|
-
```typescript
|
|
235
|
+
```typescript fragment
|
|
236
236
|
// routes/index.ts
|
|
237
237
|
Router.raw("GET", "/__ping", () => new Response("pong"));
|
|
238
238
|
Router.raw("GET", "/health", async () => {
|
|
@@ -252,7 +252,7 @@ Router.raw("GET", "/health", async () => {
|
|
|
252
252
|
|
|
253
253
|
Added by the `@zerotal/flow` package:
|
|
254
254
|
|
|
255
|
-
```typescript
|
|
255
|
+
```typescript fragment
|
|
256
256
|
// routes/index.ts
|
|
257
257
|
import { Router } from "zerotal";
|
|
258
258
|
|
|
@@ -267,7 +267,7 @@ See the [Flow](/docs/flow) guide for full details.
|
|
|
267
267
|
Groups apply a shared prefix and/or middleware stack to a set of routes. Groups
|
|
268
268
|
nest — prefix and middleware accumulate.
|
|
269
269
|
|
|
270
|
-
```typescript
|
|
270
|
+
```typescript fragment
|
|
271
271
|
// routes/index.ts
|
|
272
272
|
Router.group({ prefix: "/api/v1" }, () => {
|
|
273
273
|
Router.get("/users", UserController, "index");
|
|
@@ -276,7 +276,7 @@ Router.group({ prefix: "/api/v1" }, () => {
|
|
|
276
276
|
});
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
```typescript
|
|
279
|
+
```typescript fragment
|
|
280
280
|
// routes/index.ts
|
|
281
281
|
Router.group({ prefix: "/api/v1", middleware: AuthMiddleware }, () => {
|
|
282
282
|
Router.resource("posts", PostController);
|
|
@@ -294,7 +294,7 @@ routes declared after it.
|
|
|
294
294
|
Define a group of middleware classes under a string key, then reference it by
|
|
295
295
|
name:
|
|
296
296
|
|
|
297
|
-
```typescript
|
|
297
|
+
```typescript fragment
|
|
298
298
|
// in a ServiceProvider or bootstrap
|
|
299
299
|
Router.middlewareGroup("api", [ThrottleMiddleware, JsonMiddleware]);
|
|
300
300
|
Router.middlewareGroup("web", [SessionMiddleware, CsrfMiddleware]);
|
|
@@ -316,7 +316,7 @@ it everywhere that group is used instead of requiring an edit at every call site
|
|
|
316
316
|
|
|
317
317
|
### Nested groups
|
|
318
318
|
|
|
319
|
-
```typescript
|
|
319
|
+
```typescript fragment
|
|
320
320
|
// routes/index.ts
|
|
321
321
|
Router.group({ prefix: "/admin" }, () => {
|
|
322
322
|
Router.group({ middleware: [AuthMiddleware, AdminMiddleware] }, () => {
|
|
@@ -345,7 +345,7 @@ group.
|
|
|
345
345
|
The `domain` option scopes a group of routes to a specific host. Dynamic `:label`
|
|
346
346
|
segments are captured and exposed on the context via `ctx.subdomains`.
|
|
347
347
|
|
|
348
|
-
```typescript
|
|
348
|
+
```typescript fragment
|
|
349
349
|
// routes/index.ts
|
|
350
350
|
import { Router } from "zerotal";
|
|
351
351
|
|
|
@@ -360,7 +360,7 @@ Router.group({ domain: ":tenant.app.com" }, () => {
|
|
|
360
360
|
});
|
|
361
361
|
```
|
|
362
362
|
|
|
363
|
-
```typescript
|
|
363
|
+
```typescript fragment
|
|
364
364
|
// app/controllers/DashboardController.ts
|
|
365
365
|
class DashboardController {
|
|
366
366
|
index(ctx: HttpContext) {
|
|
@@ -383,7 +383,7 @@ time.
|
|
|
383
383
|
`@zerotal/tenancy`'s `SubdomainResolver` resolves the tenant _model_ from the same
|
|
384
384
|
subdomain a domain group captures, so the two compose directly:
|
|
385
385
|
|
|
386
|
-
```typescript
|
|
386
|
+
```typescript fragment
|
|
387
387
|
// routes/index.ts
|
|
388
388
|
Router.group({ domain: ":tenant.app.com" }, () => {
|
|
389
389
|
Router.get("/dashboard", DashboardController, "index");
|
|
@@ -397,7 +397,7 @@ Configuring the resolver is covered in [Multi-tenancy](/docs/tenancy).
|
|
|
397
397
|
|
|
398
398
|
Assign a name and generate URLs from it:
|
|
399
399
|
|
|
400
|
-
```typescript
|
|
400
|
+
```typescript fragment
|
|
401
401
|
// routes/index.ts
|
|
402
402
|
Router.get("/posts/:slug", PostController, "show").name("posts.show");
|
|
403
403
|
|
|
@@ -436,7 +436,7 @@ route("docs.show", { "*": ["guides", "intro"] }); // → '/docs/guides/intro'
|
|
|
436
436
|
Run `bun zt route:types` and the names above stop being strings the compiler has
|
|
437
437
|
to take on faith:
|
|
438
438
|
|
|
439
|
-
```typescript
|
|
439
|
+
```typescript fragment
|
|
440
440
|
route("psots.show", { slug }); // ✗ not assignable to RouteName
|
|
441
441
|
route("posts.show"); // ✗ Expected 2 arguments, but got 1
|
|
442
442
|
route("posts.show", {}); // ✗ Property 'slug' is missing
|
|
@@ -475,7 +475,7 @@ it always did — every name accepted, nothing checked.
|
|
|
475
475
|
**When the name is not known at compile time** — read from config, chosen by a
|
|
476
476
|
package — use the escape hatch, which does the same work with no checking:
|
|
477
477
|
|
|
478
|
-
```typescript
|
|
478
|
+
```typescript fragment
|
|
479
479
|
route.dynamic(config("app.home_route"), { id });
|
|
480
480
|
```
|
|
481
481
|
|
|
@@ -506,7 +506,7 @@ server renders — a `view` build's `href` attributes and form actions included.
|
|
|
506
506
|
A browser bundle is a different process with no router to read, so there it needs
|
|
507
507
|
the table handed to it once, at your entry point:
|
|
508
508
|
|
|
509
|
-
```typescript
|
|
509
|
+
```typescript fragment
|
|
510
510
|
// resources/js/app.js
|
|
511
511
|
import { defineRoutes } from "zerotal/routes";
|
|
512
512
|
import { ROUTES } from "../../types/routes.generated";
|
|
@@ -516,7 +516,7 @@ defineRoutes(ROUTES);
|
|
|
516
516
|
|
|
517
517
|
From there the call is the one you already know:
|
|
518
518
|
|
|
519
|
-
```typescript
|
|
519
|
+
```typescript fragment
|
|
520
520
|
route("posts.show", { slug }); // → '/posts/hello'
|
|
521
521
|
route("posts.index", {}, { page: 2 }); // → '/posts?page=2'
|
|
522
522
|
```
|
|
@@ -532,7 +532,7 @@ type-checks in a controller type-checks in a component.
|
|
|
532
532
|
`defineRoutes()` also puts `route()` on `globalThis`, so a page, a component or a
|
|
533
533
|
controller calls it with nothing at the top of the file:
|
|
534
534
|
|
|
535
|
-
```tsx
|
|
535
|
+
```tsx fragment
|
|
536
536
|
// no import line
|
|
537
537
|
<a href={route("posts.show", { slug })}>{post.title}</a>
|
|
538
538
|
```
|
|
@@ -585,7 +585,7 @@ Nothing to install, and the names are the same ones the server rendered with.
|
|
|
585
585
|
`route()` gives you a URL. A form needs two things — where to send the request
|
|
586
586
|
and how — and a URL alone leaves the second one to be typed out beside it:
|
|
587
587
|
|
|
588
|
-
```typescript
|
|
588
|
+
```typescript fragment
|
|
589
589
|
// The URL is generated; the verb is a guess that happens to be right today.
|
|
590
590
|
form.post(route("posts.comments.store", { post: id }));
|
|
591
591
|
```
|
|
@@ -593,7 +593,7 @@ form.post(route("posts.comments.store", { post: id }));
|
|
|
593
593
|
`bun zt route:types` also writes a `METHODS` table, so the verb can come from
|
|
594
594
|
the same place the URL does. `action()` returns both:
|
|
595
595
|
|
|
596
|
-
```typescript
|
|
596
|
+
```typescript fragment
|
|
597
597
|
// resources/js/app.js
|
|
598
598
|
import { defineRouteMethods, defineRoutes } from "zerotal/routes";
|
|
599
599
|
import { METHODS, ROUTES } from "../../types/routes.generated";
|
|
@@ -602,7 +602,7 @@ defineRoutes(ROUTES);
|
|
|
602
602
|
defineRouteMethods(METHODS);
|
|
603
603
|
```
|
|
604
604
|
|
|
605
|
-
```typescript
|
|
605
|
+
```typescript fragment
|
|
606
606
|
import { action } from "zerotal/routes";
|
|
607
607
|
|
|
608
608
|
const endpoint = action("posts.comments.store", { post: id });
|
|
@@ -634,7 +634,7 @@ Opt in with `app.fileBasedRouting()` in `bootstrap/app.ts`, passing a map of
|
|
|
634
634
|
named route groups (each `web`/`api` group brings its own default prefix and
|
|
635
635
|
middleware) to directories:
|
|
636
636
|
|
|
637
|
-
```typescript
|
|
637
|
+
```typescript fragment
|
|
638
638
|
// bootstrap/app.ts
|
|
639
639
|
import { Application, basePath } from "zerotal";
|
|
640
640
|
import providers from "./providers";
|
|
@@ -667,7 +667,7 @@ segment); `index` becomes the directory URL.
|
|
|
667
667
|
|
|
668
668
|
A file may export handlers for one or more HTTP verbs:
|
|
669
669
|
|
|
670
|
-
```typescript
|
|
670
|
+
```typescript fragment
|
|
671
671
|
// app/routes/users/[id].ts
|
|
672
672
|
import type { HttpContext } from "zerotal";
|
|
673
673
|
|
|
@@ -725,7 +725,7 @@ Place a `_middleware.ts` file in any directory to protect all routes under it.
|
|
|
725
725
|
The scanner walks from the root down to the file's directory and stacks
|
|
726
726
|
middleware outermost-first:
|
|
727
727
|
|
|
728
|
-
```typescript
|
|
728
|
+
```typescript fragment
|
|
729
729
|
// app/routes/api/v2/me/_middleware.ts
|
|
730
730
|
import { RequireAuthMiddleware } from "../../../middleware/RequireAuthMiddleware.ts";
|
|
731
731
|
|
|
@@ -762,7 +762,7 @@ When `@zerotal/flow` is installed, the file scanner recognises Flow
|
|
|
762
762
|
configuration. Verb handlers (`POST`, `DELETE`, etc.) in the same file still
|
|
763
763
|
register normally alongside the page:
|
|
764
764
|
|
|
765
|
-
```typescript
|
|
765
|
+
```typescript fragment
|
|
766
766
|
// app/flow/pages/(protected)/dashboard.tsx
|
|
767
767
|
import type { HttpContext } from "zerotal";
|
|
768
768
|
|
|
@@ -789,7 +789,7 @@ route param whose name matches a model resolves to a loaded instance with no
|
|
|
789
789
|
configuration. `:user` resolves via `User`, `:post` via `Post`, `:blogPost` via
|
|
790
790
|
`BlogPost` (and a plural `:users` resolves to `User` too, via singularization).
|
|
791
791
|
|
|
792
|
-
```typescript
|
|
792
|
+
```typescript fragment
|
|
793
793
|
// routes/index.ts — nothing to declare:
|
|
794
794
|
Router.get("/users/:user", UserController, "show");
|
|
795
795
|
|
|
@@ -811,7 +811,7 @@ A model that resolves by something other than its primary key says so once, on t
|
|
|
811
811
|
model, rather than at every route that mentions it. Declare
|
|
812
812
|
`static resolveRouteBinding` and it is used wherever that model binds:
|
|
813
813
|
|
|
814
|
-
```typescript
|
|
814
|
+
```typescript fragment
|
|
815
815
|
// app/models/User.ts
|
|
816
816
|
@table("users")
|
|
817
817
|
export class User extends Model {
|
|
@@ -852,7 +852,7 @@ controller**. That ordering is a guarantee, not an implementation detail:
|
|
|
852
852
|
|
|
853
853
|
Two static properties on the model control implicit binding:
|
|
854
854
|
|
|
855
|
-
```typescript
|
|
855
|
+
```typescript fragment
|
|
856
856
|
// app/models/User.ts
|
|
857
857
|
@table("users")
|
|
858
858
|
export class User extends Model {
|
|
@@ -879,7 +879,7 @@ There are exactly two ways to change how a param resolves, and they differ only
|
|
|
879
879
|
scope: put it on the **model** when it is how that model always resolves, or on the
|
|
880
880
|
**route** when it belongs to that one route.
|
|
881
881
|
|
|
882
|
-
```typescript
|
|
882
|
+
```typescript fragment
|
|
883
883
|
// in a controller — either way, it is already resolved
|
|
884
884
|
async show(ctx: HttpContext) {
|
|
885
885
|
const user = ctx.model<User>('user'); // no DB call needed here
|
|
@@ -894,7 +894,7 @@ controller runs.
|
|
|
894
894
|
|
|
895
895
|
Overrides the model's own resolution for a single route:
|
|
896
896
|
|
|
897
|
-
```typescript
|
|
897
|
+
```typescript fragment
|
|
898
898
|
// routes/index.ts
|
|
899
899
|
Router.get("/posts/:post", PostController, "show").name("posts.show").bind("post", Post);
|
|
900
900
|
|
|
@@ -909,7 +909,7 @@ Router.get("/articles/:article", ArticleController, "show").bind("article", (val
|
|
|
909
909
|
The resolved instance is available two ways — via `ctx.model()`, or on
|
|
910
910
|
`ctx.params` under the param's name:
|
|
911
911
|
|
|
912
|
-
```typescript
|
|
912
|
+
```typescript fragment
|
|
913
913
|
// in a controller — via ctx.model()
|
|
914
914
|
async show(ctx: HttpContext) {
|
|
915
915
|
const post = ctx.model<Post>('post');
|
|
@@ -945,7 +945,7 @@ keys — registering the same path twice overwrites the first.
|
|
|
945
945
|
For resource routes, register literal paths **before** dynamic ones when there is
|
|
946
946
|
a naming conflict:
|
|
947
947
|
|
|
948
|
-
```typescript
|
|
948
|
+
```typescript fragment
|
|
949
949
|
// routes/index.ts
|
|
950
950
|
// Correct — /posts/create is matched before /posts/:slug
|
|
951
951
|
Router.get("/posts/create", PostController, "showCreate");
|
|
@@ -994,7 +994,7 @@ Both forms boot the app, so a route a provider registers is included.
|
|
|
994
994
|
|
|
995
995
|
### Programmatic inspection
|
|
996
996
|
|
|
997
|
-
```typescript
|
|
997
|
+
```typescript fragment
|
|
998
998
|
// anywhere after boot
|
|
999
999
|
Router.routes; // ReadonlyMap<string, RouteDefinition>
|
|
1000
1000
|
Router.namedRoutes; // ReadonlyMap<string, string> (name → path)
|
|
@@ -1008,7 +1008,7 @@ Router.middlewareFor("GET", "/dashboard"); // MiddlewareClass[]
|
|
|
1008
1008
|
Use `HttpContext.fake()` to unit-test controllers and route handlers without a
|
|
1009
1009
|
running server:
|
|
1010
1010
|
|
|
1011
|
-
```typescript
|
|
1011
|
+
```typescript fragment
|
|
1012
1012
|
// src/tests/PostController.test.ts
|
|
1013
1013
|
import { HttpContext } from "zerotal";
|
|
1014
1014
|
|
|
@@ -1022,7 +1022,7 @@ await new PostController().show(ctx);
|
|
|
1022
1022
|
For full integration tests, boot the real app with `createTestApp()` from
|
|
1023
1023
|
`@zerotal/testing` and exercise it over real requests:
|
|
1024
1024
|
|
|
1025
|
-
```typescript
|
|
1025
|
+
```typescript fragment
|
|
1026
1026
|
// src/tests/PostTest.ts
|
|
1027
1027
|
import { beforeAll, afterAll, it } from "bun:test";
|
|
1028
1028
|
import { createTestApp, type TestApp } from "@zerotal/testing";
|
package/docs/scaffolding.md
CHANGED
|
@@ -269,7 +269,7 @@ The underlying codemod utilities are exported from `zerotal` for use in your
|
|
|
269
269
|
own generators, install scripts, or migration tools. Each is idempotent — a no-op
|
|
270
270
|
when the change is already present.
|
|
271
271
|
|
|
272
|
-
```ts
|
|
272
|
+
```ts fragment
|
|
273
273
|
// in your own generator or script
|
|
274
274
|
import {
|
|
275
275
|
addImport,
|
package/docs/scheduler.md
CHANGED
|
@@ -80,7 +80,7 @@ cadence with either a `cron` string or the fluent `frequency()` method. Every
|
|
|
80
80
|
`Schedule` subclass under `app/schedules/` is discovered and registered
|
|
81
81
|
automatically — no manual wiring, no central list.
|
|
82
82
|
|
|
83
|
-
```typescript
|
|
83
|
+
```typescript fragment
|
|
84
84
|
// app/schedules/SendDailyReports.ts
|
|
85
85
|
import { Schedule } from "@zerotal/scheduler";
|
|
86
86
|
import { Queue } from "@zerotal/queue";
|
|
@@ -100,7 +100,7 @@ export class SendDailyReports extends Schedule {
|
|
|
100
100
|
Prefer the fluent frequency builder over a raw cron string when it reads better —
|
|
101
101
|
override `frequency()` and return a configured task:
|
|
102
102
|
|
|
103
|
-
```typescript
|
|
103
|
+
```typescript fragment
|
|
104
104
|
// app/schedules/WarmCache.ts
|
|
105
105
|
import { Schedule, type SchedulerBuilder } from "@zerotal/scheduler";
|
|
106
106
|
|
|
@@ -152,7 +152,7 @@ Every setting is an optional property (or method) on your `Schedule` subclass:
|
|
|
152
152
|
> Discovery warns at boot when it sees static schedule config, and
|
|
153
153
|
> `bun zt doctor` reports it.
|
|
154
154
|
|
|
155
|
-
```typescript
|
|
155
|
+
```typescript fragment
|
|
156
156
|
// app/schedules/NightlyBackup.ts
|
|
157
157
|
import { Schedule, type SchedulerBuilder } from "@zerotal/scheduler";
|
|
158
158
|
|
|
@@ -213,7 +213,7 @@ expose fluent cadence methods. Each returns the configured task.
|
|
|
213
213
|
For quick, in-code definitions (e.g. inside a provider) use the `Scheduler` facade,
|
|
214
214
|
which exposes the underlying manager fluently:
|
|
215
215
|
|
|
216
|
-
```typescript
|
|
216
|
+
```typescript fragment
|
|
217
217
|
// in a provider's onBooted()
|
|
218
218
|
import { Scheduler } from "@zerotal/scheduler";
|
|
219
219
|
|
|
@@ -228,7 +228,7 @@ Scheduler.add("rotate-logs", "0 */6 * * *", () => rotateLogs());
|
|
|
228
228
|
`ScheduledTask`, so you can chain the same fluent tuning the class form exposes
|
|
229
229
|
declaratively:
|
|
230
230
|
|
|
231
|
-
```typescript
|
|
231
|
+
```typescript fragment
|
|
232
232
|
// in a provider's onBooted()
|
|
233
233
|
Scheduler.job("nightly-backup", () => runBackup())
|
|
234
234
|
.dailyAt("02:30")
|
|
@@ -332,7 +332,7 @@ is configured (Redis or SQLite via the [lock primitive](/docs/lock)) — also ta
|
|
|
332
332
|
machines. Cross-process locking is **on by default**; pass `{ crossProcess: false }`
|
|
333
333
|
to guard within this process only:
|
|
334
334
|
|
|
335
|
-
```typescript
|
|
335
|
+
```typescript fragment
|
|
336
336
|
// app/schedules/RebuildSearchIndex.ts
|
|
337
337
|
withoutOverlapping = { expiresAfterMinutes: 30 }; // cross-process (default)
|
|
338
338
|
// withoutOverlapping = { crossProcess: false }; // in-process guard only
|
|
@@ -370,7 +370,7 @@ Anything the task writes to `console.log` can be persisted or emailed:
|
|
|
370
370
|
> **Note** — `sendOutputTo` is a facade-only tuning method; on a `Schedule`
|
|
371
371
|
> subclass, use the `appendOutputTo` or `emailOutputTo` properties.
|
|
372
372
|
|
|
373
|
-
```typescript
|
|
373
|
+
```typescript fragment
|
|
374
374
|
// app/schedules/GenerateSitemap.ts
|
|
375
375
|
import { Schedule } from "@zerotal/scheduler";
|
|
376
376
|
|
|
@@ -404,7 +404,7 @@ ScheduledTask.outputMailer = async (email, subject, body) => {
|
|
|
404
404
|
Every run emits a framework event you can listen for to feed metrics, logs, or
|
|
405
405
|
alerts. Subscribe in a provider's `onBooted()`:
|
|
406
406
|
|
|
407
|
-
```typescript
|
|
407
|
+
```typescript fragment
|
|
408
408
|
// in a provider's onBooted()
|
|
409
409
|
import { FrameworkEvents } from "zerotal";
|
|
410
410
|
import { TaskRan, TaskFailed, TaskSkipped } from "@zerotal/scheduler";
|
|
@@ -446,7 +446,7 @@ as data. To keep the scheduler but drop the section, set
|
|
|
446
446
|
`ScheduledTask` exposes introspection getters and a `runNow()` that executes the
|
|
447
447
|
handler immediately, bypassing the cron/time-window guards — ideal in tests:
|
|
448
448
|
|
|
449
|
-
```typescript
|
|
449
|
+
```typescript fragment
|
|
450
450
|
// in a test
|
|
451
451
|
import { Scheduler } from "@zerotal/scheduler";
|
|
452
452
|
|
|
@@ -474,7 +474,7 @@ bun zt worker # starts the queue worker + scheduler
|
|
|
474
474
|
For simpler deployments, `AppServiceProvider.onStarted()` can run inline polling
|
|
475
475
|
instead of a dedicated worker process (skip it when this IS the worker):
|
|
476
476
|
|
|
477
|
-
```typescript
|
|
477
|
+
```typescript fragment
|
|
478
478
|
// app/providers/AppServiceProvider.ts (onStarted)
|
|
479
479
|
override async onStarted(): Promise<void> {
|
|
480
480
|
if (Bun.env.APP_ENV === "worker") return; // dedicated worker handles it
|