@zerotal/arch 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,336 @@
1
+ ---
2
+ title: Flow Lifecycle Hooks
3
+ description: Where to run code as a component mounts, updates, and tears down.
4
+ ---
5
+
6
+ # Lifecycle Hooks
7
+
8
+ Lifecycle hooks let you run code at precise points in a component's life — on first load, on each WebSocket round-trip, before and after actions, around the render cycle, and on errors. They run server-side and have full access to the database, models, and services.
9
+
10
+ ## Hook reference
11
+
12
+ | Hook | `GET` (initial) | WebSocket (subsequent) |
13
+ | ----------------------------- | --------------- | ------------------------------- |
14
+ | `onBoot()` | ✓ | ✓ |
15
+ | `onMount()` | ✓ | only if `this.refresh()` called |
16
+ | `onHydrate()` | — | ✓ |
17
+ | `onUpdating(prop, val, key?)` | — | ✓ (per client write) |
18
+ | `onUpdated(prop, val, key?)` | — | ✓ (per client write) |
19
+ | `action()` | — | ✓ (the invoked method) |
20
+ | `onUpdate()` | — | ✓ (once, after action) |
21
+ | `onRendering()` | ✓ | ✓ |
22
+ | `render()` | ✓ | ✓ |
23
+ | `onRendered(html)` | ✓ | ✓ |
24
+ | `onDehydrate()` | ✓ | ✓ |
25
+ | `onError(error)` | — | ✓ (on throw) |
26
+
27
+ ## Request flow
28
+
29
+ ```
30
+ GET /page (initial render) WebSocket action frame (subsequent)
31
+ │ │
32
+ ├─ onBoot(ctx) ├─ onBoot(ctx)
33
+ ├─ onMount(ctx) ├─ onHydrate() ← state restored from snapshot
34
+ ├─ onRendering() ├─ onUpdating/onUpdated ← per client-written property
35
+ ├─ render() ├─ action() ← the invoked exposed method
36
+ ├─ onRendered(html) ├─ onUpdate() ← once, after action
37
+ └─ onDehydrate() ├─ onRendering()
38
+ ├─ render()
39
+ ├─ onRendered(html)
40
+ └─ onDehydrate()
41
+ ```
42
+
43
+ `onMount()` is absent from the WebSocket column because it only runs on the initial `GET` — unless the action called `this.refresh()`, which re-inserts it into the WebSocket flow.
44
+
45
+ ## Child component initialisation
46
+
47
+ A child gets its data from its parent, not from the URL. Each prop lands on the field of the same name before `onBoot()` and `onMount()` run, so the field's initialiser is its default and a hook can use the value straight away:
48
+
49
+ ```typescript
50
+ export class CounterWidget extends Component {
51
+ @locked label: string = "Count"; // ← <CounterWidget label="Views" />
52
+ @locked step: number = 1; // ← defaults to 1 when the parent omits it
53
+ @expose count: number = 0;
54
+
55
+ @expose increment(): void {
56
+ this.count += this.step;
57
+ }
58
+
59
+ override async render() {
60
+ return (
61
+ <div class="card">
62
+ <p class="text-sm text-gray-500">{this.label}</p>
63
+ <p class="text-3xl font-bold">{this.count}</p>
64
+ <button onClick={this.increment}>+ {this.step}</button>
65
+ </div>
66
+ );
67
+ }
68
+ }
69
+ ```
70
+
71
+ Props that must survive round-trips need to be `@locked` — that keeps them in the snapshot, and the value is then restored from there rather than re-assigned. A `@reactive` prop is different: the parent re-pushes it whenever it changes, so the child sees the new value on the next round-trip.
72
+
73
+ A child receives the request `HttpContext` in `onBoot(ctx)` / `onMount(ctx)` like any page, so it can still read the session or the signed-in user. What it never reads is the URL's segments — those fill the page, not the pieces inside it, which is what lets a child be dropped anywhere.
74
+
75
+ ## Per-request setup
76
+
77
+ Runs on **every** request: the initial `GET` and every WebSocket update. Use it for setup that must be fresh on every round-trip — resolving the authenticated user from context, initialising i18n, wiring up per-request services:
78
+
79
+ ```typescript
80
+ import { request } from "zerotal";
81
+
82
+ override async onBoot() {
83
+ const ctx = request();
84
+ this.currentUserId = ctx.user?.id ?? 0;
85
+ this.locale = ctx.string("locale", "en") ?? "en";
86
+ }
87
+ ```
88
+
89
+ `onBoot()` runs before both `onMount()` and `onHydrate()`, so state it sets is available in both.
90
+
91
+ Avoid expensive database queries in `onBoot()` — it fires on every round-trip, including fast UI interactions. Reserve those for `onMount()` or `onHydrate()`.
92
+
93
+ ## Loading data
94
+
95
+ Runs once on the initial `GET` render, then is skipped on all subsequent WebSocket updates. It's the primary place to load data for the page.
96
+
97
+ It receives the route `HttpContext` — the same argument a controller action gets — so a page on a dynamic segment reads its [route-model binding](/docs/routing#route-model-binding) straight off `ctx.params` instead of querying for it:
98
+
99
+ ```typescript
100
+ override async onMount({ params: { post } }: HttpContext<{ post: Post }>) {
101
+ this.post = post; // resolved by the router; a missing record 404s before this runs
102
+ }
103
+ ```
104
+
105
+ The context is passed to `onBoot()` too, but only the initial `GET` populates `ctx.params` — see the [warning in Routing](/docs/flow/routing#path-parameters). The argument is optional because a component can also be mounted outside a request (in a test, for example).
106
+
107
+ ```typescript
108
+ override async onMount() {
109
+ const [posts, user] = await Promise.all([
110
+ Post.query()
111
+ .where("status", "published")
112
+ .orderBy("created_at", "desc")
113
+ .limit(20)
114
+ .get(),
115
+ User.findOrFail(this.currentUserId),
116
+ ]);
117
+
118
+ this.posts = posts;
119
+ this.user = user;
120
+ this.total = posts.length;
121
+ }
122
+ ```
123
+
124
+ To force `onMount()` to re-run during a WebSocket action — for example after creating a new record and wanting to reload the list — call `this.refresh()` inside the action:
125
+
126
+ ```typescript
127
+ @expose async createPost(): Promise<void> {
128
+ await Post.create({ title: this.title, body: this.body });
129
+ this.title = "";
130
+ this.body = "";
131
+ this.refresh(); // triggers onMount() on this round-trip
132
+ this.flash("Post published.");
133
+ }
134
+ ```
135
+
136
+ ## Re-deriving state after hydration
137
+
138
+ Runs on every WebSocket round-trip, immediately after state is restored from the snapshot. Use it to re-derive transient or protected state that wasn't persisted in the snapshot:
139
+
140
+ ```typescript
141
+ export class PostEditorPage extends Component {
142
+ @locked postId: number = 0; // persisted in snapshot
143
+ @transient post: Post | null = null; // NOT persisted — reset each round-trip
144
+
145
+ override async onHydrate() {
146
+ // Re-load the full Post model from the database using the persisted ID:
147
+ if (this.postId) {
148
+ this.post = await Post.findOrFail(this.postId);
149
+ }
150
+ }
151
+
152
+ @expose async updateTitle(title: string): Promise<void> {
153
+ if (!this.post) return;
154
+ await this.post.fill({ title }).save();
155
+ this.flash("Title updated.");
156
+ }
157
+ }
158
+ ```
159
+
160
+ This is the correct pattern for holding live model instances on a component: persist only the ID in `@locked`, then re-query the model in `onHydrate()`. The model is always fresh from the database, never stale from a deserialized snapshot.
161
+
162
+ ## Intercepting client writes
163
+
164
+ Fires **before** a client-written property value is applied to the component. Throw to reject the write — the value is discarded, an error is added, and the component re-renders:
165
+
166
+ ```typescript
167
+ override async onUpdating(prop: string, value: unknown, key?: string) {
168
+ // Prevent role escalation
169
+ if (prop === "role" && value === "super_admin") {
170
+ throw new Error("You cannot assign this role.");
171
+ }
172
+
173
+ // Reject negative numbers for any numeric prop
174
+ if (typeof value === "number" && value < 0) {
175
+ throw new Error(`${prop} cannot be negative.`);
176
+ }
177
+ }
178
+ ```
179
+
180
+ `key` is present when the property is an array or object and the client updated a nested path (e.g., `form.email` — `prop` is `"form"`, `key` is `"email"`).
181
+
182
+ ## Reacting after client writes
183
+
184
+ Fires **after** a client-written property is applied. Use it to normalise values, enforce computed side-effects, or trigger cascading updates:
185
+
186
+ ```typescript
187
+ override async onUpdated(prop: string, value: unknown) {
188
+ if (prop === "categoryId") {
189
+ // When the category changes, reload the subcategories
190
+ this.subcategories = await Category.where("parent_id", value).get();
191
+ this.subcategoryId = null;
192
+ }
193
+ }
194
+ ```
195
+
196
+ ## Per-property update hooks
197
+
198
+ Instead of branching on `prop` inside `onUpdating`/`onUpdated`, define a per-property method named `onUpdating<PropName>` or `onUpdated<PropName>` (Pascal-cased). Flow calls it automatically and keeps the generic fallback as a catch-all:
199
+
200
+ ```typescript
201
+ @expose username = "";
202
+ @expose email = "";
203
+ @expose tags: string[] = [];
204
+
205
+ // Called only when the client writes to `username`:
206
+ async onUpdatedUsername(value: string) {
207
+ this.username = value.toLowerCase().trim();
208
+ }
209
+
210
+ // Called only when the client writes to `email`:
211
+ async onUpdatingEmail(value: string) {
212
+ if (!value.includes("@")) {
213
+ throw new Error("Not a valid email address.");
214
+ }
215
+ }
216
+
217
+ // Generic fallback — fired for any property not handled above:
218
+ async onUpdating(prop: string, value: unknown) {
219
+ if (prop === "role" && value === "super_admin") {
220
+ throw new Error("You cannot set this role.");
221
+ }
222
+ }
223
+ ```
224
+
225
+ The per-property form is cleaner and TypeScript-friendly: the parameter type matches the property type rather than `unknown`.
226
+
227
+ ## After action, before render
228
+
229
+ Runs once after the invoked action completes, before the render cycle. Use it to apply cross-cutting logic that should happen after any action:
230
+
231
+ ```typescript
232
+ override async onUpdate() {
233
+ // Always log the current state to the audit trail after any action:
234
+ await AuditLog.create({
235
+ component: this.constructor.name,
236
+ userId: this.currentUserId,
237
+ snapshotSum: JSON.stringify(this).length,
238
+ });
239
+ }
240
+ ```
241
+
242
+ Unlike `onUpdated` (which fires per property, before the action), `onUpdate()` fires once per request, after the action returns.
243
+
244
+ ## Before render
245
+
246
+ Runs immediately before `render()` on every request (initial and WebSocket). Use it for template-level setup that shouldn't be in `render()` itself — resolving shared view data, picking a layout variant, etc.:
247
+
248
+ ```typescript
249
+ override async onRendering() {
250
+ // Decide which layout variant to use based on the user's subscription
251
+ if (this.user?.isPro) {
252
+ this.layoutVariant = "pro";
253
+ }
254
+ }
255
+ ```
256
+
257
+ Avoid async database calls here unless truly necessary — `onMount()` and `onHydrate()` are the right places to load data. `onRendering()` is for lightweight, synchronous prep.
258
+
259
+ ## After render
260
+
261
+ Receives the rendered HTML string. Use it to post-process the output, measure render time, or send the HTML to a cache:
262
+
263
+ ```typescript
264
+ override async onRendered(html: string) {
265
+ // Log very long renders for investigation
266
+ if (html.length > 100_000) {
267
+ await Logger.warn("flow.render.large", {
268
+ component: this.constructor.name,
269
+ bytes: html.length,
270
+ });
271
+ }
272
+ }
273
+ ```
274
+
275
+ The `html` parameter is the raw HTML of this component only — not the full page. Do not mutate it here; return value is ignored.
276
+
277
+ ## Normalising before snapshot
278
+
279
+ Runs just before the component state is serialised into the snapshot at the end of every request. Use it to strip sensitive or ephemeral state that shouldn't be persisted:
280
+
281
+ ```typescript
282
+ override async onDehydrate() {
283
+ // Never persist raw upload paths between round-trips
284
+ this.tempUploadPath = null;
285
+
286
+ // Strip the full user object — only the ID needs to survive
287
+ this.userObject = null;
288
+
289
+ // Trim large arrays before snapshot to keep payload size down
290
+ if (this.logBuffer.length > 100) {
291
+ this.logBuffer = this.logBuffer.slice(-100);
292
+ }
293
+ }
294
+ ```
295
+
296
+ After `onDehydrate()`, the snapshot is signed and sent to the browser as an encrypted opaque blob. The next round-trip restores it before `onHydrate()` runs.
297
+
298
+ ## Custom error handling
299
+
300
+ Called when an `@expose`d action throws an unhandled error. The default behaviour flashes the error message with level `"error"`. Override to log to an error tracker or display a custom message:
301
+
302
+ ```typescript
303
+ override async onError(error: Error) {
304
+ // Log to your error tracker
305
+ await Sentry.captureException(error, {
306
+ extra: {
307
+ component: this.constructor.name,
308
+ userId: this.currentUserId,
309
+ },
310
+ });
311
+
312
+ // Show a user-friendly message instead of the raw error text
313
+ this.flash("Something went wrong. Our team has been notified.", "error");
314
+ }
315
+ ```
316
+
317
+ If you want some errors to propagate normally and only handle specific types:
318
+
319
+ ```typescript
320
+ override async onError(error: Error) {
321
+ if (error instanceof DatabaseConnectionError) {
322
+ this.flash("Database is temporarily unavailable. Please try again.", "error");
323
+ return;
324
+ }
325
+
326
+ // Re-throw everything else to get the default flash behaviour
327
+ throw error;
328
+ }
329
+ ```
330
+
331
+ `onError()` does not run during the initial `GET` render — if `onMount()` throws, the error propagates to the HTTP layer and results in a 500 response. It only runs during WebSocket action frames.
332
+
333
+ ## Next steps
334
+
335
+ - [Flow overview](/docs/flow) — the guide's front page and the rest of the sections.
336
+ - [Reference](/docs/flow/references) — every decorator, prop, and directive in one table.