@warlock.js/cascade 5.19.0 → 5.20.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 +6 -0
- package/cjs/index.cjs +78 -7
- package/cjs/index.cjs.map +1 -1
- package/esm/context/database-transaction-context.d.mts +10 -0
- package/esm/context/database-transaction-context.d.mts.map +1 -1
- package/esm/context/database-transaction-context.mjs +17 -0
- package/esm/context/database-transaction-context.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +13 -5
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +8 -3
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/index.d.mts +2 -1
- package/esm/index.mjs +2 -1
- package/esm/migration/migration-runner.mjs +1 -1
- package/esm/transactions/after-commit.d.mts +24 -0
- package/esm/transactions/after-commit.d.mts.map +1 -0
- package/esm/transactions/after-commit.mjs +47 -0
- package/esm/transactions/after-commit.mjs.map +1 -0
- package/llms-full.txt +2783 -2766
- package/llms.txt +15 -15
- package/package.json +6 -6
- 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/index.md +23 -0
- package/skills/manage-data-sources/SKILL.md +140 -140
- package/skills/manage-transactions/SKILL.md +208 -191
- 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,213 +1,213 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: query-data
|
|
3
|
-
description:
|
|
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`
|