@warlock.js/cascade 5.23.2 → 5.25.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 +352 -329
- package/README.md +101 -101
- package/bin/cascade.js +0 -0
- package/cjs/index.cjs +208 -69
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-driver.contract.d.mts +22 -1
- package/esm/contracts/database-driver.contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +1 -1
- package/esm/data-source/data-source-registry.d.mts +8 -6
- package/esm/data-source/data-source-registry.d.mts.map +1 -1
- package/esm/data-source/data-source-registry.mjs +7 -1
- package/esm/data-source/data-source-registry.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +13 -0
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +71 -2
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +2 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +9 -0
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +61 -5
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts +1 -4
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +4 -5
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/types.d.mts +5 -0
- package/esm/drivers/postgres/types.d.mts.map +1 -1
- package/esm/errors/unsafe-filter.error.mjs.map +1 -1
- package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -1
- package/esm/errors/unsupported-lock-for-update.error.d.mts +11 -0
- package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -0
- package/esm/errors/unsupported-lock-for-update.error.mjs +16 -0
- package/esm/errors/unsupported-lock-for-update.error.mjs.map +1 -0
- package/esm/events/model-events.mjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +2 -1
- package/esm/migration/migration-runner.mjs.map +1 -1
- package/esm/migration/sql-grammar.mjs.map +1 -1
- package/esm/model/generate-model-id.mjs +23 -0
- package/esm/model/generate-model-id.mjs.map +1 -0
- package/esm/model/methods/delete-methods.mjs.map +1 -1
- package/esm/model/methods/meta-methods.mjs.map +1 -1
- package/esm/model/methods/query-methods.mjs +25 -2
- package/esm/model/methods/query-methods.mjs.map +1 -1
- package/esm/model/methods/write-methods.mjs.map +1 -1
- package/esm/query-builder/query-builder.mjs +1 -1
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/esm/utils/sanitize-filter.mjs.map +1 -1
- package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -1
- package/esm/writer/database-writer.d.mts +0 -33
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs +4 -52
- package/esm/writer/database-writer.mjs.map +1 -1
- package/llms-full.txt +2835 -2783
- package/package.json +5 -5
- package/skills/aggregate-data/SKILL.md +165 -165
- package/skills/alter-migration/SKILL.md +531 -531
- package/skills/configure-delete-strategy/SKILL.md +130 -130
- package/skills/define-model/SKILL.md +205 -205
- package/skills/define-relations/SKILL.md +171 -171
- package/skills/manage-data-sources/SKILL.md +177 -140
- package/skills/manage-transactions/SKILL.md +233 -208
- package/skills/paginate-results/SKILL.md +122 -122
- package/skills/perform-atomic-ops/SKILL.md +156 -156
- package/skills/query-data/SKILL.md +213 -213
- package/skills/run-cascade-cli/SKILL.md +125 -125
- package/skills/search-by-vector/SKILL.md +127 -127
- package/skills/subscribe-to-model-events/SKILL.md +148 -148
- package/skills/track-changes/SKILL.md +127 -127
- package/skills/write-migration/SKILL.md +206 -206
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: define-relations
|
|
3
|
-
description: 'Define and query relations — `@BelongsTo` / `@HasMany` / `@BelongsToMany`, `.with("relation")` eager loading, `.whereHas(relation, cb)` filter-by-related, `setRelation` on save, `.joinWith` for SQL joins, `loadRelation`, `lazy(() => Model)`. Triggers: `@BelongsTo`, `@HasMany`, `@BelongsToMany`, `.with`, `.whereHas`, `setRelation`, `.joinWith`, `lazy`; "define a relation", "avoid N+1", "eager load posts", "filter parents by child"; typical import `import { BelongsTo, HasMany, BelongsToMany } from "@warlock.js/cascade"`. Skip: model basics — `@warlock.js/cascade/define-model/SKILL.md`; competing libs `mongoose populate`, `prisma include`, `typeorm relations`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Define and query relations
|
|
7
|
-
|
|
8
|
-
Relations are decorators on model fields. Cascade's global registry (populated by `@RegisterModel()`) lets each relation look up its peer by name — so `@BelongsTo("User")` finds the `User` class without an import (which avoids circular-dep hell when two models reference each other).
|
|
9
|
-
|
|
10
|
-
## Three core relation types
|
|
11
|
-
|
|
12
|
-
### `@BelongsTo(target)` — the foreign-key side
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
import { BelongsTo, Model, RegisterModel } from "@warlock.js/cascade";
|
|
16
|
-
|
|
17
|
-
@RegisterModel()
|
|
18
|
-
export class Post extends Model<PostSchema> {
|
|
19
|
-
public static table = "posts";
|
|
20
|
-
public static schema = postSchema;
|
|
21
|
-
|
|
22
|
-
@BelongsTo("User", { foreignKey: "author_id" })
|
|
23
|
-
public author!: User;
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`Post.belongs_to(User)`. The `author_id` column lives on `posts`. Pass the model name (string) for late-bound lookup, or a `lazy(() => User)` thunk if you need explicit type checking. `lazy` comes from `@mongez/reinforcements` (`import { lazy } from "@mongez/reinforcements"`), not from cascade.
|
|
28
|
-
|
|
29
|
-
### `@HasMany(target)` — the inverse, one-to-many
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
@RegisterModel()
|
|
33
|
-
export class User extends Model<UserSchema> {
|
|
34
|
-
public static table = "users";
|
|
35
|
-
public static schema = userSchema;
|
|
36
|
-
|
|
37
|
-
@HasMany("Post", { foreignKey: "author_id" })
|
|
38
|
-
public posts!: Post[];
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
`User.has_many(Post)`. Same `author_id` column on `posts` — the relation is described from both ends so eager loading and filtering work in both directions.
|
|
43
|
-
|
|
44
|
-
### `@BelongsToMany(target, options)` — many-to-many via a pivot
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
@RegisterModel()
|
|
48
|
-
export class User extends Model<UserSchema> {
|
|
49
|
-
// ...
|
|
50
|
-
|
|
51
|
-
@BelongsToMany("Role", {
|
|
52
|
-
pivot: "user_roles",
|
|
53
|
-
localKey: "user_id", // pivot column → this model (User)
|
|
54
|
-
foreignKey: "role_id", // pivot column → the related model (Role)
|
|
55
|
-
})
|
|
56
|
-
public roles!: Role[];
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`User.belongs_to_many(Role)` via the `user_roles` pivot table with `user_id` + `role_id`. The pivot's two columns are `localKey` (this model's FK) and `foreignKey` (the related model's FK) — there is no `relatedKey` option. Both sides of the many-to-many declare the relation (symmetric).
|
|
61
|
-
|
|
62
|
-
#### Pivot operations
|
|
63
|
-
|
|
64
|
-
Manage pivot rows through `model.pivot(relation)`, which returns the relation's `PivotOperations` handle:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
await user.pivot("roles").attach([adminId, editorId]); // add (skips existing)
|
|
68
|
-
await user.pivot("roles").attach([adminId], { addedBy: actorId }); // + extra pivot columns
|
|
69
|
-
await user.pivot("roles").detach([editorId]); // remove a subset
|
|
70
|
-
await user.pivot("roles").detach(); // remove all
|
|
71
|
-
await user.pivot("roles").sync([adminId]); // replace the whole set
|
|
72
|
-
await user.pivot("roles").toggle([adminId]); // flip each id
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
`model.attach(relation, ids, pivotData?)` and `model.detach(relation, ids?)` are thin shortcuts for the two most common ops; `.sync()` / `.toggle()` live only on the `pivot(relation)` handle (or the standalone `createPivotOperations(model, relation)`). Passing a non-`belongsToMany` relation throws. Routing through `model.pivot(relation)` keeps the join-table `.sync()` distinct from `Model.sync(Target, field)` (the denormalization-embed feature). Pivot ops run direct driver writes — no model lifecycle events fire on the related model.
|
|
76
|
-
|
|
77
|
-
## Eager loading — `.with(relation)`
|
|
78
|
-
|
|
79
|
-
Load related models in the same query, avoiding N+1:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
const posts = await Post.with("author").get();
|
|
83
|
-
|
|
84
|
-
for (const post of posts) {
|
|
85
|
-
post.getRelation("author"); // already loaded — no second query
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Multiple relations:
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
await User.with("posts", "roles").get();
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Nested relations via dot:
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
await User.with("posts.comments").get();
|
|
99
|
-
// Loads users → their posts → comments for each post — all in 3 queries, not N²
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Filtering by related conditions — `.whereHas`
|
|
103
|
-
|
|
104
|
-
Filter parents based on conditions on children:
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
const usersWithPublishedPosts = await User
|
|
108
|
-
.whereHas("posts", (query) => {
|
|
109
|
-
query.where("status", "published");
|
|
110
|
-
})
|
|
111
|
-
.get();
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
`.whereHas(relation, callback)` runs the callback on a query builder scoped to the related model. Use when "find Xs that have at least one Y matching Z."
|
|
115
|
-
|
|
116
|
-
## Setting a relation on save — `setRelation`
|
|
117
|
-
|
|
118
|
-
When you persist a model that ALSO needs to wire a relation slot at the same time, use `setRelation` — NOT `.set()`:
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
const post = new Post();
|
|
122
|
-
post.merge({ title: "Hello", body: "..." });
|
|
123
|
-
post.setRelation("author", currentUser);
|
|
124
|
-
await post.save();
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Why not `post.set("author", currentUser)`? `set` treats the value as a column write — for a relation slot, you'd be writing the User instance into a column. `setRelation` knows it's a relation, picks the right foreign-key column, and stores the ID.
|
|
128
|
-
|
|
129
|
-
## Reading loaded relations
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
const post = await Post.with("author").first();
|
|
133
|
-
|
|
134
|
-
post.getRelation("author"); // typed access to the loaded User
|
|
135
|
-
post.author; // direct field — only if you declared it as a typed field
|
|
136
|
-
|
|
137
|
-
// Without eager loading — load on demand, then read:
|
|
138
|
-
await post.load("author");
|
|
139
|
-
const author = post.getRelation("author");
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
`getRelation("name")` returns the loaded relation (or null if not loaded). `post.load("relation", ...)` lazy-loads one or more relations onto the instance and returns the model (`this`) — read the value back with `getRelation` afterward. Useful when you didn't `.with()` upfront and need a relation conditionally.
|
|
143
|
-
|
|
144
|
-
## Joins — when eager loading isn't enough
|
|
145
|
-
|
|
146
|
-
`.with()` runs separate queries and hydrates relations. `.joinWith()` joins at the SQL/aggregation level:
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
const result = await User
|
|
150
|
-
.joinWith("posts")
|
|
151
|
-
.where("posts.status", "published")
|
|
152
|
-
.get();
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Use `.joinWith` when you need conditions across both tables in a single query. Use `.with` when you want hydrated relation models without joining.
|
|
156
|
-
|
|
157
|
-
## Sync — embedded relations stay fresh
|
|
158
|
-
|
|
159
|
-
For embedded/denormalized relations (cached `author_name` on posts, etc.), Cascade's sync system keeps the embedded copy in step with the source-of-truth via `Model.sync(Target, field)` and the `@warlock.js/cascade` sync helpers.
|
|
160
|
-
|
|
161
|
-
## Things NOT to do
|
|
162
|
-
|
|
163
|
-
- Don't `post.set("author", user)`. Use `setRelation("author", user)`. `set` treats the value as a column write; `setRelation` picks the right foreign key.
|
|
164
|
-
- Don't load all parents and iterate to fetch each child — that's the N+1 problem. Eager-load with `.with(...)` (or load specific children with `.load(...)` after the parent fetch).
|
|
165
|
-
- Don't define a relation on only one side. Both ends of `BelongsTo`/`HasMany` and both sides of `BelongsToMany` need the decorator for queries to work bidirectionally.
|
|
166
|
-
- Don't import the related class for `@BelongsTo` if it would create a circular dependency. Use the string form (`@BelongsTo("User")`) or `lazy(() => User)`.
|
|
167
|
-
|
|
168
|
-
## See also
|
|
169
|
-
|
|
170
|
-
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — `.whereHas` and other filters
|
|
171
|
-
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `@RegisterModel`, the registry that makes name-based lookup work
|
|
1
|
+
---
|
|
2
|
+
name: define-relations
|
|
3
|
+
description: 'Define and query relations — `@BelongsTo` / `@HasMany` / `@BelongsToMany`, `.with("relation")` eager loading, `.whereHas(relation, cb)` filter-by-related, `setRelation` on save, `.joinWith` for SQL joins, `loadRelation`, `lazy(() => Model)`. Triggers: `@BelongsTo`, `@HasMany`, `@BelongsToMany`, `.with`, `.whereHas`, `setRelation`, `.joinWith`, `lazy`; "define a relation", "avoid N+1", "eager load posts", "filter parents by child"; typical import `import { BelongsTo, HasMany, BelongsToMany } from "@warlock.js/cascade"`. Skip: model basics — `@warlock.js/cascade/define-model/SKILL.md`; competing libs `mongoose populate`, `prisma include`, `typeorm relations`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Define and query relations
|
|
7
|
+
|
|
8
|
+
Relations are decorators on model fields. Cascade's global registry (populated by `@RegisterModel()`) lets each relation look up its peer by name — so `@BelongsTo("User")` finds the `User` class without an import (which avoids circular-dep hell when two models reference each other).
|
|
9
|
+
|
|
10
|
+
## Three core relation types
|
|
11
|
+
|
|
12
|
+
### `@BelongsTo(target)` — the foreign-key side
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { BelongsTo, Model, RegisterModel } from "@warlock.js/cascade";
|
|
16
|
+
|
|
17
|
+
@RegisterModel()
|
|
18
|
+
export class Post extends Model<PostSchema> {
|
|
19
|
+
public static table = "posts";
|
|
20
|
+
public static schema = postSchema;
|
|
21
|
+
|
|
22
|
+
@BelongsTo("User", { foreignKey: "author_id" })
|
|
23
|
+
public author!: User;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`Post.belongs_to(User)`. The `author_id` column lives on `posts`. Pass the model name (string) for late-bound lookup, or a `lazy(() => User)` thunk if you need explicit type checking. `lazy` comes from `@mongez/reinforcements` (`import { lazy } from "@mongez/reinforcements"`), not from cascade.
|
|
28
|
+
|
|
29
|
+
### `@HasMany(target)` — the inverse, one-to-many
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
@RegisterModel()
|
|
33
|
+
export class User extends Model<UserSchema> {
|
|
34
|
+
public static table = "users";
|
|
35
|
+
public static schema = userSchema;
|
|
36
|
+
|
|
37
|
+
@HasMany("Post", { foreignKey: "author_id" })
|
|
38
|
+
public posts!: Post[];
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`User.has_many(Post)`. Same `author_id` column on `posts` — the relation is described from both ends so eager loading and filtering work in both directions.
|
|
43
|
+
|
|
44
|
+
### `@BelongsToMany(target, options)` — many-to-many via a pivot
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
@RegisterModel()
|
|
48
|
+
export class User extends Model<UserSchema> {
|
|
49
|
+
// ...
|
|
50
|
+
|
|
51
|
+
@BelongsToMany("Role", {
|
|
52
|
+
pivot: "user_roles",
|
|
53
|
+
localKey: "user_id", // pivot column → this model (User)
|
|
54
|
+
foreignKey: "role_id", // pivot column → the related model (Role)
|
|
55
|
+
})
|
|
56
|
+
public roles!: Role[];
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`User.belongs_to_many(Role)` via the `user_roles` pivot table with `user_id` + `role_id`. The pivot's two columns are `localKey` (this model's FK) and `foreignKey` (the related model's FK) — there is no `relatedKey` option. Both sides of the many-to-many declare the relation (symmetric).
|
|
61
|
+
|
|
62
|
+
#### Pivot operations
|
|
63
|
+
|
|
64
|
+
Manage pivot rows through `model.pivot(relation)`, which returns the relation's `PivotOperations` handle:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
await user.pivot("roles").attach([adminId, editorId]); // add (skips existing)
|
|
68
|
+
await user.pivot("roles").attach([adminId], { addedBy: actorId }); // + extra pivot columns
|
|
69
|
+
await user.pivot("roles").detach([editorId]); // remove a subset
|
|
70
|
+
await user.pivot("roles").detach(); // remove all
|
|
71
|
+
await user.pivot("roles").sync([adminId]); // replace the whole set
|
|
72
|
+
await user.pivot("roles").toggle([adminId]); // flip each id
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`model.attach(relation, ids, pivotData?)` and `model.detach(relation, ids?)` are thin shortcuts for the two most common ops; `.sync()` / `.toggle()` live only on the `pivot(relation)` handle (or the standalone `createPivotOperations(model, relation)`). Passing a non-`belongsToMany` relation throws. Routing through `model.pivot(relation)` keeps the join-table `.sync()` distinct from `Model.sync(Target, field)` (the denormalization-embed feature). Pivot ops run direct driver writes — no model lifecycle events fire on the related model.
|
|
76
|
+
|
|
77
|
+
## Eager loading — `.with(relation)`
|
|
78
|
+
|
|
79
|
+
Load related models in the same query, avoiding N+1:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const posts = await Post.with("author").get();
|
|
83
|
+
|
|
84
|
+
for (const post of posts) {
|
|
85
|
+
post.getRelation("author"); // already loaded — no second query
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Multiple relations:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
await User.with("posts", "roles").get();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Nested relations via dot:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
await User.with("posts.comments").get();
|
|
99
|
+
// Loads users → their posts → comments for each post — all in 3 queries, not N²
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Filtering by related conditions — `.whereHas`
|
|
103
|
+
|
|
104
|
+
Filter parents based on conditions on children:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const usersWithPublishedPosts = await User
|
|
108
|
+
.whereHas("posts", (query) => {
|
|
109
|
+
query.where("status", "published");
|
|
110
|
+
})
|
|
111
|
+
.get();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`.whereHas(relation, callback)` runs the callback on a query builder scoped to the related model. Use when "find Xs that have at least one Y matching Z."
|
|
115
|
+
|
|
116
|
+
## Setting a relation on save — `setRelation`
|
|
117
|
+
|
|
118
|
+
When you persist a model that ALSO needs to wire a relation slot at the same time, use `setRelation` — NOT `.set()`:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const post = new Post();
|
|
122
|
+
post.merge({ title: "Hello", body: "..." });
|
|
123
|
+
post.setRelation("author", currentUser);
|
|
124
|
+
await post.save();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Why not `post.set("author", currentUser)`? `set` treats the value as a column write — for a relation slot, you'd be writing the User instance into a column. `setRelation` knows it's a relation, picks the right foreign-key column, and stores the ID.
|
|
128
|
+
|
|
129
|
+
## Reading loaded relations
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const post = await Post.with("author").first();
|
|
133
|
+
|
|
134
|
+
post.getRelation("author"); // typed access to the loaded User
|
|
135
|
+
post.author; // direct field — only if you declared it as a typed field
|
|
136
|
+
|
|
137
|
+
// Without eager loading — load on demand, then read:
|
|
138
|
+
await post.load("author");
|
|
139
|
+
const author = post.getRelation("author");
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`getRelation("name")` returns the loaded relation (or null if not loaded). `post.load("relation", ...)` lazy-loads one or more relations onto the instance and returns the model (`this`) — read the value back with `getRelation` afterward. Useful when you didn't `.with()` upfront and need a relation conditionally.
|
|
143
|
+
|
|
144
|
+
## Joins — when eager loading isn't enough
|
|
145
|
+
|
|
146
|
+
`.with()` runs separate queries and hydrates relations. `.joinWith()` joins at the SQL/aggregation level:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const result = await User
|
|
150
|
+
.joinWith("posts")
|
|
151
|
+
.where("posts.status", "published")
|
|
152
|
+
.get();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use `.joinWith` when you need conditions across both tables in a single query. Use `.with` when you want hydrated relation models without joining.
|
|
156
|
+
|
|
157
|
+
## Sync — embedded relations stay fresh
|
|
158
|
+
|
|
159
|
+
For embedded/denormalized relations (cached `author_name` on posts, etc.), Cascade's sync system keeps the embedded copy in step with the source-of-truth via `Model.sync(Target, field)` and the `@warlock.js/cascade` sync helpers.
|
|
160
|
+
|
|
161
|
+
## Things NOT to do
|
|
162
|
+
|
|
163
|
+
- Don't `post.set("author", user)`. Use `setRelation("author", user)`. `set` treats the value as a column write; `setRelation` picks the right foreign key.
|
|
164
|
+
- Don't load all parents and iterate to fetch each child — that's the N+1 problem. Eager-load with `.with(...)` (or load specific children with `.load(...)` after the parent fetch).
|
|
165
|
+
- Don't define a relation on only one side. Both ends of `BelongsTo`/`HasMany` and both sides of `BelongsToMany` need the decorator for queries to work bidirectionally.
|
|
166
|
+
- Don't import the related class for `@BelongsTo` if it would create a circular dependency. Use the string form (`@BelongsTo("User")`) or `lazy(() => User)`.
|
|
167
|
+
|
|
168
|
+
## See also
|
|
169
|
+
|
|
170
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — `.whereHas` and other filters
|
|
171
|
+
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `@RegisterModel`, the registry that makes name-based lookup work
|