@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/orm/lifecycle.md
CHANGED
|
@@ -47,7 +47,7 @@ query → hydrate row → afterFind
|
|
|
47
47
|
Run async callbacks before or after any persistence event. Register them once at
|
|
48
48
|
boot (typically in a service provider's `onBooting()`):
|
|
49
49
|
|
|
50
|
-
```typescript
|
|
50
|
+
```typescript fragment
|
|
51
51
|
// in a ServiceProvider.onBooting()
|
|
52
52
|
import { HookRegistry } from "@zerotal/orm";
|
|
53
53
|
|
|
@@ -76,7 +76,7 @@ HookRegistry.register(Order, "beforeUpdate", async (order) => {
|
|
|
76
76
|
|
|
77
77
|
The signature is:
|
|
78
78
|
|
|
79
|
-
```typescript
|
|
79
|
+
```typescript fragment
|
|
80
80
|
HookRegistry.register<T>(ModelClass: Function, hook: HookName, fn: (model: T) => Promise<void> | void): void
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -108,7 +108,7 @@ names follow the event-tense convention (`creating`/`created`, `updating`/
|
|
|
108
108
|
`updated`, `saving`/`saved`, `deleting`/`deleted`, `retrieved`) — implement only
|
|
109
109
|
the ones you need:
|
|
110
110
|
|
|
111
|
-
```typescript
|
|
111
|
+
```typescript fragment
|
|
112
112
|
// app/observers/UserObserver.ts
|
|
113
113
|
import type { ModelObserver } from "@zerotal/orm";
|
|
114
114
|
|
|
@@ -135,7 +135,7 @@ export class UserObserver implements ModelObserver<User> {
|
|
|
135
135
|
|
|
136
136
|
Register the observer once at boot:
|
|
137
137
|
|
|
138
|
-
```typescript
|
|
138
|
+
```typescript fragment
|
|
139
139
|
// in a ServiceProvider.onBooting()
|
|
140
140
|
import { User } from "#app/models/User.ts";
|
|
141
141
|
import { UserObserver } from "#app/observers/UserObserver.ts";
|
|
@@ -229,7 +229,7 @@ export class Subscription extends Model.using(State) {
|
|
|
229
229
|
Register callbacks that fire after a successful transition. Call `onTransition` in
|
|
230
230
|
a service provider's `onBooting()` so it runs once at startup:
|
|
231
231
|
|
|
232
|
-
```typescript
|
|
232
|
+
```typescript fragment
|
|
233
233
|
// in a ServiceProvider.onBooting()
|
|
234
234
|
// Specific state
|
|
235
235
|
Post.onTransition("published", async (post, { from }) => {
|
|
@@ -245,7 +245,7 @@ Post.onTransition("*", async (post, { from, to }) => {
|
|
|
245
245
|
|
|
246
246
|
### Using transitions
|
|
247
247
|
|
|
248
|
-
```typescript
|
|
248
|
+
```typescript fragment
|
|
249
249
|
// in a controller or service
|
|
250
250
|
// Validates canTransitionTo, runs the guard, saves, then fires callbacks
|
|
251
251
|
await post.transitionTo("review");
|
|
@@ -268,7 +268,7 @@ followed by any `"*"` wildcard callbacks.
|
|
|
268
268
|
|
|
269
269
|
### State machine in a controller
|
|
270
270
|
|
|
271
|
-
```typescript
|
|
271
|
+
```typescript fragment
|
|
272
272
|
// app/controllers/PostController.ts
|
|
273
273
|
import { StateError } from "@zerotal/orm";
|
|
274
274
|
|
|
@@ -295,7 +295,7 @@ observers — useful when consumers live in separate modules. See the
|
|
|
295
295
|
[ORM index page](/docs/orm#bridging-model-events-to-the-app-event-bus) for the
|
|
296
296
|
companion overview.
|
|
297
297
|
|
|
298
|
-
```typescript
|
|
298
|
+
```typescript fragment
|
|
299
299
|
// app/events/PostEvents.ts
|
|
300
300
|
export class PostCreated {
|
|
301
301
|
constructor(public post: Post) {}
|
|
@@ -319,7 +319,7 @@ ORM runs standalone).
|
|
|
319
319
|
|
|
320
320
|
Subscribe using the event bus anywhere in your application:
|
|
321
321
|
|
|
322
|
-
```typescript
|
|
322
|
+
```typescript fragment
|
|
323
323
|
// in a ServiceProvider or listener module
|
|
324
324
|
import { Events } from "zerotal";
|
|
325
325
|
|
|
@@ -336,7 +336,7 @@ queries.
|
|
|
336
336
|
|
|
337
337
|
### Implementing pruning
|
|
338
338
|
|
|
339
|
-
```typescript
|
|
339
|
+
```typescript fragment
|
|
340
340
|
// app/models/AuditLog.ts
|
|
341
341
|
@table("audit_logs")
|
|
342
342
|
export class AuditLog extends Model {
|
|
@@ -354,7 +354,7 @@ export class AuditLog extends Model {
|
|
|
354
354
|
}
|
|
355
355
|
```
|
|
356
356
|
|
|
357
|
-
```typescript
|
|
357
|
+
```typescript fragment
|
|
358
358
|
// in a scheduled task or REPL
|
|
359
359
|
const removed = await AuditLog.prune(); // default chunk size: 1000
|
|
360
360
|
const fewer = await AuditLog.prune(500); // custom chunk size
|
|
@@ -362,7 +362,7 @@ const fewer = await AuditLog.prune(500); // custom chunk size
|
|
|
362
362
|
|
|
363
363
|
The signature is:
|
|
364
364
|
|
|
365
|
-
```typescript
|
|
365
|
+
```typescript fragment
|
|
366
366
|
static prune(chunkSize = 1000): Promise<number>
|
|
367
367
|
```
|
|
368
368
|
|
|
@@ -372,7 +372,7 @@ method throws.
|
|
|
372
372
|
|
|
373
373
|
### Scheduling pruning
|
|
374
374
|
|
|
375
|
-
```typescript
|
|
375
|
+
```typescript fragment
|
|
376
376
|
// bootstrap/app.ts or a scheduler provider
|
|
377
377
|
scheduler
|
|
378
378
|
.job("prune-audit-logs", async () => {
|
package/docs/orm/queries.md
CHANGED
|
@@ -16,7 +16,7 @@ fluent API is available on `DB.table()` for unmodelled tables (see
|
|
|
16
16
|
|
|
17
17
|
## Basic finders
|
|
18
18
|
|
|
19
|
-
```typescript
|
|
19
|
+
```typescript fragment
|
|
20
20
|
// in a controller or service
|
|
21
21
|
// By primary key — returns null if not found
|
|
22
22
|
const user = await User.find(1);
|
|
@@ -48,7 +48,7 @@ const admins = await User.query().where("role", "admin").get<User>();
|
|
|
48
48
|
|
|
49
49
|
## Create, update, delete
|
|
50
50
|
|
|
51
|
-
```typescript
|
|
51
|
+
```typescript fragment
|
|
52
52
|
// in a controller or service
|
|
53
53
|
// INSERT a single record
|
|
54
54
|
const user = await User.create({ name: "Alice", email: "alice@example.com" });
|
|
@@ -81,7 +81,7 @@ export class Post extends Model.using(SoftDeletes) {
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
```typescript
|
|
84
|
+
```typescript fragment
|
|
85
85
|
// in a controller or service
|
|
86
86
|
// Soft delete — sets deleted_at; row hidden from default queries
|
|
87
87
|
await post.delete();
|
|
@@ -108,7 +108,7 @@ await Post.onlyTrashed().get();
|
|
|
108
108
|
`upsert` inserts a row, or updates the named columns when a conflict on `conflictKeys`
|
|
109
109
|
occurs:
|
|
110
110
|
|
|
111
|
-
```typescript
|
|
111
|
+
```typescript fragment
|
|
112
112
|
function upsert(
|
|
113
113
|
data: InsertPayload<T>,
|
|
114
114
|
conflictKeys: (keyof T & string)[],
|
|
@@ -116,7 +116,7 @@ function upsert(
|
|
|
116
116
|
): Promise<void>;
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
```typescript
|
|
119
|
+
```typescript fragment
|
|
120
120
|
// in a controller or service
|
|
121
121
|
// Conflict on email → update name and role; omit updateCols to update every column
|
|
122
122
|
await User.upsert(
|
|
@@ -132,7 +132,7 @@ await User.upsert(
|
|
|
132
132
|
|
|
133
133
|
## Convenience finders
|
|
134
134
|
|
|
135
|
-
```typescript
|
|
135
|
+
```typescript fragment
|
|
136
136
|
// in a controller or service
|
|
137
137
|
// Find or create — returns the existing or newly-created instance
|
|
138
138
|
const user = await User.firstOrCreate(
|
|
@@ -161,7 +161,7 @@ const user = await User.findOrNew(1);
|
|
|
161
161
|
|
|
162
162
|
### Filtering
|
|
163
163
|
|
|
164
|
-
```typescript
|
|
164
|
+
```typescript fragment
|
|
165
165
|
// in a controller or service
|
|
166
166
|
Post.query()
|
|
167
167
|
.where("status", "published")
|
|
@@ -184,7 +184,7 @@ Post.query()
|
|
|
184
184
|
|
|
185
185
|
### Selecting
|
|
186
186
|
|
|
187
|
-
```typescript
|
|
187
|
+
```typescript fragment
|
|
188
188
|
// in a controller or service
|
|
189
189
|
Post.query().select("id", "title", "slug").get();
|
|
190
190
|
Post.query().selectRaw("COUNT(*) as total, MAX(score) as top").get();
|
|
@@ -193,14 +193,14 @@ Post.query().distinct().select("user_id").get();
|
|
|
193
193
|
|
|
194
194
|
### Ordering and limits
|
|
195
195
|
|
|
196
|
-
```typescript
|
|
196
|
+
```typescript fragment
|
|
197
197
|
// in a controller or service
|
|
198
198
|
Post.query().orderBy("published_at", "desc").orderBy("id", "asc").limit(10).offset(20).get();
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
### Joins
|
|
202
202
|
|
|
203
|
-
```typescript
|
|
203
|
+
```typescript fragment
|
|
204
204
|
// in a controller or service
|
|
205
205
|
// Inner join
|
|
206
206
|
Post.query()
|
|
@@ -228,7 +228,7 @@ Post.query()
|
|
|
228
228
|
|
|
229
229
|
### Grouping and aggregates
|
|
230
230
|
|
|
231
|
-
```typescript
|
|
231
|
+
```typescript fragment
|
|
232
232
|
// in a controller or service
|
|
233
233
|
// Terminal aggregates — return a single value
|
|
234
234
|
const total = await Post.query().where("status", "published").count();
|
|
@@ -250,7 +250,7 @@ await DB.table("posts")
|
|
|
250
250
|
|
|
251
251
|
Load aggregate values alongside model instances without extra queries:
|
|
252
252
|
|
|
253
|
-
```typescript
|
|
253
|
+
```typescript fragment
|
|
254
254
|
// in a controller or service
|
|
255
255
|
const posts = await Post.query()
|
|
256
256
|
.withCount("comments")
|
|
@@ -268,7 +268,7 @@ posts[0].commentsAvg_rating; // number | null
|
|
|
268
268
|
|
|
269
269
|
### Relation existence filtering
|
|
270
270
|
|
|
271
|
-
```typescript
|
|
271
|
+
```typescript fragment
|
|
272
272
|
// in a controller or service
|
|
273
273
|
// Posts that have at least one comment
|
|
274
274
|
Post.query().has("comments").get();
|
|
@@ -299,7 +299,7 @@ Post.query()
|
|
|
299
299
|
|
|
300
300
|
Build queries dynamically based on optional inputs without branching `if` statements:
|
|
301
301
|
|
|
302
|
-
```typescript
|
|
302
|
+
```typescript fragment
|
|
303
303
|
// in a controller
|
|
304
304
|
const posts = await Post.query()
|
|
305
305
|
.when(ctx.query("status"), (q, status) => q.where("status", status))
|
|
@@ -314,7 +314,7 @@ making it easy to chain optional filters.
|
|
|
314
314
|
|
|
315
315
|
### Pessimistic locking
|
|
316
316
|
|
|
317
|
-
```typescript
|
|
317
|
+
```typescript fragment
|
|
318
318
|
// in a controller or service
|
|
319
319
|
// Exclusive write lock — SELECT … FOR UPDATE
|
|
320
320
|
await DB.transaction(async (trx) => {
|
|
@@ -346,7 +346,7 @@ const post = await Post.query().where("id", postId).sharedLock().first();
|
|
|
346
346
|
Classic page-number pagination. Best for small-to-medium tables where users jump to
|
|
347
347
|
arbitrary pages:
|
|
348
348
|
|
|
349
|
-
```typescript
|
|
349
|
+
```typescript fragment
|
|
350
350
|
// in a controller
|
|
351
351
|
const page = await Post.query()
|
|
352
352
|
.where("status", "published")
|
|
@@ -365,7 +365,7 @@ const page = await Post.query()
|
|
|
365
365
|
Simple, performant pagination using the last-seen ID as a cursor. Stable against
|
|
366
366
|
inserts/deletes between pages:
|
|
367
367
|
|
|
368
|
-
```typescript
|
|
368
|
+
```typescript fragment
|
|
369
369
|
// in a controller
|
|
370
370
|
const p1 = await Post.query().cursorPaginate({ limit: 20 });
|
|
371
371
|
const p2 = await Post.query().cursorPaginate({ cursor: p1.nextCursor, limit: 20 });
|
|
@@ -379,7 +379,7 @@ const p2 = await Post.query().cursorPaginate({ cursor: p1.nextCursor, limit: 20
|
|
|
379
379
|
Scales to any table size. Uses an indexed column value as the cursor instead of an
|
|
380
380
|
offset. The best choice for infinite scroll and large datasets:
|
|
381
381
|
|
|
382
|
-
```typescript
|
|
382
|
+
```typescript fragment
|
|
383
383
|
// in a controller
|
|
384
384
|
const p1 = await Post.query()
|
|
385
385
|
.where("status", "published")
|
|
@@ -409,7 +409,7 @@ ordering.
|
|
|
409
409
|
|
|
410
410
|
Use these for large datasets to avoid loading thousands of rows into memory at once:
|
|
411
411
|
|
|
412
|
-
```typescript
|
|
412
|
+
```typescript fragment
|
|
413
413
|
// in a console command or job
|
|
414
414
|
// Process in fixed-size batches
|
|
415
415
|
await Post.query().chunk(100, async (posts) => {
|
|
@@ -442,7 +442,7 @@ await Post.query().each(async (post) => {
|
|
|
442
442
|
The builder compiles to SQL without executing, so you can inspect exactly what will
|
|
443
443
|
run:
|
|
444
444
|
|
|
445
|
-
```typescript
|
|
445
|
+
```typescript fragment
|
|
446
446
|
// in a controller or service
|
|
447
447
|
// Compiled SQL with `?` placeholders (no bindings)
|
|
448
448
|
const sql = Post.query().where("status", "published").toSql();
|
|
@@ -465,7 +465,7 @@ Post.query().where("active", 1).dump().get();
|
|
|
465
465
|
|
|
466
466
|
Clone a base query to reuse it with different conditions:
|
|
467
467
|
|
|
468
|
-
```typescript
|
|
468
|
+
```typescript fragment
|
|
469
469
|
// in a controller or service
|
|
470
470
|
const base = Post.query().where("active", 1);
|
|
471
471
|
const admins = await base.clone().where("role", "admin").get();
|
|
@@ -476,7 +476,7 @@ const editors = await base.clone().where("role", "editor").get();
|
|
|
476
476
|
|
|
477
477
|
### Loading and refreshing
|
|
478
478
|
|
|
479
|
-
```typescript
|
|
479
|
+
```typescript fragment
|
|
480
480
|
// in a controller or service
|
|
481
481
|
// Reload a fresh copy from the database (returns a new instance, doesn't mutate)
|
|
482
482
|
const fresh = await post.fresh();
|
|
@@ -503,7 +503,7 @@ await post.loadMax("comments", "score");
|
|
|
503
503
|
|
|
504
504
|
Know which fields have changed since the last database read or save:
|
|
505
505
|
|
|
506
|
-
```typescript
|
|
506
|
+
```typescript fragment
|
|
507
507
|
// in a controller or service
|
|
508
508
|
post.name = "Changed";
|
|
509
509
|
|
|
@@ -534,7 +534,7 @@ post.markDirty("slug");
|
|
|
534
534
|
|
|
535
535
|
### Incrementing and touch
|
|
536
536
|
|
|
537
|
-
```typescript
|
|
537
|
+
```typescript fragment
|
|
538
538
|
// in a controller or service
|
|
539
539
|
await post.increment("views"); // +1
|
|
540
540
|
await post.increment("views", 5); // +5
|
|
@@ -546,7 +546,7 @@ await post.touch();
|
|
|
546
546
|
|
|
547
547
|
### Comparison and copying
|
|
548
548
|
|
|
549
|
-
```typescript
|
|
549
|
+
```typescript fragment
|
|
550
550
|
// in a controller or service
|
|
551
551
|
// True if both are the same model class with the same primary key
|
|
552
552
|
post.is(otherPost); // boolean
|
|
@@ -563,7 +563,7 @@ const copy = post.replicate(["slug", "viewCount"]);
|
|
|
563
563
|
|
|
564
564
|
### Saving without updating timestamps
|
|
565
565
|
|
|
566
|
-
```typescript
|
|
566
|
+
```typescript fragment
|
|
567
567
|
// in a controller or service
|
|
568
568
|
await User.withoutTimestamps(async () => {
|
|
569
569
|
user.role = "admin";
|
|
@@ -599,7 +599,7 @@ Apply them via `withScopes()`. The callback receives a proxy whose methods invok
|
|
|
599
599
|
scope in turn — call them as separate statements (the proxy methods return `void`, so
|
|
600
600
|
they do not chain):
|
|
601
601
|
|
|
602
|
-
```typescript
|
|
602
|
+
```typescript fragment
|
|
603
603
|
// in a controller
|
|
604
604
|
const posts = await Post.query()
|
|
605
605
|
.withScopes((s) => {
|
|
@@ -616,7 +616,7 @@ const posts = await Post.query()
|
|
|
616
616
|
Global scopes are applied automatically to every query on the model. Register them in
|
|
617
617
|
a [service provider's](/docs/providers) `onBooting()`:
|
|
618
618
|
|
|
619
|
-
```typescript
|
|
619
|
+
```typescript fragment
|
|
620
620
|
// in AppServiceProvider.onBooting()
|
|
621
621
|
Post.addGlobalScope("tenant", (q) => q.where("tenant_id", currentTenantId()));
|
|
622
622
|
|
|
@@ -7,7 +7,7 @@ description: Define and query associations between models with relation decorato
|
|
|
7
7
|
|
|
8
8
|
Relation decorators describe how your models connect — one-to-one, one-to-many, many-to-many, through, and polymorphic — so you can traverse and eager-load associated records without writing JOINs by hand.
|
|
9
9
|
|
|
10
|
-
Zerotal's ORM supports all standard Active Record relationship types, plus polymorphic variants and eager loading with constraints. Relations are declared on the model class with decorators imported from `@zerotal/orm`; see [ORM](/docs/orm
|
|
10
|
+
Zerotal's ORM supports all standard Active Record relationship types, plus polymorphic variants and eager loading with constraints. Relations are declared on the model class with decorators imported from `@zerotal/orm`; see [ORM](/docs/orm) for installing and configuring the package.
|
|
11
11
|
|
|
12
12
|
## One-to-one — @hasOne / @belongsTo
|
|
13
13
|
|
|
@@ -35,7 +35,7 @@ export class Profile extends Model {
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
```typescript
|
|
38
|
+
```typescript fragment
|
|
39
39
|
// in a controller
|
|
40
40
|
const user = await User.query().with("profile").findOrFail(1);
|
|
41
41
|
const profile = user.profile; // Profile — no extra query
|
|
@@ -52,7 +52,7 @@ Both `@hasOne` and `@belongsTo` accept a `localKey` (defaults to `"id"`) to over
|
|
|
52
52
|
|
|
53
53
|
A `User` has many `Post`s. The foreign key (`user_id`) lives on the `posts` table:
|
|
54
54
|
|
|
55
|
-
```typescript
|
|
55
|
+
```typescript fragment
|
|
56
56
|
// app/models/User.ts
|
|
57
57
|
@table("users")
|
|
58
58
|
export class User extends Model {
|
|
@@ -69,7 +69,7 @@ export class Post extends Model {
|
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
```typescript
|
|
72
|
+
```typescript fragment
|
|
73
73
|
// in a controller
|
|
74
74
|
const user = await User.query().with("posts").findOrFail(1);
|
|
75
75
|
user.posts; // Post[]
|
|
@@ -84,7 +84,7 @@ const users = await User.query()
|
|
|
84
84
|
|
|
85
85
|
Set or clear a `belongsTo` foreign key without having to know the parent's ID directly:
|
|
86
86
|
|
|
87
|
-
```typescript
|
|
87
|
+
```typescript fragment
|
|
88
88
|
// in a controller
|
|
89
89
|
post.associate("author", user); // sets post.userId = user.id
|
|
90
90
|
await post.save();
|
|
@@ -121,7 +121,7 @@ export class Tag extends Model {
|
|
|
121
121
|
|
|
122
122
|
### Pivot operations
|
|
123
123
|
|
|
124
|
-
```typescript
|
|
124
|
+
```typescript fragment
|
|
125
125
|
// in a controller
|
|
126
126
|
// Attach one or multiple tags
|
|
127
127
|
await post.tags.attach(tagId);
|
|
@@ -144,7 +144,7 @@ await post.tags.toggle(tagId);
|
|
|
144
144
|
|
|
145
145
|
If the pivot table has additional columns, declare them with `withPivot`:
|
|
146
146
|
|
|
147
|
-
```typescript
|
|
147
|
+
```typescript fragment
|
|
148
148
|
// app/models/User.ts
|
|
149
149
|
@manyToMany(() => Role, {
|
|
150
150
|
pivotTable: "user_roles",
|
|
@@ -155,7 +155,7 @@ If the pivot table has additional columns, declare them with `withPivot`:
|
|
|
155
155
|
roles!: ManyToMany<Role>;
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
```typescript
|
|
158
|
+
```typescript fragment
|
|
159
159
|
// in a controller — attach with extra pivot data
|
|
160
160
|
await user.roles.attach(roleId, { assigned_by: adminId, assigned_at: new Date() });
|
|
161
161
|
```
|
|
@@ -166,7 +166,7 @@ await user.roles.attach(roleId, { assigned_by: adminId, assigned_at: new Date()
|
|
|
166
166
|
|
|
167
167
|
Access distant models through an intermediate model. A `Country` has many `Post`s through `User`s:
|
|
168
168
|
|
|
169
|
-
```typescript
|
|
169
|
+
```typescript fragment
|
|
170
170
|
// app/models/Country.ts
|
|
171
171
|
import { hasManyThrough, hasOneThrough } from "@zerotal/orm";
|
|
172
172
|
|
|
@@ -188,7 +188,7 @@ export class Country extends Model {
|
|
|
188
188
|
}
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
```typescript
|
|
191
|
+
```typescript fragment
|
|
192
192
|
// in a controller
|
|
193
193
|
const country = await Country.query().with("posts").findOrFail(1);
|
|
194
194
|
country.posts; // Post[] — no manual JOIN required
|
|
@@ -202,7 +202,7 @@ Polymorphic relationships let a single model belong to multiple other models usi
|
|
|
202
202
|
|
|
203
203
|
A `Comment` can belong to either a `Post` or a `Video`:
|
|
204
204
|
|
|
205
|
-
```typescript
|
|
205
|
+
```typescript fragment
|
|
206
206
|
// app/models/Post.ts
|
|
207
207
|
import {
|
|
208
208
|
morphMany,
|
|
@@ -243,7 +243,7 @@ The `morphName` (`"commentable"`) determines the `commentable_type` and `comment
|
|
|
243
243
|
|
|
244
244
|
> **Warning** — Renaming a `morphMap` key without migrating existing `*_type` rows will break every stored polymorphic association. Keep the keys stable — changing one requires a data migration.
|
|
245
245
|
|
|
246
|
-
```typescript
|
|
246
|
+
```typescript fragment
|
|
247
247
|
// in a controller
|
|
248
248
|
// Eager load polymorphic relations
|
|
249
249
|
const posts = await Post.query().with("comments").get();
|
|
@@ -257,7 +257,7 @@ comment.commentable; // Post | Video
|
|
|
257
257
|
|
|
258
258
|
Share a tagging system across multiple model types through a single `taggables` pivot:
|
|
259
259
|
|
|
260
|
-
```typescript
|
|
260
|
+
```typescript fragment
|
|
261
261
|
// app/models/Post.ts
|
|
262
262
|
import { morphToMany, morphedByMany, type ManyToMany } from "@zerotal/orm";
|
|
263
263
|
|
|
@@ -300,7 +300,7 @@ Always prefer eager loading over lazy loading in loops — it prevents N+1 queri
|
|
|
300
300
|
|
|
301
301
|
> **Tip** — Reach for `.with()` whenever you access a relation across a collection; lazy-loading inside a loop fires one query per row.
|
|
302
302
|
|
|
303
|
-
```typescript
|
|
303
|
+
```typescript fragment
|
|
304
304
|
// in a controller
|
|
305
305
|
// Single relation
|
|
306
306
|
const posts = await Post.query().with("author").get();
|
|
@@ -330,7 +330,7 @@ counted[0].commentsCount; // number
|
|
|
330
330
|
|
|
331
331
|
When you already have a model instance and realise you need a relation:
|
|
332
332
|
|
|
333
|
-
```typescript
|
|
333
|
+
```typescript fragment
|
|
334
334
|
// in a controller
|
|
335
335
|
const post = await Post.findOrFail(1);
|
|
336
336
|
|
|
@@ -345,7 +345,7 @@ await post.loadMissing(["author"]);
|
|
|
345
345
|
|
|
346
346
|
Filter a parent model based on whether its relation exists, without loading the related rows:
|
|
347
347
|
|
|
348
|
-
```typescript
|
|
348
|
+
```typescript fragment
|
|
349
349
|
// in a controller
|
|
350
350
|
// Posts that have at least one comment
|
|
351
351
|
Post.query().has("comments").get();
|
|
@@ -11,7 +11,7 @@ Models serialize to plain JavaScript objects for API responses and `JSON.stringi
|
|
|
11
11
|
|
|
12
12
|
Every `Model` has a `toJSON()` method. It returns a plain object you can pass straight to the `json()` response helper — and `JSON.stringify()` calls it for you automatically:
|
|
13
13
|
|
|
14
|
-
```typescript
|
|
14
|
+
```typescript fragment
|
|
15
15
|
// in a controller
|
|
16
16
|
import { json } from "zerotal";
|
|
17
17
|
import { User } from "../models/User.ts";
|
|
@@ -56,7 +56,7 @@ user.toJSON();
|
|
|
56
56
|
|
|
57
57
|
`visible` takes precedence over `hidden`. When set (non-empty), `toJSON()` includes **only** those keys:
|
|
58
58
|
|
|
59
|
-
```typescript
|
|
59
|
+
```typescript fragment
|
|
60
60
|
// app/models/User.ts
|
|
61
61
|
@table("users")
|
|
62
62
|
export class User extends Model {
|
|
@@ -71,7 +71,7 @@ Use `visible` for models that have many internal columns and you want to be expl
|
|
|
71
71
|
|
|
72
72
|
Include the result of a getter method in `toJSON()`. The getter runs at serialization time:
|
|
73
73
|
|
|
74
|
-
```typescript
|
|
74
|
+
```typescript fragment
|
|
75
75
|
// app/models/User.ts
|
|
76
76
|
@table("users")
|
|
77
77
|
export class User extends Model {
|
|
@@ -97,7 +97,7 @@ user.toJSON();
|
|
|
97
97
|
|
|
98
98
|
Adjust what a specific instance exposes without modifying the class definition. Each method mutates the instance and returns `this`, so they chain and can be passed straight to `json()`:
|
|
99
99
|
|
|
100
|
-
```typescript
|
|
100
|
+
```typescript fragment
|
|
101
101
|
// in a controller
|
|
102
102
|
// Temporarily hide additional fields for this response:
|
|
103
103
|
json(user.makeHidden("email", "phone").toJSON());
|
|
@@ -124,7 +124,7 @@ json(user.makeHidden("password").makeVisible("phoneVerifiedAt").append("isVerifi
|
|
|
124
124
|
- **Appended accessors included** — per `static appends` and any `append()` overrides.
|
|
125
125
|
- **Loaded relations included** — nested models serialize via their own `toJSON()`.
|
|
126
126
|
|
|
127
|
-
```typescript
|
|
127
|
+
```typescript fragment
|
|
128
128
|
// in a controller
|
|
129
129
|
const post = await Post.query().with("author").findOrFail(1);
|
|
130
130
|
|
|
@@ -145,7 +145,7 @@ post.toJSON();
|
|
|
145
145
|
|
|
146
146
|
When you have an array of models, call `toJSON()` on each item or rely on `JSON.stringify()`:
|
|
147
147
|
|
|
148
|
-
```typescript
|
|
148
|
+
```typescript fragment
|
|
149
149
|
// in a controller
|
|
150
150
|
const posts = await Post.query().where("status", "published").get();
|
|
151
151
|
|
|
@@ -160,7 +160,7 @@ json(posts);
|
|
|
160
160
|
|
|
161
161
|
Hidden/visible lists apply per-model and do **not** propagate to nested relations. Each nested model serializes via its own class configuration:
|
|
162
162
|
|
|
163
|
-
```typescript
|
|
163
|
+
```typescript fragment
|
|
164
164
|
// app/models/User.ts and app/models/Post.ts
|
|
165
165
|
@table("users")
|
|
166
166
|
export class User extends Model {
|
|
@@ -182,7 +182,7 @@ post.toJSON();
|
|
|
182
182
|
|
|
183
183
|
For fine-grained, per-endpoint serialization, use API Resources instead of class-level `hidden`/`visible`. A `Resource` wraps a model in `this.resource` and lets you shape the output per route without touching the model class. By default the output is wrapped in a `{ data: ... }` envelope:
|
|
184
184
|
|
|
185
|
-
```typescript
|
|
185
|
+
```typescript fragment
|
|
186
186
|
// app/resources/PostResource.ts
|
|
187
187
|
import { Resource } from "zerotal/http";
|
|
188
188
|
import type { Post } from "../models/Post.ts";
|
|
@@ -201,7 +201,7 @@ export class PostResource extends Resource<Post> {
|
|
|
201
201
|
|
|
202
202
|
Build a single resource with `new PostResource(post)`, then `toJson()` for a plain object or `toResponse()` for a `Response`. Serialize a list with the static `collection()` helper, which takes the resource class first:
|
|
203
203
|
|
|
204
|
-
```typescript
|
|
204
|
+
```typescript fragment
|
|
205
205
|
// in a controller
|
|
206
206
|
import { json } from "zerotal";
|
|
207
207
|
import { PostResource } from "../resources/PostResource.ts";
|
|
@@ -228,7 +228,7 @@ Use model-level `hidden`/`visible` for globally sensitive fields. Use Resources
|
|
|
228
228
|
|
|
229
229
|
## References
|
|
230
230
|
|
|
231
|
-
`toJSON()` and the override methods live on every [`Model`](/docs/orm
|
|
231
|
+
`toJSON()` and the override methods live on every [`Model`](/docs/orm); the `Resource` helpers are exported from `zerotal`.
|
|
232
232
|
|
|
233
233
|
| Member | Signature | Description |
|
|
234
234
|
| ----------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------- |
|
|
@@ -245,7 +245,7 @@ Use model-level `hidden`/`visible` for globally sensitive fields. Use Resources
|
|
|
245
245
|
|
|
246
246
|
## Next steps
|
|
247
247
|
|
|
248
|
-
- [ORM](/docs/orm
|
|
248
|
+
- [ORM](/docs/orm) — model definition, casts, and configuration.
|
|
249
249
|
- [ORM relationships](/docs/orm/relationships) — load the related models you serialize here.
|
|
250
250
|
- [ORM casts](/docs/orm/casts) — control how column values appear in serialized output.
|
|
251
251
|
- [Responses](/docs/responses) — the `json()` helper that calls `toJSON()`.
|
|
@@ -86,7 +86,7 @@ Everything a consumer should import is re-exported from `index.ts`. Keep
|
|
|
86
86
|
implementation files internal; export the manager, the provider, the config
|
|
87
87
|
factory and its shape type, any facade, and the typed error vocabulary.
|
|
88
88
|
|
|
89
|
-
```typescript
|
|
89
|
+
```typescript fragment
|
|
90
90
|
// packages/cache/src/index.ts
|
|
91
91
|
export { CacheManager } from "./CacheManager.ts";
|
|
92
92
|
export { CacheProvider } from "./provider/CacheProvider.ts";
|
|
@@ -106,7 +106,7 @@ The provider is the only thing the application boots. It binds your services int
|
|
|
106
106
|
the container and registers any commands. It must live at `src/provider/` and
|
|
107
107
|
declare both `static provides` and `static environments`.
|
|
108
108
|
|
|
109
|
-
```typescript
|
|
109
|
+
```typescript fragment
|
|
110
110
|
// packages/cache/src/provider/CacheProvider.ts
|
|
111
111
|
import { ServiceProvider } from "@zerotal/core";
|
|
112
112
|
import type { AppEnvironment } from "@zerotal/core";
|
|
@@ -223,7 +223,7 @@ export function CacheConfig(options: Partial<CacheConfigShape> = {}): CacheConfi
|
|
|
223
223
|
Register the namespace for **typed config dot-paths** by augmenting `ConfigRegistry`
|
|
224
224
|
(the config analogue of `ContainerBindings`) at the bottom of `config.ts`:
|
|
225
225
|
|
|
226
|
-
```typescript
|
|
226
|
+
```typescript fragment
|
|
227
227
|
// packages/cache/src/config.ts
|
|
228
228
|
declare module "@zerotal/core" {
|
|
229
229
|
interface ConfigRegistry {
|
|
@@ -291,7 +291,7 @@ import { createFacade } from "@zerotal/core";
|
|
|
291
291
|
export const Cache = createFacade("cache");
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
-
```typescript
|
|
294
|
+
```typescript fragment
|
|
295
295
|
function createFacade<K extends keyof ContainerBindings>(key: K): ContainerBindings[K];
|
|
296
296
|
```
|
|
297
297
|
|
|
@@ -309,7 +309,7 @@ consumers call `Router.flow(...)` as though it shipped with the router. Register
|
|
|
309
309
|
it in `onRegister()` — that runs before route files load, so the method exists by
|
|
310
310
|
the time an app calls it:
|
|
311
311
|
|
|
312
|
-
```typescript
|
|
312
|
+
```typescript fragment
|
|
313
313
|
// packages/flow/src/FlowProvider.ts — inside onRegister():
|
|
314
314
|
Router.macro("flow", flowRoute);
|
|
315
315
|
```
|
|
@@ -317,7 +317,7 @@ Router.macro("flow", flowRoute);
|
|
|
317
317
|
The call is untyped on its own; augment the matching interface so consumers get
|
|
318
318
|
completion and type-checking:
|
|
319
319
|
|
|
320
|
-
```typescript
|
|
320
|
+
```typescript fragment
|
|
321
321
|
// packages/flow/src/types.ts
|
|
322
322
|
declare module "@zerotal/core" {
|
|
323
323
|
interface RouterMacros {
|
|
@@ -337,7 +337,7 @@ boot (the way `app/models` and `app/policies` do), contribute a **concern
|
|
|
337
337
|
descriptor** from your provider's `onRegister()`. Core stays unaware of your
|
|
338
338
|
package — discovery is push-based.
|
|
339
339
|
|
|
340
|
-
```typescript
|
|
340
|
+
```typescript fragment
|
|
341
341
|
// packages/webhooks/src/provider/WebhooksProvider.ts
|
|
342
342
|
import type { ConcernDescriptor } from "@zerotal/core";
|
|
343
343
|
|
|
@@ -375,7 +375,7 @@ declare it and `bun zt dev` runs it beside the server in its own tab. Otherwise
|
|
|
375
375
|
every user of your package has to remember a second terminal, and there is no way
|
|
376
376
|
for you to help them.
|
|
377
377
|
|
|
378
|
-
```typescript
|
|
378
|
+
```typescript fragment
|
|
379
379
|
// packages/webhooks/src/provider/WebhooksProvider.ts
|
|
380
380
|
import type { DevProcessDefinition } from "@zerotal/core";
|
|
381
381
|
|
|
@@ -427,7 +427,7 @@ user sees.
|
|
|
427
427
|
`bun zt doctor` is what a developer (or an agent) runs to find out whether an app
|
|
428
428
|
is wired correctly. Contribute the checks only your package can make:
|
|
429
429
|
|
|
430
|
-
```typescript
|
|
430
|
+
```typescript fragment
|
|
431
431
|
// packages/webhooks/src/provider/WebhooksProvider.ts
|
|
432
432
|
import type { DoctorCheck } from "@zerotal/core";
|
|
433
433
|
|
|
@@ -464,7 +464,7 @@ Every package must ship at least one `*.test.ts` file — the linter treats thei
|
|
|
464
464
|
absence as a high-severity violation. Co-locate tests next to the code they cover
|
|
465
465
|
(`CacheManager.test.ts` beside `CacheManager.ts`) and run them with `bun test`.
|
|
466
466
|
|
|
467
|
-
```typescript
|
|
467
|
+
```typescript fragment
|
|
468
468
|
// packages/cache/src/config.test.ts
|
|
469
469
|
import { test, expect } from "bun:test";
|
|
470
470
|
import { CacheConfig } from "./config.ts";
|