@warlock.js/cascade 5.24.0 → 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.
Files changed (70) hide show
  1. package/CHANGELOG.md +18 -5
  2. package/README.md +101 -101
  3. package/bin/cascade.js +0 -0
  4. package/cjs/index.cjs +201 -71
  5. package/cjs/index.cjs.map +1 -1
  6. package/esm/contracts/database-driver.contract.d.mts +22 -1
  7. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  8. package/esm/contracts/index.d.mts +1 -1
  9. package/esm/data-source/data-source-registry.d.mts +8 -6
  10. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  11. package/esm/data-source/data-source-registry.mjs +7 -1
  12. package/esm/data-source/data-source-registry.mjs.map +1 -1
  13. package/esm/drivers/mongodb/mongodb-driver.d.mts +13 -0
  14. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-driver.mjs +71 -2
  16. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-query-builder.mjs +2 -1
  19. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  20. package/esm/drivers/postgres/postgres-driver.d.mts +9 -0
  21. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  22. package/esm/drivers/postgres/postgres-driver.mjs +55 -7
  23. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  24. package/esm/drivers/postgres/postgres-query-parser.d.mts +1 -4
  25. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  26. package/esm/drivers/postgres/postgres-query-parser.mjs +4 -5
  27. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  28. package/esm/errors/unsafe-filter.error.mjs.map +1 -1
  29. package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -1
  30. package/esm/errors/unsupported-lock-for-update.error.d.mts +11 -0
  31. package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -0
  32. package/esm/errors/unsupported-lock-for-update.error.mjs +16 -0
  33. package/esm/errors/unsupported-lock-for-update.error.mjs.map +1 -0
  34. package/esm/events/model-events.mjs.map +1 -1
  35. package/esm/index.d.mts +3 -2
  36. package/esm/index.mjs +2 -1
  37. package/esm/migration/migration-runner.mjs.map +1 -1
  38. package/esm/migration/sql-grammar.mjs.map +1 -1
  39. package/esm/model/generate-model-id.mjs +23 -0
  40. package/esm/model/generate-model-id.mjs.map +1 -0
  41. package/esm/model/methods/delete-methods.mjs.map +1 -1
  42. package/esm/model/methods/meta-methods.mjs.map +1 -1
  43. package/esm/model/methods/query-methods.mjs +25 -2
  44. package/esm/model/methods/query-methods.mjs.map +1 -1
  45. package/esm/model/methods/write-methods.mjs.map +1 -1
  46. package/esm/query-builder/query-builder.mjs +1 -1
  47. package/esm/query-builder/query-builder.mjs.map +1 -1
  48. package/esm/utils/sanitize-filter.mjs.map +1 -1
  49. package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -1
  50. package/esm/writer/database-writer.d.mts +0 -33
  51. package/esm/writer/database-writer.d.mts.map +1 -1
  52. package/esm/writer/database-writer.mjs +3 -52
  53. package/esm/writer/database-writer.mjs.map +1 -1
  54. package/llms-full.txt +2901 -2864
  55. package/package.json +5 -5
  56. package/skills/aggregate-data/SKILL.md +165 -165
  57. package/skills/alter-migration/SKILL.md +531 -531
  58. package/skills/configure-delete-strategy/SKILL.md +130 -130
  59. package/skills/define-model/SKILL.md +205 -205
  60. package/skills/define-relations/SKILL.md +171 -171
  61. package/skills/manage-data-sources/SKILL.md +177 -140
  62. package/skills/manage-transactions/SKILL.md +215 -205
  63. package/skills/paginate-results/SKILL.md +122 -122
  64. package/skills/perform-atomic-ops/SKILL.md +156 -156
  65. package/skills/query-data/SKILL.md +213 -213
  66. package/skills/run-cascade-cli/SKILL.md +125 -125
  67. package/skills/search-by-vector/SKILL.md +127 -127
  68. package/skills/subscribe-to-model-events/SKILL.md +148 -148
  69. package/skills/track-changes/SKILL.md +127 -127
  70. 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