@querykitjs/mongoose 1.0.0 → 3.0.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/README.en.md +284 -18
- package/README.md +285 -18
- package/dist/index.cjs +1 -513
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -90
- package/dist/index.d.ts +195 -90
- package/dist/index.js +1 -506
- package/dist/index.js.map +1 -1
- package/package.json +18 -9
- package/skills/querykit-mongoose/SKILL.md +189 -0
package/README.en.md
CHANGED
|
@@ -69,6 +69,15 @@ bun add @querykitjs/mongoose mongoose
|
|
|
69
69
|
|
|
70
70
|
`mongoose` (>= 8) is a peer dependency.
|
|
71
71
|
|
|
72
|
+
### Requirements
|
|
73
|
+
|
|
74
|
+
| | Version | Tested (`bun run test:matrix`) |
|
|
75
|
+
| ----------------- | ------------------ | ------------------------------ |
|
|
76
|
+
| Node.js | `>=22.12` | 24 |
|
|
77
|
+
| `mongoose` (peer) | `^8.24 \|\| ^9.11` | 8.24.7, 9.11.4 |
|
|
78
|
+
|
|
79
|
+
Node 20 is no longer supported (`engines`: `>=22.12`). Peer ranges cover the last two majors; both ends are tested.
|
|
80
|
+
|
|
72
81
|
## Setup
|
|
73
82
|
|
|
74
83
|
### 1. Registry (once)
|
|
@@ -83,7 +92,27 @@ export const registry = createRegistry(mongoose.connection);
|
|
|
83
92
|
// export const registry = createRegistry(mongoose.connection, { defaultPerPage: 20, defaultLimit: 20 });
|
|
84
93
|
```
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
The `connection` is passed in (so transactions share the right session).
|
|
96
|
+
|
|
97
|
+
**`createRegistry` options** — identical to the drizzle-pg adapter:
|
|
98
|
+
|
|
99
|
+
| Option | Default | Meaning |
|
|
100
|
+
| -------------------- | --------------------------------- | ------------------------------------------------------------ |
|
|
101
|
+
| `defaultPerPage` | core `DEFAULT_PER_PAGE` (20) | `findList` page size |
|
|
102
|
+
| `defaultLimit` | core `DEFAULT_LIMIT` (20) | `findInfinite`/`findCursor` limit |
|
|
103
|
+
| `maxPerPage` | core `DEFAULT_MAX_PER_PAGE` (200) | upper bound for `perPage` (`Infinity` disables it) |
|
|
104
|
+
| `maxLimit` | core `DEFAULT_MAX_LIMIT` (200) | upper bound for `limit` |
|
|
105
|
+
| `strict` | `false` | unknown key → `QueryKitError` (400) instead of a silent drop |
|
|
106
|
+
| `onSkippedCondition` | — | callback for every dropped condition |
|
|
107
|
+
|
|
108
|
+
`maxPerPage`/`maxLimit` are a **second layer**: even if validation
|
|
109
|
+
(`@querykitjs/zod` factories) is bypassed, the repository clamps.
|
|
110
|
+
|
|
111
|
+
`timestamps` — the createdAt / deletedAt field names (for example in a snake_case project): the default sort and the default `sortable` use that `createdAt`, soft delete (`softDelete`, `restore`, `withDeleted`) uses that `deletedAt`.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export const registry = createRegistry(mongoose.connection, { timestamps: { createdAt: "created_at", deletedAt: "deleted_at" } });
|
|
115
|
+
```
|
|
87
116
|
|
|
88
117
|
### 2. Per-model repositories
|
|
89
118
|
|
|
@@ -97,6 +126,82 @@ export const usersRepository = registry.repository(User, base => ({
|
|
|
97
126
|
}));
|
|
98
127
|
```
|
|
99
128
|
|
|
129
|
+
`repository()` takes four shapes (the same as the drizzle-pg adapter):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
registry.repository(User); // plain
|
|
133
|
+
registry.repository(User, base => ({ … })); // + custom methods
|
|
134
|
+
registry.repository(User, options); // + per-repo options
|
|
135
|
+
registry.repository(User, options, base => ({ … })); // both
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Projection guards (`RepositoryOptions`)
|
|
139
|
+
|
|
140
|
+
`columns` can arrive straight from the client, so the safe selection is declared
|
|
141
|
+
on the **repository** — where `scope` (RBAC) already lives:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
export const usersRepository = registry.repository(User, {
|
|
145
|
+
forcedColumns: { _id: true, fullName: true, email: true }, // password can never come out
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// or softer: the client picks, but only from this list
|
|
149
|
+
export const postsRepository = registry.repository(Post, {
|
|
150
|
+
allowedColumns: ["_id", "title", "createdAt"],
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
| Option | Behaviour |
|
|
155
|
+
| ---------------- | ----------------------------------------------------------------------------- |
|
|
156
|
+
| `forcedColumns` | the client's `columns` is **ignored entirely** |
|
|
157
|
+
| `allowedColumns` | the client's selection is **intersected**; empty intersection → the allowlist |
|
|
158
|
+
| `scope` | constant equality filter on every read/write (also available via `scoped()`) |
|
|
159
|
+
| `relations` | populatable relations (type-only — drives `with` inference) |
|
|
160
|
+
|
|
161
|
+
Details worth knowing:
|
|
162
|
+
|
|
163
|
+
- An empty intersection yields the allowlist, **not the full document**.
|
|
164
|
+
- `aggregate` is guarded too: `min("password")` or `groupBy: "password"` leaks as
|
|
165
|
+
much as a projection.
|
|
166
|
+
- `cursorKey` comes from the client and the cursor field must be selected for
|
|
167
|
+
pagination — under a guard it stays in the query but is **stripped** from the
|
|
168
|
+
documents you get back.
|
|
169
|
+
- `forcedColumns: {}` or `allowedColumns: []` **throws** when the repository is built.
|
|
170
|
+
- ⚠️ The guard affects **read** methods only (`findAll`/`findOne`/`findById`/
|
|
171
|
+
`findList`/`findInfinite`/`findCursor` + `aggregate`). Write methods (`create`,
|
|
172
|
+
`upsert`, `updateById`, `softDelete`, …) return the **full document** by type
|
|
173
|
+
contract. Re-read with `findById`, or map it yourself, before handing a write
|
|
174
|
+
result to a client.
|
|
175
|
+
|
|
176
|
+
### 4. Bad conditions: watch them, or refuse them
|
|
177
|
+
|
|
178
|
+
An unknown filter/sort key is **dropped silently** by default. The catch: a filter
|
|
179
|
+
is meant to _narrow_ a result set, so a mistyped key that vanishes makes the
|
|
180
|
+
endpoint return **more** data than intended.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// step 1: watch (behaviour unchanged)
|
|
184
|
+
createRegistry(mongoose.connection, {
|
|
185
|
+
onSkippedCondition: info => logger.warn({ querykit: info }, "condition dropped"),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// step 2: once the log is clean — strict mode
|
|
189
|
+
createRegistry(mongoose.connection, { strict: true });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { QueryKitError } from "@querykitjs/core";
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
return await usersRepository.findList(params);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (err instanceof QueryKitError) return c.json({ error: err.message, info: err.info }, 400);
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A `scope` or `cursorKey` key is **always** fatal, regardless of `strict`.
|
|
204
|
+
|
|
100
205
|
## MongoDB specifics
|
|
101
206
|
|
|
102
207
|
- **`id` ↔ `_id`.** Use `"id"` anywhere (filters, `columns`, `idKey`, `cursorKey`) — it resolves to Mongo's `_id`, so the wire contract is identical to the SQL adapter. String ids from the frontend cast to `ObjectId` automatically:
|
|
@@ -109,6 +214,41 @@ export const usersRepository = registry.repository(User, base => ({
|
|
|
109
214
|
- **Cursor** keys on `_id` by default (override with `cursorKey`) — stable under inserts.
|
|
110
215
|
- **Transactions need a replica set** — a single-node one is enough for local dev (`mongod --replSet rs0` then `rs.initiate()`, or [`mongodb-memory-server`](https://github.com/typegoose/mongodb-memory-server) in tests).
|
|
111
216
|
|
|
217
|
+
- **Indexes and operators.** `startsWith` is an anchored (`^`) but case-insensitive (`i`) regex: MongoDB uses an index poorly for it. On a large collection keep a lowercase shadow field (`nameLower`) and give that `=` / `startsWith`. `contains`, `endsWith`, `!=` and `notIn` cannot use an index (COLLSCAN) — closed in the default `filterable`, open them only for small collections. Use `in` for enum fields.
|
|
218
|
+
|
|
219
|
+
## Safe by default — `filterable`, `sortable`, `limits`
|
|
220
|
+
|
|
221
|
+
A client filters and sorts only by the paths the server allows, and only with the operators it allows. A refused request stops with a `QueryKitError` before it reaches MongoDB — whatever `strict` and `onSkippedCondition` say. Option names, defaults and `reason`/`site` are the same as drizzle-pg's and prisma-pg's (`id` ≡ `_id`).
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const usersRepository = registry.repository(User, {
|
|
225
|
+
forcedColumns: { _id: true, name: true, status: true, createdAt: true },
|
|
226
|
+
filterable: {
|
|
227
|
+
status: ["=", "in"],
|
|
228
|
+
createdAt: ["between", ">=", "<="],
|
|
229
|
+
name: ["startsWith"], // open only where listed
|
|
230
|
+
},
|
|
231
|
+
sortable: ["_id", "createdAt", "name"],
|
|
232
|
+
limits: { maxInItems: 500 },
|
|
233
|
+
maxTimeMS: 1000,
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Option | When not given (default) | When refused |
|
|
238
|
+
| ------------ | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
239
|
+
| `filterable` | `allowedColumns` (else the `forcedColumns` keys, else only the primary key), with `"="` and `"in"` only | `reason: "forbidden-key"` / `"forbidden-operator"`, `site: "filter"` |
|
|
240
|
+
| `sortable` | the primary key and `createdAt` (when the table has it); a client-sent `cursorKey` too | `reason: "forbidden-key"`, `site: "sort"` / `"cursorKey"` |
|
|
241
|
+
| `limits` | core `DEFAULT_QUERY_LIMITS` (depth 5, conditions 30, array 200, string 256, offset 10 000, sort 3) | `reason: "limit-exceeded"`, `site: "filter"` / `"sort"` / `"page"` |
|
|
242
|
+
|
|
243
|
+
- `aggregate`: the `filter` and every `groupBy`/`sum`/`avg`/`min`/`max` column must be in `filterable` (`site: "aggregate"`).
|
|
244
|
+
- `findById`: only the client `filter` is checked; `idKey` is the server's choice.
|
|
245
|
+
- A key the table does **not** have (`nope`) stays `unknown-key` — skipped, or an error under `strict`.
|
|
246
|
+
- Mutation filters (`updateMany`, `deleteMany`, …) count as server code and are not checked.
|
|
247
|
+
- Operator aliases are one: `["="]` also admits `eq`, `["contains"]` also `%_%`. `"*"` admits every operator.
|
|
248
|
+
- An unbranded raw Mongo filter (`{ $where: … }`, `{ secret: { $regex: … } }`, `{ $expr: … }`) can come from a JSON body — `forbidden-key`, nothing is sent (R19). Server code brands it with `raw({...})`; ⚠️ `raw()` is not checked by `filterable`.
|
|
249
|
+
- LIKE patterns: more than `maxLikeWildcards` (2) `%`/`_` → `limit-exceeded`; a run of `%%` counts once.
|
|
250
|
+
- Every read (`find`, `findOne`, `countDocuments`, `aggregate`) carries `maxTimeMS` (default 2 000); past it ⇒ `QueryKitTimeoutError` (`code: "QUERYKIT_TIMEOUT"`) — map it to 503/504, not 400.
|
|
251
|
+
|
|
112
252
|
## Advanced filters
|
|
113
253
|
|
|
114
254
|
A filter is a tree of field conditions and logical groups (`and`/`or`/`not`). A flat array is treated as implicit `AND`. `id` resolves to `_id`.
|
|
@@ -142,6 +282,8 @@ Falls back to `createdAt DESC`, then `_id DESC`, when no sort is given — a det
|
|
|
142
282
|
```ts
|
|
143
283
|
// 1. Offset — total_items / total_pages
|
|
144
284
|
const page = await usersRepository.findList({ page: 2, perPage: 20, filter, sort });
|
|
285
|
+
// withTotal: false — no count query (large tables): perPage + 1 rows, total_items / total_pages = null
|
|
286
|
+
const fast = await usersRepository.findList({ page: 2, perPage: 20, withTotal: false });
|
|
145
287
|
|
|
146
288
|
// 2. Infinite scroll (limit + offset) — has_more / next_offset
|
|
147
289
|
const feed = await usersRepository.findInfinite({ limit: 20, offset: 40 });
|
|
@@ -152,8 +294,32 @@ const p2 = await usersRepository.findCursor({ limit: 20, cursor: p1.meta.next_cu
|
|
|
152
294
|
const back = await usersRepository.findCursor({ cursor: p2.meta.prev_cursor, direction: "backward" });
|
|
153
295
|
```
|
|
154
296
|
|
|
297
|
+
### Cursor: ties and signing
|
|
298
|
+
|
|
299
|
+
- The seek key is `(cursorKey, _id)`: with a non-unique `cursorKey` (for example a shared `createdAt`) no row is skipped or repeated. When `cursorKey` is the primary key itself the seek uses that one field.
|
|
300
|
+
- A token (v2) holds only scalar values (string, number, boolean, ISO date) and they are cast to the field type; an object, an array or a value that does not fit → `QueryKitError` `reason: "invalid-value"`, `site: "cursorKey"` (the driver is not called).
|
|
301
|
+
- The registry option `cursorSecret` signs tokens with HMAC-SHA256; an unsigned or altered token → `invalid-value`. Without it tokens are unsigned — a client can build one itself (still only within the allowed `sortable` keys and the filter).
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
export const registry = createRegistry(mongoose.connection, { cursorSecret: process.env.CURSOR_SECRET });
|
|
305
|
+
```
|
|
306
|
+
|
|
155
307
|
## Typed relations & field selection
|
|
156
308
|
|
|
309
|
+
A client `with` opens only the relations listed in `relations` (task 014, R21):
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const postsRepository = registry.repository(Post, {
|
|
313
|
+
relations: { author: { model: User, columns: ["name"] } }, // model — for typing only
|
|
314
|
+
});
|
|
315
|
+
await postsRepository.findAll({ with: { author: true } }); // author with name only
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- `columns` is required — a list or an explicit `"*"`. `with: { rel: true }` also selects only those columns.
|
|
319
|
+
- An unlisted relation, column, filter field or nested relation → `QueryKitError` `reason: "forbidden-key"`, `site: "with"`, `key` `relation` or `relation.field`; the driver is not called.
|
|
320
|
+
- Allowed options: `select`, a nested `populate` (with `nested`), `match` (its fields in `filterable`; no `$where` / `$expr`), `options.limit` / `options.skip`.
|
|
321
|
+
- The shorthand `{ author: User }` is for typing only: `with: { author: … }` → `forbidden-key`.
|
|
322
|
+
|
|
157
323
|
Read methods infer their return type from `with` (populate) and `columns` — no manual generics:
|
|
158
324
|
|
|
159
325
|
```ts
|
|
@@ -165,10 +331,31 @@ rows[0].id; // ObjectId
|
|
|
165
331
|
rows[0].email; // ❌ type error — not selected
|
|
166
332
|
```
|
|
167
333
|
|
|
168
|
-
To type populated relations
|
|
334
|
+
To type and open populated relations, give the referenced model inside the rule: `registry.repository(Post, { relations: { author: { model: User, columns: ["name"] } } })` (the shorthand `{ author: User }` only types — it does not allow `with`).
|
|
335
|
+
|
|
336
|
+
## Bulk update and delete
|
|
337
|
+
|
|
338
|
+
`updateWhere` / `deleteWhere` are one write (no read before it) and return `{ count }`:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
const { count } = await usersRepository.updateWhere([{ key: "status", value: "trial" }], { status: "expired" });
|
|
342
|
+
await usersRepository.deleteWhere([{ key: "status", value: "expired" }]); // { count }
|
|
343
|
+
|
|
344
|
+
// returning: { limit } — { count, rows }, at most limit rows (maxLimit, default 200)
|
|
345
|
+
const { rows } = await usersRepository.updateWhere(filter, patch, { returning: { limit: 50 } });
|
|
346
|
+
|
|
347
|
+
// no filter — only when you say so (the scope still applies)
|
|
348
|
+
await usersRepository.deleteWhere({ all: true });
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- An empty filter (`undefined`, `{}`, `[]`) or one whose every condition was skipped (for example only an unknown key) → `QueryKitError` `reason: "empty-filter"`, `site: "mutation"` — the driver is not called, the table is never wiped silently.
|
|
352
|
+
- A mutation filter passes `filterable` and `limits` like a read's (`forbidden-key`, `forbidden-operator`, `limit-exceeded`) — the server's own filters too.
|
|
353
|
+
- `rows` are **not** atomic: `updateWhere` reads them right **after** the update with the same filter (if the patch changes a filtered field, other documents may show up); `deleteWhere` reads them just **before** the delete. `count` is `matchedCount` / `deletedCount`.
|
|
169
354
|
|
|
170
355
|
## Transactions
|
|
171
356
|
|
|
357
|
+
> ⚠️ Only **database work** inside the callback. External effects (HTTP calls, messages, email) go through an outbox: the driver may retry the transaction, and an external effect would run again and cannot be rolled back.
|
|
358
|
+
|
|
172
359
|
```ts
|
|
173
360
|
await registry.transaction(async () => {
|
|
174
361
|
await dispatchesRepository.create({ ... });
|
|
@@ -186,6 +373,85 @@ await mine.findList({ page: 1 }); // { supervisorId: user.id, ... }
|
|
|
186
373
|
await mine.create({ ... }); // supervisorId forced to user.id
|
|
187
374
|
```
|
|
188
375
|
|
|
376
|
+
A scope can also be given as `registry.repository(Model, { scope })`. An unknown
|
|
377
|
+
scope key **throws** when the repository is built — dropping it silently would
|
|
378
|
+
remove the RBAC filter.
|
|
379
|
+
|
|
380
|
+
A scope value may also be a list (task 014): `{ shopId: { in: [1, 2] } }` limits every read and write to those values; an empty list (`{ in: [] }`) matches no row (not an error). `create` fills in the equality scopes only.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
const mine = repository.scoped({ shopId: { in: user.shopIds } });
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Values, NULL order and parity
|
|
387
|
+
|
|
388
|
+
All three adapters (drizzle-pg, prisma-pg, mongoose) return the same result (task 021):
|
|
389
|
+
|
|
390
|
+
- **Literal value operators.** `contains`, `startsWith` and `endsWith` match their value literally: `startsWith "%"` finds only rows starting with `%`, `contains "_"` a real `_`. Wildcards belong to `like` / `ilike` / `notLike` only.
|
|
391
|
+
- **Type casting.** Every value is cast to its field type (number, boolean, date, ObjectId, enum). A mismatch (`age = "abc"`) is `invalid-value`: the condition is dropped and `onSkippedCondition` is called, a `QueryKitError` under `strict`; the driver is not called.
|
|
392
|
+
- **`not`** follows SQL three-valued logic: `not(and(…))` / `not(or(…))` over a NULL field agree on every adapter.
|
|
393
|
+
- **NULL order.** NULL first on `asc`, last on `desc` (MongoDB's order).
|
|
394
|
+
- **A malformed sort item** (`null`, `{}`) is dropped and reported as `invalid-value` (`site: "sort"`), an error under `strict`.
|
|
395
|
+
|
|
396
|
+
## Migrating to 3.0
|
|
397
|
+
|
|
398
|
+
In 3.0 a repository without a policy is **narrow**: `=` and `in` on `allowedColumns` / `forcedColumns` / `_id` only, sorting by `_id` and `createdAt`. To bring back the 2.x behaviour for one repository on purpose:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
const open = registry.repository(User, {
|
|
402
|
+
filterable: { _id: "*", name: "*", email: "*", status: "*", createdAt: "*" },
|
|
403
|
+
sortable: ["_id", "name", "email", "status", "createdAt"],
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
A raw Mongo filter now needs `raw()` (R19):
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { raw } from "@querykitjs/core";
|
|
411
|
+
|
|
412
|
+
// 2.x — forbidden-key in 3.0:
|
|
413
|
+
// await usersRepository.findAll({ filter: { age: { $gt: 18 } } });
|
|
414
|
+
|
|
415
|
+
// 3.0:
|
|
416
|
+
await usersRepository.findAll({ filter: raw({ age: { $gt: 18 } }) });
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- New reasons: `forbidden-key`, `forbidden-operator`, `limit-exceeded`; new site: `page` — return them as 400.
|
|
420
|
+
- Pages deeper than offset 10 000 are refused — use `findCursor`, or raise `limits: { maxOffset }`.
|
|
421
|
+
- Every read runs with `maxTimeMS: 2000`; raise `maxTimeMS` on the repository for long report queries. A timeout throws `QueryKitTimeoutError`.
|
|
422
|
+
- A LIKE pattern with more than 2 wildcards is refused (`maxLikeWildcards`).
|
|
423
|
+
- Cursor tokens are v2 now (task 012): an old (v1) `next_cursor` / `prev_cursor` is refused with `invalid-value` — after the upgrade start paging from the first page again.
|
|
424
|
+
- `updateWhere` / `deleteWhere` return `{ count }` instead of `Row[]` (task 013); for rows pass `{ returning: { limit } }` → `{ count, rows }`.
|
|
425
|
+
- A filterless `updateWhere` / `deleteWhere` is `empty-filter`; write `{ all: true }` for every row. A mutation filter now passes the `filterable` policy.
|
|
426
|
+
- `OffsetMeta.total_items` / `total_pages` are typed `number | null` (`null` with `withTotal: false`).
|
|
427
|
+
- `with` now needs the `relations` allow-list (task 014): on a repository without it `with` is `forbidden-key`, `site: "with"`.
|
|
428
|
+
- `relations: { author: User }` → `relations: { author: { model: User, columns: ["name"] } }` (the shorthand no longer allows `with`).
|
|
429
|
+
- A scope value may be `{ in: [...] }`; the registry option `timestamps` names the createdAt / deletedAt fields.
|
|
430
|
+
|
|
431
|
+
## Wire (JSON) values and migrating from DbService
|
|
432
|
+
|
|
433
|
+
A request body is JSON, so a date always arrives as a **string**. The adapter
|
|
434
|
+
casts it according to the schema path:
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{ "key": "createdAt", "operation": "<=", "value": "2026-07-28T12:00:00.000Z" }
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- `Date` paths get the ISO string cast to a `Date`. The scope is **deliberately**
|
|
441
|
+
the same as drizzle-pg's: each adapter only judges the types it maps through JS
|
|
442
|
+
and leaves the rest to the database.
|
|
443
|
+
- `in`/`notIn` arrays, `between`/`notBetween` tuples and cursor tokens are covered
|
|
444
|
+
too (`cursorKey: "createdAt"` paginates correctly).
|
|
445
|
+
- Mongo cannot `$regex` a `Date` path (`Can't use $options with Date`), so a
|
|
446
|
+
text-pattern operator (`contains`/`ilike`/…) on a date field is **skipped** as
|
|
447
|
+
`invalid-value` and reported in `onSkippedCondition` — the SQL adapters do the same.
|
|
448
|
+
- An unparseable value (`"not-a-date"`) drops the condition (or 400s under
|
|
449
|
+
`strict`). **One** bad element in an `in` list drops the whole condition.
|
|
450
|
+
|
|
451
|
+
**Migrating from DbService:** the old wire's `type: "date"` field is no longer
|
|
452
|
+
needed — `@querykitjs/zod` strips it (without erroring) and coercion happens
|
|
453
|
+
server-side from the path type.
|
|
454
|
+
|
|
189
455
|
## Soft-delete
|
|
190
456
|
|
|
191
457
|
Auto-enabled when the schema has a `deletedAt` path. Reads exclude soft-deleted docs by default; `withDeleted: true` includes them. When the schema has no `{ timestamps: true }` but a manual `updatedAt` path, it is bumped on every update.
|
|
@@ -207,22 +473,22 @@ await ordersRepository.aggregate({ sum: "amount", groupBy: "region" });
|
|
|
207
473
|
|
|
208
474
|
## API reference
|
|
209
475
|
|
|
210
|
-
| Method
|
|
211
|
-
|
|
|
212
|
-
| `findAll(params?)`
|
|
213
|
-
| `findOne(params?)` / `findById(id, params?)`
|
|
214
|
-
| `findList(params?)`
|
|
215
|
-
| `findInfinite(params?)`
|
|
216
|
-
| `findCursor(params?)`
|
|
217
|
-
| `count(filter?)` / `exists(filter?)`
|
|
218
|
-
| `aggregate(spec)`
|
|
219
|
-
| `create(values)` / `createMany(values)`
|
|
220
|
-
| `upsert(values, opts)` / `upsertMany(values, opts)`
|
|
221
|
-
| `updateById(id, patch, idKey?)` / `updateWhere(filter, patch)` | `Row \| undefined` / `
|
|
222
|
-
| `deleteById(id, idKey?)` / `deleteWhere(filter)` | `Row \| undefined` / `
|
|
223
|
-
| `softDelete(id)` / `restore(id)`
|
|
224
|
-
| `scoped(scope)`
|
|
225
|
-
| `registry.transaction(fn)`
|
|
476
|
+
| Method | Returns |
|
|
477
|
+
| ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
|
|
478
|
+
| `findAll(params?)` | `Row[]` |
|
|
479
|
+
| `findOne(params?)` / `findById(id, params?)` | `Row \| undefined` |
|
|
480
|
+
| `findList(params?)` | `{ data, meta }` offset |
|
|
481
|
+
| `findInfinite(params?)` | `{ data, meta }` infinite |
|
|
482
|
+
| `findCursor(params?)` | `{ data, meta }` cursor |
|
|
483
|
+
| `count(filter?)` / `exists(filter?)` | `number` / `boolean` |
|
|
484
|
+
| `aggregate(spec)` | `AggregateRow[]` |
|
|
485
|
+
| `create(values)` / `createMany(values)` | `Row` / `Row[]` |
|
|
486
|
+
| `upsert(values, opts)` / `upsertMany(values, opts)` | `Row` / `Row[]` |
|
|
487
|
+
| `updateById(id, patch, idKey?)` / `updateWhere(filter, patch, options?)` | `Row \| undefined` / `{ count }` (`{ count, rows }` with `returning`) |
|
|
488
|
+
| `deleteById(id, idKey?)` / `deleteWhere(filter, options?)` | `Row \| undefined` / `{ count }` (`{ count, rows }` with `returning`) |
|
|
489
|
+
| `softDelete(id)` / `restore(id)` | `Row \| undefined` |
|
|
490
|
+
| `scoped(scope)` | scoped `Repository` |
|
|
491
|
+
| `registry.transaction(fn)` | result of `fn` |
|
|
226
492
|
|
|
227
493
|
## Development
|
|
228
494
|
|