@warlock.js/cascade 5.19.1 → 5.21.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 (194) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/cjs/index.cjs +2356 -1039
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/context/database-transaction-context.d.mts +10 -0
  5. package/esm/context/database-transaction-context.d.mts.map +1 -1
  6. package/esm/context/database-transaction-context.mjs +17 -0
  7. package/esm/context/database-transaction-context.mjs.map +1 -1
  8. package/esm/contracts/query-builder.contract.d.mts +30 -3
  9. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  10. package/esm/data-source/data-source-registry.d.mts +33 -1
  11. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  12. package/esm/data-source/data-source-registry.mjs +75 -5
  13. package/esm/data-source/data-source-registry.mjs.map +1 -1
  14. package/esm/database-dirty-tracker.d.mts +5 -0
  15. package/esm/database-dirty-tracker.d.mts.map +1 -1
  16. package/esm/database-dirty-tracker.mjs +16 -0
  17. package/esm/database-dirty-tracker.mjs.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-driver.d.mts +12 -0
  19. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  20. package/esm/drivers/mongodb/mongodb-driver.mjs +58 -19
  21. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  22. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -1
  23. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  24. package/esm/drivers/mongodb/mongodb-migration-driver.mjs +15 -18
  25. package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
  26. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +13 -3
  27. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  28. package/esm/drivers/mongodb/mongodb-query-builder.mjs +72 -19
  29. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  30. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +6 -0
  31. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  32. package/esm/drivers/mongodb/mongodb-query-parser.mjs +128 -30
  33. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  34. package/esm/drivers/postgres/index.d.mts +2 -3
  35. package/esm/drivers/postgres/index.mjs +1 -2
  36. package/esm/drivers/postgres/postgres-blueprint.mjs +4 -4
  37. package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -1
  38. package/esm/drivers/postgres/postgres-dialect.d.mts +5 -1
  39. package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
  40. package/esm/drivers/postgres/postgres-dialect.mjs +38 -8
  41. package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
  42. package/esm/drivers/postgres/postgres-driver.d.mts +52 -29
  43. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  44. package/esm/drivers/postgres/postgres-driver.mjs +196 -58
  45. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  46. package/esm/drivers/postgres/postgres-migration-driver.d.mts +2 -1
  47. package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
  48. package/esm/drivers/postgres/postgres-migration-driver.mjs +8 -6
  49. package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
  50. package/esm/drivers/postgres/postgres-query-builder.d.mts +18 -7
  51. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  52. package/esm/drivers/postgres/postgres-query-builder.mjs +146 -31
  53. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  54. package/esm/drivers/postgres/postgres-query-parser.d.mts +23 -1
  55. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  56. package/esm/drivers/postgres/postgres-query-parser.mjs +160 -43
  57. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  58. package/esm/drivers/postgres/postgres-sql-serializer.mjs +3 -2
  59. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  60. package/esm/drivers/postgres/postgres-type-converters.mjs +57 -0
  61. package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -0
  62. package/esm/events/model-events.d.mts +1 -1
  63. package/esm/events/model-events.d.mts.map +1 -1
  64. package/esm/events/model-events.mjs.map +1 -1
  65. package/esm/index.d.mts +6 -6
  66. package/esm/index.mjs +5 -5
  67. package/esm/migration/column-builder.d.mts.map +1 -1
  68. package/esm/migration/column-builder.mjs +2 -2
  69. package/esm/migration/column-builder.mjs.map +1 -1
  70. package/esm/migration/index.d.mts +2 -2
  71. package/esm/migration/index.mjs +1 -1
  72. package/esm/migration/migration-order.mjs +5 -2
  73. package/esm/migration/migration-order.mjs.map +1 -1
  74. package/esm/migration/migration-runner.d.mts +50 -5
  75. package/esm/migration/migration-runner.d.mts.map +1 -1
  76. package/esm/migration/migration-runner.mjs +370 -147
  77. package/esm/migration/migration-runner.mjs.map +1 -1
  78. package/esm/migration/migration.d.mts +36 -3
  79. package/esm/migration/migration.d.mts.map +1 -1
  80. package/esm/migration/migration.mjs +62 -5
  81. package/esm/migration/migration.mjs.map +1 -1
  82. package/esm/model/methods/accessor-methods.mjs +1 -3
  83. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  84. package/esm/model/methods/delete-methods.mjs +6 -2
  85. package/esm/model/methods/delete-methods.mjs.map +1 -1
  86. package/esm/model/methods/hydration-methods.mjs +16 -2
  87. package/esm/model/methods/hydration-methods.mjs.map +1 -1
  88. package/esm/model/methods/instance-event-methods.mjs +2 -1
  89. package/esm/model/methods/instance-event-methods.mjs.map +1 -1
  90. package/esm/model/methods/meta-methods.mjs +53 -16
  91. package/esm/model/methods/meta-methods.mjs.map +1 -1
  92. package/esm/model/methods/query-methods.mjs +86 -15
  93. package/esm/model/methods/query-methods.mjs.map +1 -1
  94. package/esm/model/methods/static-event-methods.mjs +16 -1
  95. package/esm/model/methods/static-event-methods.mjs.map +1 -1
  96. package/esm/model/methods/write-methods.d.mts.map +1 -1
  97. package/esm/model/methods/write-methods.mjs +50 -50
  98. package/esm/model/methods/write-methods.mjs.map +1 -1
  99. package/esm/model/model.d.mts +16 -1
  100. package/esm/model/model.d.mts.map +1 -1
  101. package/esm/model/model.mjs +39 -5
  102. package/esm/model/model.mjs.map +1 -1
  103. package/esm/model/register-model.d.mts +6 -1
  104. package/esm/model/register-model.d.mts.map +1 -1
  105. package/esm/model/register-model.mjs +16 -1
  106. package/esm/model/register-model.mjs.map +1 -1
  107. package/esm/query-builder/query-builder.d.mts +21 -2
  108. package/esm/query-builder/query-builder.d.mts.map +1 -1
  109. package/esm/query-builder/query-builder.mjs +97 -20
  110. package/esm/query-builder/query-builder.mjs.map +1 -1
  111. package/esm/relations/key-conventions.mjs +13 -3
  112. package/esm/relations/key-conventions.mjs.map +1 -1
  113. package/esm/relations/pivot-operations.mjs +1 -1
  114. package/esm/relations/relation-loader.d.mts +15 -5
  115. package/esm/relations/relation-loader.d.mts.map +1 -1
  116. package/esm/relations/relation-loader.mjs +51 -16
  117. package/esm/relations/relation-loader.mjs.map +1 -1
  118. package/esm/remover/database-remover.d.mts.map +1 -1
  119. package/esm/remover/database-remover.mjs +21 -7
  120. package/esm/remover/database-remover.mjs.map +1 -1
  121. package/esm/restorer/database-restorer.d.mts +12 -2
  122. package/esm/restorer/database-restorer.d.mts.map +1 -1
  123. package/esm/restorer/database-restorer.mjs +43 -13
  124. package/esm/restorer/database-restorer.mjs.map +1 -1
  125. package/esm/sync/model-events.mjs +63 -3
  126. package/esm/sync/model-events.mjs.map +1 -1
  127. package/esm/sync/model-sync-operation.d.mts.map +1 -1
  128. package/esm/sync/model-sync-operation.mjs +3 -2
  129. package/esm/sync/model-sync-operation.mjs.map +1 -1
  130. package/esm/sync/sync-manager.d.mts +3 -42
  131. package/esm/sync/sync-manager.d.mts.map +1 -1
  132. package/esm/sync/sync-manager.mjs +37 -126
  133. package/esm/sync/sync-manager.mjs.map +1 -1
  134. package/esm/transactions/after-commit.d.mts +24 -0
  135. package/esm/transactions/after-commit.d.mts.map +1 -0
  136. package/esm/transactions/after-commit.mjs +47 -0
  137. package/esm/transactions/after-commit.mjs.map +1 -0
  138. package/esm/utils/connect-to-database.d.mts.map +1 -1
  139. package/esm/utils/connect-to-database.mjs +4 -4
  140. package/esm/utils/connect-to-database.mjs.map +1 -1
  141. package/esm/utils/database-writer.utils.d.mts +1 -1
  142. package/esm/utils/database-writer.utils.d.mts.map +1 -1
  143. package/esm/utils/database-writer.utils.mjs +1 -0
  144. package/esm/utils/database-writer.utils.mjs.map +1 -1
  145. package/esm/utils/define-model.d.mts.map +1 -1
  146. package/esm/utils/define-model.mjs +2 -10
  147. package/esm/utils/define-model.mjs.map +1 -1
  148. package/esm/utils/is-valid-date-value.mjs +3 -2
  149. package/esm/utils/is-valid-date-value.mjs.map +1 -1
  150. package/esm/utils/once-connected.d.mts.map +1 -1
  151. package/esm/utils/once-connected.mjs +12 -2
  152. package/esm/utils/once-connected.mjs.map +1 -1
  153. package/esm/validation/mutators/embed-mutator.mjs +19 -11
  154. package/esm/validation/mutators/embed-mutator.mjs.map +1 -1
  155. package/esm/validation/plugins/embed-validator-plugin.d.mts +2 -2
  156. package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
  157. package/esm/validation/plugins/embed-validator-plugin.mjs +8 -2
  158. package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -1
  159. package/esm/validation/rules/database-model-rule.mjs +6 -5
  160. package/esm/validation/rules/database-model-rule.mjs.map +1 -1
  161. package/esm/validation/rules/exists-rule.mjs +6 -4
  162. package/esm/validation/rules/exists-rule.mjs.map +1 -1
  163. package/esm/validation/rules/unique-rule.mjs +5 -2
  164. package/esm/validation/rules/unique-rule.mjs.map +1 -1
  165. package/esm/validation/validators/embed-validator.d.mts.map +1 -1
  166. package/esm/validation/validators/embed-validator.mjs +1 -1
  167. package/esm/validation/validators/embed-validator.mjs.map +1 -1
  168. package/esm/writer/database-writer.d.mts +27 -0
  169. package/esm/writer/database-writer.d.mts.map +1 -1
  170. package/esm/writer/database-writer.mjs +84 -17
  171. package/esm/writer/database-writer.mjs.map +1 -1
  172. package/llms-full.txt +2783 -2766
  173. package/llms.txt +15 -15
  174. package/package.json +6 -6
  175. package/skills/aggregate-data/SKILL.md +165 -165
  176. package/skills/alter-migration/SKILL.md +531 -531
  177. package/skills/configure-delete-strategy/SKILL.md +130 -130
  178. package/skills/define-model/SKILL.md +205 -205
  179. package/skills/define-relations/SKILL.md +171 -171
  180. package/skills/index.md +23 -0
  181. package/skills/manage-data-sources/SKILL.md +140 -140
  182. package/skills/manage-transactions/SKILL.md +208 -191
  183. package/skills/paginate-results/SKILL.md +122 -122
  184. package/skills/perform-atomic-ops/SKILL.md +156 -156
  185. package/skills/query-data/SKILL.md +213 -213
  186. package/skills/run-cascade-cli/SKILL.md +125 -125
  187. package/skills/search-by-vector/SKILL.md +127 -127
  188. package/skills/subscribe-to-model-events/SKILL.md +148 -148
  189. package/skills/track-changes/SKILL.md +127 -127
  190. package/skills/write-migration/SKILL.md +206 -206
  191. package/esm/drivers/postgres/postgres-sync-adapter.d.mts +0 -79
  192. package/esm/drivers/postgres/postgres-sync-adapter.d.mts.map +0 -1
  193. package/esm/drivers/postgres/postgres-sync-adapter.mjs +0 -162
  194. package/esm/drivers/postgres/postgres-sync-adapter.mjs.map +0 -1
@@ -1,213 +1,213 @@
1
- ---
2
- name: query-data
3
- description: "Query data in @warlock.js/cascade; use when you need to query data."
4
- ---
5
-
6
- # Query data
7
-
8
- The model is the query entry point. No `db.collection("users")`, no `prisma.user.findFirst()`, no repository to import — the class queries itself.
9
-
10
- ## Filter — `.where()`
11
-
12
- ### Equality — the shorthand
13
-
14
- ```ts
15
- const activeUsers = await User.where("status", "active").get();
16
- ```
17
-
18
- `User.where(field, value)` returns a query builder filtered to that condition. `.get()` runs the query and returns an array of `User` instances.
19
-
20
- ### Operators
21
-
22
- ```ts
23
- const adults = await User.where("age", ">", 18).get();
24
- const recent = await User.where("created_at", ">=", lastWeek).get();
25
- const nonAdmins = await User.where("role", "!=", "admin").get();
26
- ```
27
-
28
- 3-argument form. Common operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `notIn`, `like`, `between`. Same syntax across MongoDB and Postgres.
29
-
30
- ### Compound conditions
31
-
32
- ```ts
33
- const activeAdmins = await User
34
- .where("status", "active")
35
- .where("role", "admin")
36
- .get();
37
- ```
38
-
39
- Chained `.where()` calls combine with `AND`.
40
-
41
- ### Object form
42
-
43
- ```ts
44
- const targets = await User.where({ status: "active", role: "admin" }).get();
45
- ```
46
-
47
- Equivalent to chained equalities. Useful when the filter comes from a dynamic source. **Object form only supports equality** — use chained `.where()` for operators.
48
-
49
- ### Filters are safe to build from request data — but only through this API
50
-
51
- `where(field, value)`, `where(field, "=", value)` and the object form all run the value through a `$`-prefixed-key check before it reaches the driver, so `User.first({ email: req.body.email, password: req.body.password })` throws `UnsafeFilterError` instead of compiling `{ password: { $ne: null } }` into a live MongoDB operator query (the classic NoSQL auth-bypass). This applies to every filter-accepting model static too — `first`, `findFirst`, `findAll`, `count`, `paginate`, `all`, `deleteMany`/`delete`, `deleteOne`, and the atomic/find-and-modify statics in [`perform-atomic-ops`](@warlock.js/cascade/perform-atomic-ops/SKILL.md). Explicit operator APIs (`where(field, operator, value)` for operators other than `=`, `whereIn`/`whereNull`/`whereBetween`/…, object-form `whereRaw({ ... })`) are intentionally not routed through this check — they already express an operator on purpose. Dotted paths (`"profile.name"`) and plain nested-object equality values remain valid; only literal `$`-prefixed keys are rejected. `sanitizeFilter` / `sanitizeFilterValue` are exported if you need the same check on a filter you forward to a driver-level API directly.
52
-
53
- ```ts
54
- await User.first({ email, password }); // throws UnsafeFilterError if password is `{ $ne: null }`
55
- ```
56
-
57
- ## Raw expressions — `.whereRaw()` / `.orWhereRaw()`
58
-
59
- ```ts
60
- query.whereRaw({ $expr: { $gt: ["$stock", "$reserved"] } }); // object form — MongoDB
61
- query.whereRaw("age > ?", [30]); // string form — SQL, real bindings
62
- ```
63
-
64
- The **object form** is checked like any other filter value, plus it rejects the server-side-JavaScript operators `$where`, `$function` and `$accumulator` anywhere in the expression (throws `UnsafeRawExpressionError`); `$expr` and the other aggregation operators keep working. The **string form on the MongoDB driver throws `UnsafeRawExpressionError`** — a string used to compile to `{ $where: "<js>" }`, executing arbitrary JavaScript inside `mongod` per scanned document; there is no safe way to parameterize that, so it's rejected outright. Use the object form instead. SQL drivers keep string mode with real `?` bindings (unchanged).
65
-
66
- ## Get one record
67
-
68
- ### By ID
69
-
70
- ```ts
71
- const user = await User.find(id); // User | null
72
- ```
73
-
74
- ### First match
75
-
76
- ```ts
77
- const anyUser = await User.first(); // first user, any
78
- const firstAdmin = await User.first({ role: "admin" }); // first admin
79
- const filtered = await User.where("status", "active").first(); // chain into .first()
80
- ```
81
-
82
- `.first()` with no args returns the very first record (driver-dependent default order). With a filter object, the first match by equality. Chain off `.where()` when you need operators.
83
-
84
- ### Throw if missing — `.firstOrFail()`
85
-
86
- ```ts
87
- const user = await User.where("id", req.params.id).firstOrFail();
88
- ```
89
-
90
- Throws when nothing matches — useful when you KNOW it should exist and want the error to surface loudly instead of an `undefined`-derived NPE downstream.
91
-
92
- **Always handle `null`** from `.find()` and `.first()` — use `?.` or a guard. Resist `!` on query results.
93
-
94
- ## Order and paginate
95
-
96
- ```ts
97
- const newest = await User
98
- .where("status", "active")
99
- .orderBy("created_at", "desc")
100
- .get();
101
- ```
102
-
103
- `.orderBy(field, "asc" | "desc")` sorts. Default direction is `"asc"`. Chain multiple `.orderBy()` for tiebreakers.
104
-
105
- For pagination see [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md).
106
-
107
- ## Plain objects — `.lean()`
108
-
109
- ```ts
110
- const rows = await User.query().where("status", "active").lean().orderBy("id").get();
111
- rows[0].name; // typed as UserSchema, not a User instance
112
- ```
113
-
114
- `.lean()` (anywhere before the terminator) returns the rows as the driver sent them: no `User` instances, no driver casting (a date string stays a string, Mongo `_id` stays an `ObjectId`), no `onFetched` / model `fetched` event. Use it for read-only lists and exports. It still strips `static hidden` fields. `where` / `select` / `orderBy` / `limit` / `first` / `paginate` behave as usual. Adding `.with()` or `.joinWith()` throws `UnsupportedLeanOperationError`: load relations with a second lean query, or drop `.lean()`. About 1.9x faster than a hydrated read for 10k Mongo documents (one run on a busy machine).
115
-
116
- ## Count and existence
117
-
118
- ```ts
119
- const total = await User.count();
120
- const activeCount = await User.count({ status: "active" });
121
- const adminCount = await User.where("role", "admin").count();
122
-
123
- const hasAdmin = await User.where("role", "admin").exists(); // boolean, short-circuits
124
- const noneBlocked = await User.where("status", "blocked").notExists();
125
- ```
126
-
127
- **Don't reach for `.count() > 0`** when you only need a boolean — `.exists()` short-circuits on the first matching row. The difference shows up immediately on tables with more than a few thousand rows.
128
-
129
- ## Get many — `.all(filter?)`
130
-
131
- ```ts
132
- const allUsers = await User.all();
133
- const activeUsers = await User.all({ status: "active" });
134
- ```
135
-
136
- `Model.all(filter?)` is the shortcut for "fetch all records matching a simple equality filter, or every record if no filter."
137
-
138
- **Caution.** `.all()` with no filter loads the entire table. Use [pagination](@warlock.js/cascade/paginate-results/SKILL.md) for tables larger than a few hundred rows.
139
-
140
- ## The wider query vocabulary
141
-
142
- Cascade's query builder has around 60 methods. Reach for these as the need arises:
143
-
144
- | Reach for | When |
145
- | --- | --- |
146
- | `.whereIn(field, values)` / `.whereNotIn(field, values)` | Match against / exclude a list |
147
- | `.whereNull(field)` / `.whereNotNull(field)` | Nullability checks |
148
- | `.whereBetween(field, [a, b])` | Inclusive range |
149
- | `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
150
- | `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch` | Pattern matching — see below for literal-vs-regex semantics |
151
- | `.whereHas(relation, callback)` | Filter by conditions on a related model |
152
- | `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — [`use-aggregates`](@warlock.js/cascade/aggregate-data/SKILL.md) |
153
- | `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
154
- | `.chunk(size, callback)` | Stream a large table in batches |
155
- | `.cursorPaginate({ limit, cursor })` | Cursor pagination — [`paginate-results`](@warlock.js/cascade/paginate-results/SKILL.md) |
156
- | `.similarTo(column, embedding)` | Vector similarity — [`use-vector-search`](@warlock.js/cascade/search-by-vector/SKILL.md) |
157
-
158
- Each chains off `User.where(...)` or `User.query()` and ends with the appropriate terminator. (`where`, `with`, `joinWith`, `first`, `count`, `find`, `all`, `paginate` are static shortcuts on the model; the rest live on the query builder, so reach them via `User.query()` or by chaining off a static `where`.)
159
-
160
- ### `whereLike` / `whereSearch` — string arguments match literally
161
-
162
- ```ts
163
- await User.where(...).whereLike("email", "%@gmail.com").get(); // literal, % still wildcards
164
- await User.query().whereSearch("name", req.query.q).get(); // safe with raw user input
165
- ```
166
-
167
- A **string** argument to `whereLike`/`whereNotLike`/`whereStartsWith`/`whereEndsWith` (and their `Not` variants) and the `$regex` form of `whereSearch` is escaped and matched as a **literal substring** — the SQL `LIKE` wildcard `%` still expands (to `.*`, runs of `%` collapse), but every other regex metacharacter (`.`, `*`, `+`, `^`, `$`, …) in the string is neutralized. This is what makes `whereSearch("name", req.query.q)` safe to wire directly to a search box: before this, a user-controlled string compiled straight into a MongoDB `$regex`, so `^.*$` matched everything, `^a`/`^b`/… probed a value back one character at a time, and a nested-quantifier pattern like `(a+)+$` triggered catastrophic backtracking (ReDoS) against every scanned document.
168
-
169
- An explicit **`RegExp`** argument is still used as a pattern (verbatim, no escaping) — that's how you get real regex semantics — but a `RegExp` can't arrive as JSON, so it only reaches these calls when a developer constructs it in code. **Never build that `RegExp` from user input**; if you need dynamic pattern matching, escape the dynamic parts yourself with the exported `escapeRegex(value)` / `likePatternToRegexSource(pattern)` helpers.
170
-
171
- ```ts
172
- import { escapeRegex, likePatternToRegexSource } from "@warlock.js/cascade";
173
- ```
174
-
175
- The Postgres driver was already parameterized (`ILIKE $1`) and is unaffected either way.
176
-
177
- ## Scopes — reusable query fragments
178
-
179
- When you write the same `.where("status", "active")` across multiple services, define a scope on the model:
180
-
181
- ```ts
182
- @RegisterModel()
183
- export class User extends Model<UserSchema> {
184
- public static table = "users";
185
- public static schema = userSchema;
186
-
187
- static {
188
- this.addScope("active", (query) => {
189
- query.where("status", "active");
190
- });
191
- }
192
- }
193
-
194
- const activeUsers = await User.query().scope("active").get();
195
- ```
196
-
197
- **Local scopes** (`addScope`) — opt-in, only when you call `.scope("name")`.
198
- **Global scopes** (`addGlobalScope`) — run on every query for that model. Useful for multi-tenancy or default soft-delete filtering. Bypass per-query with `.withoutGlobalScope("name")` / `.withoutGlobalScopes()`.
199
-
200
- ## Things NOT to do
201
-
202
- - Don't `.count() > 0` for existence — use `.exists()`.
203
- - Don't `Model.all()` without a filter on a production table — use pagination or chunking.
204
- - Don't `!` away the null from `.find()` / `.first()` — handle the missing case explicitly or use `.firstOrFail()` when absence is a real error.
205
- - Don't write the same filter chain across multiple services — promote it to a scope.
206
- - Don't build a `RegExp` for `whereLike`/`whereSearch` out of user input — a string argument is already safely escaped; a raw `RegExp` bypasses that and reintroduces the injection/ReDoS risk it closes.
207
- - Don't reach for `whereRaw()` string mode on MongoDB expecting it to still work — it now throws `UnsafeRawExpressionError`; use the object form (`whereRaw({ $expr: … })`).
208
-
209
- ## See also
210
-
211
- - [`@warlock.js/cascade/define-relations/SKILL.md`](@warlock.js/cascade/define-relations/SKILL.md) — `.with(...)`, `.whereHas(...)`, eager loading
212
- - [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md) — pagination + cursor + chunk
213
- - [`@warlock.js/cascade/aggregate-data/SKILL.md`](@warlock.js/cascade/aggregate-data/SKILL.md) — `.sum`, `.avg`, `.groupBy`, `.having`
1
+ ---
2
+ name: query-data
3
+ description: 'Query records via the model — `.where(field, value)` / `.where(field, op, value)`, `.find(id)` / `.first` / `.all`, `.orderBy`, `.count` / `.exists`, `.lean()` plain-object reads, plus `.whereIn` / `.whereBetween` / `.whereLike` / `.pluck` / `.firstOrFail` / scopes via `addScope`. Covers filter safety: `where()` rejects `$`-prefixed keys (`UnsafeFilterError`), `whereRaw()` string form rejects `$where`/`$function`/`$accumulator` (`UnsafeRawExpressionError`), and `whereLike`/`whereStartsWith`/`whereEndsWith`/`whereSearch` match string arguments literally (pass a `RegExp` for pattern semantics). Triggers: `.where`, `.find`, `.first`, `.firstOrFail`, `.all`, `.get`, `.orderBy`, `.exists`, `.whereIn`, `.whereBetween`, `.whereLike`, `.whereRaw`, `.lean`, `UnsupportedLeanOperationError`, `addScope`, `escapeRegex`, `likePatternToRegexSource`, `UnsafeFilterError`, `UnsafeRawExpressionError`; "filter by status", "find by id", "fetch active users", "check existence", "search box query", "is where() safe from injection"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: pagination — `@warlock.js/cascade/paginate-results/SKILL.md`; aggregates — `@warlock.js/cascade/aggregate-data/SKILL.md`.'
4
+ ---
5
+
6
+ # Query data
7
+
8
+ The model is the query entry point. No `db.collection("users")`, no `prisma.user.findFirst()`, no repository to import — the class queries itself.
9
+
10
+ ## Filter — `.where()`
11
+
12
+ ### Equality — the shorthand
13
+
14
+ ```ts
15
+ const activeUsers = await User.where("status", "active").get();
16
+ ```
17
+
18
+ `User.where(field, value)` returns a query builder filtered to that condition. `.get()` runs the query and returns an array of `User` instances.
19
+
20
+ ### Operators
21
+
22
+ ```ts
23
+ const adults = await User.where("age", ">", 18).get();
24
+ const recent = await User.where("created_at", ">=", lastWeek).get();
25
+ const nonAdmins = await User.where("role", "!=", "admin").get();
26
+ ```
27
+
28
+ 3-argument form. Common operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `notIn`, `like`, `between`. Same syntax across MongoDB and Postgres.
29
+
30
+ ### Compound conditions
31
+
32
+ ```ts
33
+ const activeAdmins = await User
34
+ .where("status", "active")
35
+ .where("role", "admin")
36
+ .get();
37
+ ```
38
+
39
+ Chained `.where()` calls combine with `AND`.
40
+
41
+ ### Object form
42
+
43
+ ```ts
44
+ const targets = await User.where({ status: "active", role: "admin" }).get();
45
+ ```
46
+
47
+ Equivalent to chained equalities. Useful when the filter comes from a dynamic source. **Object form only supports equality** — use chained `.where()` for operators.
48
+
49
+ ### Filters are safe to build from request data — but only through this API
50
+
51
+ `where(field, value)`, `where(field, "=", value)` and the object form all run the value through a `$`-prefixed-key check before it reaches the driver, so `User.first({ email: req.body.email, password: req.body.password })` throws `UnsafeFilterError` instead of compiling `{ password: { $ne: null } }` into a live MongoDB operator query (the classic NoSQL auth-bypass). This applies to every filter-accepting model static too — `first`, `findFirst`, `findAll`, `count`, `paginate`, `all`, `deleteMany`/`delete`, `deleteOne`, and the atomic/find-and-modify statics in [`perform-atomic-ops`](@warlock.js/cascade/perform-atomic-ops/SKILL.md). Explicit operator APIs (`where(field, operator, value)` for operators other than `=`, `whereIn`/`whereNull`/`whereBetween`/…, object-form `whereRaw({ ... })`) are intentionally not routed through this check — they already express an operator on purpose. Dotted paths (`"profile.name"`) and plain nested-object equality values remain valid; only literal `$`-prefixed keys are rejected. `sanitizeFilter` / `sanitizeFilterValue` are exported if you need the same check on a filter you forward to a driver-level API directly.
52
+
53
+ ```ts
54
+ await User.first({ email, password }); // throws UnsafeFilterError if password is `{ $ne: null }`
55
+ ```
56
+
57
+ ## Raw expressions — `.whereRaw()` / `.orWhereRaw()`
58
+
59
+ ```ts
60
+ query.whereRaw({ $expr: { $gt: ["$stock", "$reserved"] } }); // object form — MongoDB
61
+ query.whereRaw("age > ?", [30]); // string form — SQL, real bindings
62
+ ```
63
+
64
+ The **object form** is checked like any other filter value, plus it rejects the server-side-JavaScript operators `$where`, `$function` and `$accumulator` anywhere in the expression (throws `UnsafeRawExpressionError`); `$expr` and the other aggregation operators keep working. The **string form on the MongoDB driver throws `UnsafeRawExpressionError`** — a string used to compile to `{ $where: "<js>" }`, executing arbitrary JavaScript inside `mongod` per scanned document; there is no safe way to parameterize that, so it's rejected outright. Use the object form instead. SQL drivers keep string mode with real `?` bindings (unchanged).
65
+
66
+ ## Get one record
67
+
68
+ ### By ID
69
+
70
+ ```ts
71
+ const user = await User.find(id); // User | null
72
+ ```
73
+
74
+ ### First match
75
+
76
+ ```ts
77
+ const anyUser = await User.first(); // first user, any
78
+ const firstAdmin = await User.first({ role: "admin" }); // first admin
79
+ const filtered = await User.where("status", "active").first(); // chain into .first()
80
+ ```
81
+
82
+ `.first()` with no args returns the very first record (driver-dependent default order). With a filter object, the first match by equality. Chain off `.where()` when you need operators.
83
+
84
+ ### Throw if missing — `.firstOrFail()`
85
+
86
+ ```ts
87
+ const user = await User.where("id", req.params.id).firstOrFail();
88
+ ```
89
+
90
+ Throws when nothing matches — useful when you KNOW it should exist and want the error to surface loudly instead of an `undefined`-derived NPE downstream.
91
+
92
+ **Always handle `null`** from `.find()` and `.first()` — use `?.` or a guard. Resist `!` on query results.
93
+
94
+ ## Order and paginate
95
+
96
+ ```ts
97
+ const newest = await User
98
+ .where("status", "active")
99
+ .orderBy("created_at", "desc")
100
+ .get();
101
+ ```
102
+
103
+ `.orderBy(field, "asc" | "desc")` sorts. Default direction is `"asc"`. Chain multiple `.orderBy()` for tiebreakers.
104
+
105
+ For pagination see [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md).
106
+
107
+ ## Plain objects — `.lean()`
108
+
109
+ ```ts
110
+ const rows = await User.query().where("status", "active").lean().orderBy("id").get();
111
+ rows[0].name; // typed as UserSchema, not a User instance
112
+ ```
113
+
114
+ `.lean()` (anywhere before the terminator) returns the rows as the driver sent them: no `User` instances, no driver casting (a date string stays a string, Mongo `_id` stays an `ObjectId`), no `onFetched` / model `fetched` event. Use it for read-only lists and exports. It still strips `static hidden` fields. `where` / `select` / `orderBy` / `limit` / `first` / `paginate` behave as usual. Adding `.with()` or `.joinWith()` throws `UnsupportedLeanOperationError`: load relations with a second lean query, or drop `.lean()`. About 1.9x faster than a hydrated read for 10k Mongo documents (one run on a busy machine).
115
+
116
+ ## Count and existence
117
+
118
+ ```ts
119
+ const total = await User.count();
120
+ const activeCount = await User.count({ status: "active" });
121
+ const adminCount = await User.where("role", "admin").count();
122
+
123
+ const hasAdmin = await User.where("role", "admin").exists(); // boolean, short-circuits
124
+ const noneBlocked = await User.where("status", "blocked").notExists();
125
+ ```
126
+
127
+ **Don't reach for `.count() > 0`** when you only need a boolean — `.exists()` short-circuits on the first matching row. The difference shows up immediately on tables with more than a few thousand rows.
128
+
129
+ ## Get many — `.all(filter?)`
130
+
131
+ ```ts
132
+ const allUsers = await User.all();
133
+ const activeUsers = await User.all({ status: "active" });
134
+ ```
135
+
136
+ `Model.all(filter?)` is the shortcut for "fetch all records matching a simple equality filter, or every record if no filter."
137
+
138
+ **Caution.** `.all()` with no filter loads the entire table. Use [pagination](@warlock.js/cascade/paginate-results/SKILL.md) for tables larger than a few hundred rows.
139
+
140
+ ## The wider query vocabulary
141
+
142
+ Cascade's query builder has around 60 methods. Reach for these as the need arises:
143
+
144
+ | Reach for | When |
145
+ | --- | --- |
146
+ | `.whereIn(field, values)` / `.whereNotIn(field, values)` | Match against / exclude a list |
147
+ | `.whereNull(field)` / `.whereNotNull(field)` | Nullability checks |
148
+ | `.whereBetween(field, [a, b])` | Inclusive range |
149
+ | `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
150
+ | `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` / `.whereSearch` | Pattern matching — see below for literal-vs-regex semantics |
151
+ | `.whereHas(relation, callback)` | Filter by conditions on a related model |
152
+ | `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — [`use-aggregates`](@warlock.js/cascade/aggregate-data/SKILL.md) |
153
+ | `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
154
+ | `.chunk(size, callback)` | Stream a large table in batches |
155
+ | `.cursorPaginate({ limit, cursor })` | Cursor pagination — [`paginate-results`](@warlock.js/cascade/paginate-results/SKILL.md) |
156
+ | `.similarTo(column, embedding)` | Vector similarity — [`use-vector-search`](@warlock.js/cascade/search-by-vector/SKILL.md) |
157
+
158
+ Each chains off `User.where(...)` or `User.query()` and ends with the appropriate terminator. (`where`, `with`, `joinWith`, `first`, `count`, `find`, `all`, `paginate` are static shortcuts on the model; the rest live on the query builder, so reach them via `User.query()` or by chaining off a static `where`.)
159
+
160
+ ### `whereLike` / `whereSearch` — string arguments match literally
161
+
162
+ ```ts
163
+ await User.where(...).whereLike("email", "%@gmail.com").get(); // literal, % still wildcards
164
+ await User.query().whereSearch("name", req.query.q).get(); // safe with raw user input
165
+ ```
166
+
167
+ A **string** argument to `whereLike`/`whereNotLike`/`whereStartsWith`/`whereEndsWith` (and their `Not` variants) and the `$regex` form of `whereSearch` is escaped and matched as a **literal substring** — the SQL `LIKE` wildcard `%` still expands (to `.*`, runs of `%` collapse), but every other regex metacharacter (`.`, `*`, `+`, `^`, `$`, …) in the string is neutralized. This is what makes `whereSearch("name", req.query.q)` safe to wire directly to a search box: before this, a user-controlled string compiled straight into a MongoDB `$regex`, so `^.*$` matched everything, `^a`/`^b`/… probed a value back one character at a time, and a nested-quantifier pattern like `(a+)+$` triggered catastrophic backtracking (ReDoS) against every scanned document.
168
+
169
+ An explicit **`RegExp`** argument is still used as a pattern (verbatim, no escaping) — that's how you get real regex semantics — but a `RegExp` can't arrive as JSON, so it only reaches these calls when a developer constructs it in code. **Never build that `RegExp` from user input**; if you need dynamic pattern matching, escape the dynamic parts yourself with the exported `escapeRegex(value)` / `likePatternToRegexSource(pattern)` helpers.
170
+
171
+ ```ts
172
+ import { escapeRegex, likePatternToRegexSource } from "@warlock.js/cascade";
173
+ ```
174
+
175
+ The Postgres driver was already parameterized (`ILIKE $1`) and is unaffected either way.
176
+
177
+ ## Scopes — reusable query fragments
178
+
179
+ When you write the same `.where("status", "active")` across multiple services, define a scope on the model:
180
+
181
+ ```ts
182
+ @RegisterModel()
183
+ export class User extends Model<UserSchema> {
184
+ public static table = "users";
185
+ public static schema = userSchema;
186
+
187
+ static {
188
+ this.addScope("active", (query) => {
189
+ query.where("status", "active");
190
+ });
191
+ }
192
+ }
193
+
194
+ const activeUsers = await User.query().scope("active").get();
195
+ ```
196
+
197
+ **Local scopes** (`addScope`) — opt-in, only when you call `.scope("name")`.
198
+ **Global scopes** (`addGlobalScope`) — run on every query for that model. Useful for multi-tenancy or default soft-delete filtering. Bypass per-query with `.withoutGlobalScope("name")` / `.withoutGlobalScopes()`.
199
+
200
+ ## Things NOT to do
201
+
202
+ - Don't `.count() > 0` for existence — use `.exists()`.
203
+ - Don't `Model.all()` without a filter on a production table — use pagination or chunking.
204
+ - Don't `!` away the null from `.find()` / `.first()` — handle the missing case explicitly or use `.firstOrFail()` when absence is a real error.
205
+ - Don't write the same filter chain across multiple services — promote it to a scope.
206
+ - Don't build a `RegExp` for `whereLike`/`whereSearch` out of user input — a string argument is already safely escaped; a raw `RegExp` bypasses that and reintroduces the injection/ReDoS risk it closes.
207
+ - Don't reach for `whereRaw()` string mode on MongoDB expecting it to still work — it now throws `UnsafeRawExpressionError`; use the object form (`whereRaw({ $expr: … })`).
208
+
209
+ ## See also
210
+
211
+ - [`@warlock.js/cascade/define-relations/SKILL.md`](@warlock.js/cascade/define-relations/SKILL.md) — `.with(...)`, `.whereHas(...)`, eager loading
212
+ - [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md) — pagination + cursor + chunk
213
+ - [`@warlock.js/cascade/aggregate-data/SKILL.md`](@warlock.js/cascade/aggregate-data/SKILL.md) — `.sum`, `.avg`, `.groupBy`, `.having`