@querykitjs/mongoose 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Suhrobbek Soatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,239 @@
1
+ <div align="right">
2
+
3
+ **English** · [O'zbekcha](./README.md)
4
+
5
+ </div>
6
+
7
+ # @querykitjs/mongoose
8
+
9
+ [![npm](https://img.shields.io/npm/v/@querykitjs/mongoose.svg)](https://www.npmjs.com/package/@querykitjs/mongoose) [![license](https://img.shields.io/npm/l/@querykitjs/mongoose.svg)](./LICENSE)
10
+
11
+ > Advanced filtering + flexible pagination repository layer for Mongoose (MongoDB).
12
+
13
+ Collapses the code you'd otherwise hand-write on top of Mongoose — complex filters, three pagination styles, transactions, RBAC scoping, soft-delete, bulk/aggregation — into one typed repository. Result types are inferred automatically from `with`/`columns`. The public surface mirrors [`@querykitjs/drizzle-pg`](https://www.npmjs.com/package/@querykitjs/drizzle-pg), so the **same frontend contract** ([`@querykitjs/web`](https://www.npmjs.com/package/@querykitjs/web)) drives both a Postgres and a MongoDB backend.
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import mongoose, { Schema } from "mongoose";
19
+ import { createRegistry, createFilters } from "@querykitjs/mongoose";
20
+
21
+ // 1. a normal Mongoose model
22
+ interface IUser {
23
+ _id: mongoose.Types.ObjectId;
24
+ name: string;
25
+ email: string;
26
+ age: number | null;
27
+ status: string;
28
+ createdAt: Date;
29
+ updatedAt: Date;
30
+ }
31
+ const User = mongoose.model<IUser>("User", new Schema<IUser>({ name: String, email: String, age: Number, status: String }, { timestamps: true }));
32
+
33
+ // 2. connect, then build the registry from the connection
34
+ await mongoose.connect(process.env.MONGO_URL!);
35
+ export const registry = createRegistry(mongoose.connection);
36
+
37
+ // 3. one typed repository per model
38
+ export const usersRepository = registry.repository(User);
39
+
40
+ // 4. query — filters, sort, pagination, all typed
41
+ const f = createFilters<IUser>();
42
+ const { data, meta } = await usersRepository.findList({
43
+ page: 2,
44
+ perPage: 20,
45
+ filter: f.and(f.eq("status", "active"), f.gte("age", 18)),
46
+ sort: [{ key: "createdAt", direction: "desc" }],
47
+ });
48
+ // ^? data: IUser[] meta: { total_items, total_pages, has_next, ... }
49
+ ```
50
+
51
+ ## Features
52
+
53
+ - **Advanced filters** — nested `and`/`or`/`not`, 25+ operators, raw Mongo query escape hatch, `f` helpers. SQL three-valued-logic parity (negation excludes null/missing).
54
+ - **3 pagination styles** — offset (`findList`), infinite scroll (`findInfinite`), cursor/keyset (`findCursor`, default key `_id`).
55
+ - **`with` + `columns` inference** — populated relations & selected fields typed with no manual generics.
56
+ - **Multi-field sort**, `count`, `exists`, `aggregate` (count/sum/avg/min/max + groupBy).
57
+ - **Transactions** — `registry.transaction()`; repositories inside auto-join it (requires a replica set).
58
+ - **Scoped repository** — permanent base filter + create defaults for RBAC / multi-tenancy.
59
+ - **Soft-delete** — automatic when a `deletedAt` path exists; `softDelete`/`restore`/`withDeleted`.
60
+ - **Upsert / bulk** — `upsert`, `upsertMany` (auto-chunked, input-order results).
61
+ - `id` ↔ `_id` aliasing so the wire contract stays identical across adapters; reads return plain POJOs (`.lean()`).
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ bun add @querykitjs/mongoose mongoose
67
+ # or: npm i @querykitjs/mongoose mongoose
68
+ ```
69
+
70
+ `mongoose` (>= 8) is a peer dependency.
71
+
72
+ ## Setup
73
+
74
+ ### 1. Registry (once)
75
+
76
+ ```ts
77
+ // db/registry.ts
78
+ import mongoose from "mongoose";
79
+ import { createRegistry } from "@querykitjs/mongoose";
80
+
81
+ export const registry = createRegistry(mongoose.connection);
82
+ // or configure pagination defaults:
83
+ // export const registry = createRegistry(mongoose.connection, { defaultPerPage: 20, defaultLimit: 20 });
84
+ ```
85
+
86
+ `defaultPerPage` (findList) / `defaultLimit` (infinite/cursor) fall back to `@querykitjs/core`'s **20** when omitted. The `connection` is passed in (so transactions share the right session).
87
+
88
+ ### 2. Per-model repositories
89
+
90
+ ```ts
91
+ // db/repositories/users.repository.ts
92
+ import { registry } from "../registry";
93
+ import { User } from "../models/user.model";
94
+
95
+ export const usersRepository = registry.repository(User, base => ({
96
+ findByEmail: (email: string) => base.findOne({ filter: [{ key: "email", operation: "=", value: email }] }),
97
+ }));
98
+ ```
99
+
100
+ ## MongoDB specifics
101
+
102
+ - **`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:
103
+ ```ts
104
+ await usersRepository.findById("665f0e...b3a"); // string → ObjectId
105
+ await usersRepository.findAll({ filter: [{ key: "id", operation: "in", value: [id1, id2] }] });
106
+ ```
107
+ - **Value casting.** Wire strings are cast by the schema — an ISO date string → `Date`, a hex string → `ObjectId` — consistently in `find`, `aggregate` `$match`, and cursor tokens.
108
+ - **Reads are `.lean()`** — plain POJOs (not hydrated Mongoose documents), matching the SQL adapter's rows.
109
+ - **Cursor** keys on `_id` by default (override with `cursorKey`) — stable under inserts.
110
+ - **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
+
112
+ ## Advanced filters
113
+
114
+ 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`.
115
+
116
+ ```ts
117
+ import { createFilters } from "@querykitjs/mongoose";
118
+ const f = createFilters<IUser>(); // field-name autocomplete
119
+
120
+ await usersRepository.findAll({
121
+ filter: f.and(f.eq("status", "active"), f.or(f.gte("age", 18), f.in("role", ["admin", "owner"])), f.not(f.isNull("deletedAt"))),
122
+ });
123
+ ```
124
+
125
+ **Operators:** `= != > >= < <=` (and `eq ne gt gte lt lte`), `like ilike notLike`, `contains startsWith endsWith` (case-insensitive), `in notIn`, `between notBetween` (`value: [min, max]`), `isNull isNotNull`. A raw Mongo query object can be dropped in anywhere. Values pass through as-is; wire strings (dates / ObjectIds) are cast by the schema. Negation operators (`ne`/`notIn`/`notLike`/`notBetween`, and single-field `not`) exclude null/missing to match SQL three-valued logic.
126
+
127
+ ## Multi-field sort
128
+
129
+ ```ts
130
+ sort: [
131
+ { key: "name", direction: "asc" },
132
+ { key: "createdAt", direction: "desc" },
133
+ ]; // { name: 1, createdAt: -1 }
134
+ ```
135
+
136
+ Sort is always a `{ key, direction }[]` array — the same shape across `@querykitjs/web` and every adapter.
137
+
138
+ Falls back to `createdAt DESC`, then `_id DESC`, when no sort is given — a deterministic order so pagination stays stable.
139
+
140
+ ## Pagination — three strategies
141
+
142
+ ```ts
143
+ // 1. Offset — total_items / total_pages
144
+ const page = await usersRepository.findList({ page: 2, perPage: 20, filter, sort });
145
+
146
+ // 2. Infinite scroll (limit + offset) — has_more / next_offset
147
+ const feed = await usersRepository.findInfinite({ limit: 20, offset: 40 });
148
+
149
+ // 3. Cursor / keyset (stable under inserts, keyed on `_id` by default) — next_cursor / prev_cursor
150
+ const p1 = await usersRepository.findCursor({ limit: 20, order: "asc" });
151
+ const p2 = await usersRepository.findCursor({ limit: 20, cursor: p1.meta.next_cursor });
152
+ const back = await usersRepository.findCursor({ cursor: p2.meta.prev_cursor, direction: "backward" });
153
+ ```
154
+
155
+ ## Typed relations & field selection
156
+
157
+ Read methods infer their return type from `with` (populate) and `columns` — no manual generics:
158
+
159
+ ```ts
160
+ const post = await postsRepository.findById(id, { with: { author: true } });
161
+ post?.author.name; // populated
162
+
163
+ const rows = await usersRepository.findAll({ columns: { id: true, name: true } });
164
+ rows[0].id; // ObjectId
165
+ rows[0].email; // ❌ type error — not selected
166
+ ```
167
+
168
+ To type populated relations at runtime, pass the referenced models: `registry.repository(Post, { relations: { author: User } })`.
169
+
170
+ ## Transactions
171
+
172
+ ```ts
173
+ await registry.transaction(async () => {
174
+ await dispatchesRepository.create({ ... });
175
+ await stockRepository.updateById(stockId, { qty: next });
176
+ }); // throws -> full rollback
177
+ ```
178
+
179
+ Repositories used inside automatically use the transaction session (ambient context). **MongoDB transactions require a replica set** (a single-node replica set is enough for local dev). Nesting reuses the outer session.
180
+
181
+ ## Scoped repository (RBAC / multi-tenancy)
182
+
183
+ ```ts
184
+ const mine = roadmapsRepository.scoped({ supervisorId: user.id });
185
+ await mine.findList({ page: 1 }); // { supervisorId: user.id, ... }
186
+ await mine.create({ ... }); // supervisorId forced to user.id
187
+ ```
188
+
189
+ ## Soft-delete
190
+
191
+ 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.
192
+
193
+ ```ts
194
+ await postsRepository.softDelete(id);
195
+ await postsRepository.restore(id);
196
+ await postsRepository.findAll({ withDeleted: true });
197
+ ```
198
+
199
+ ## Upsert, bulk & aggregation
200
+
201
+ ```ts
202
+ await usersRepository.upsert({ email: "a@b.com", name: "Ali" }, { target: "email" });
203
+ await productsRepository.upsertMany(rows, { target: "externalId" }); // chunked, returns in input order
204
+ await visitsRepository.aggregate({ count: true, groupBy: "supervisorId" });
205
+ await ordersRepository.aggregate({ sum: "amount", groupBy: "region" });
206
+ ```
207
+
208
+ ## API reference
209
+
210
+ | Method | Returns |
211
+ | -------------------------------------------------------------- | ---------------------------- |
212
+ | `findAll(params?)` | `Row[]` |
213
+ | `findOne(params?)` / `findById(id, params?)` | `Row \| undefined` |
214
+ | `findList(params?)` | `{ data, meta }` offset |
215
+ | `findInfinite(params?)` | `{ data, meta }` infinite |
216
+ | `findCursor(params?)` | `{ data, meta }` cursor |
217
+ | `count(filter?)` / `exists(filter?)` | `number` / `boolean` |
218
+ | `aggregate(spec)` | `AggregateRow[]` |
219
+ | `create(values)` / `createMany(values)` | `Row` / `Row[]` |
220
+ | `upsert(values, opts)` / `upsertMany(values, opts)` | `Row` / `Row[]` |
221
+ | `updateById(id, patch, idKey?)` / `updateWhere(filter, patch)` | `Row \| undefined` / `Row[]` |
222
+ | `deleteById(id, idKey?)` / `deleteWhere(filter)` | `Row \| undefined` / `Row[]` |
223
+ | `softDelete(id)` / `restore(id)` | `Row \| undefined` |
224
+ | `scoped(scope)` | scoped `Repository` |
225
+ | `registry.transaction(fn)` | result of `fn` |
226
+
227
+ ## Development
228
+
229
+ ```bash
230
+ bun install
231
+ bun run typecheck
232
+ bun run lint
233
+ bun run build # tsup -> dist (ESM + CJS + .d.ts)
234
+ bun run --filter @querykitjs/mongoose test:smoke # in-memory replica set (mongodb-memory-server)
235
+ ```
236
+
237
+ ## License
238
+
239
+ MIT © Suhrobbek Soatov
package/README.md ADDED
@@ -0,0 +1,239 @@
1
+ <div align="right">
2
+
3
+ [English](./README.en.md) · **O'zbekcha**
4
+
5
+ </div>
6
+
7
+ # @querykitjs/mongoose
8
+
9
+ [![npm](https://img.shields.io/npm/v/@querykitjs/mongoose.svg)](https://www.npmjs.com/package/@querykitjs/mongoose) [![license](https://img.shields.io/npm/l/@querykitjs/mongoose.svg)](./LICENSE)
10
+
11
+ > Mongoose (MongoDB) uchun ilg'or filtrlash + moslashuvchan paginatsiya repository qatlami.
12
+
13
+ Mongoose ustiga qo'lda yozadigan kodni — murakkab filterlar, 3 xil paginatsiya, tranzaksiya, RBAC scope, soft-delete, bulk/aggregatsiya — bitta tipli repository'ga jamlaydi. Natija tiplari `with`/`columns`dan avtomatik chiqariladi. Ommaviy yuza [`@querykitjs/drizzle-pg`](https://www.npmjs.com/package/@querykitjs/drizzle-pg) bilan bir xil — shuning uchun **bitta frontend kontrakti** ([`@querykitjs/web`](https://www.npmjs.com/package/@querykitjs/web)) ham Postgres, ham MongoDB backend'ini boshqaradi.
14
+
15
+ ## Tez boshlash
16
+
17
+ ```ts
18
+ import mongoose, { Schema } from "mongoose";
19
+ import { createRegistry, createFilters } from "@querykitjs/mongoose";
20
+
21
+ // 1. oddiy Mongoose modeli
22
+ interface IUser {
23
+ _id: mongoose.Types.ObjectId;
24
+ name: string;
25
+ email: string;
26
+ age: number | null;
27
+ status: string;
28
+ createdAt: Date;
29
+ updatedAt: Date;
30
+ }
31
+ const User = mongoose.model<IUser>("User", new Schema<IUser>({ name: String, email: String, age: Number, status: String }, { timestamps: true }));
32
+
33
+ // 2. ulaning, so'ng connection'dan registry quring
34
+ await mongoose.connect(process.env.MONGO_URL!);
35
+ export const registry = createRegistry(mongoose.connection);
36
+
37
+ // 3. har model uchun bitta tipli repository
38
+ export const usersRepository = registry.repository(User);
39
+
40
+ // 4. so'rov — filter, sort, pagination, hammasi tipli
41
+ const f = createFilters<IUser>();
42
+ const { data, meta } = await usersRepository.findList({
43
+ page: 2,
44
+ perPage: 20,
45
+ filter: f.and(f.eq("status", "active"), f.gte("age", 18)),
46
+ sort: [{ key: "createdAt", direction: "desc" }],
47
+ });
48
+ // ^? data: IUser[] meta: { total_items, total_pages, has_next, ... }
49
+ ```
50
+
51
+ ## Imkoniyatlar
52
+
53
+ - **Ilg'or filterlar** — ichma-ich `and`/`or`/`not`, 25+ operator, raw Mongo query escape-hatch, `f` helperlar. SQL uch-qiymatli mantiq pariteti (negatsiya null/yo'qni chetlaydi).
54
+ - **3 xil paginatsiya** — offset (`findList`), infinite scroll (`findInfinite`), cursor/keyset (`findCursor`, default kalit `_id`).
55
+ - **`with` + `columns` inference** — populate qilingan relation'lar va tanlangan maydonlar qo'lda generic'siz tiplanadi.
56
+ - **Ko'p-maydonli sort**, `count`, `exists`, `aggregate` (count/sum/avg/min/max + groupBy).
57
+ - **Tranzaksiya** — `registry.transaction()`; ichidagi repository'lar avtomatik qo'shiladi (replica set talab qiladi).
58
+ - **Scoped repository** — RBAC / multi-tenancy uchun doimiy asosiy filter + create default'lari.
59
+ - **Soft-delete** — `deletedAt` path bo'lsa avtomatik; `softDelete`/`restore`/`withDeleted`.
60
+ - **Upsert / bulk** — `upsert`, `upsertMany` (avto-chunk, input-tartibda natija).
61
+ - `id` ↔ `_id` alias — wire kontrakti adapterlar bo'ylab bir xil; o'qishlar oddiy POJO qaytaradi (`.lean()`).
62
+
63
+ ## O'rnatish
64
+
65
+ ```bash
66
+ bun add @querykitjs/mongoose mongoose
67
+ # yoki: npm i @querykitjs/mongoose mongoose
68
+ ```
69
+
70
+ `mongoose` (>= 8) — peer dependency.
71
+
72
+ ## Sozlash
73
+
74
+ ### 1. Registry (bir marta)
75
+
76
+ ```ts
77
+ // db/registry.ts
78
+ import mongoose from "mongoose";
79
+ import { createRegistry } from "@querykitjs/mongoose";
80
+
81
+ export const registry = createRegistry(mongoose.connection);
82
+ // yoki paginatsiya default'larини sozlash:
83
+ // export const registry = createRegistry(mongoose.connection, { defaultPerPage: 20, defaultLimit: 20 });
84
+ ```
85
+
86
+ `defaultPerPage` (findList) / `defaultLimit` (infinite/cursor) berilmasa `@querykitjs/core`ning **20**siga tushadi. `connection` tashqaridan beriladi (tranzaksiya to'g'ri session'ni ulashsin uchun).
87
+
88
+ ### 2. Har model uchun repository
89
+
90
+ ```ts
91
+ // db/repositories/users.repository.ts
92
+ import { registry } from "../registry";
93
+ import { User } from "../models/user.model";
94
+
95
+ export const usersRepository = registry.repository(User, base => ({
96
+ findByEmail: (email: string) => base.findOne({ filter: [{ key: "email", operation: "=", value: email }] }),
97
+ }));
98
+ ```
99
+
100
+ ## MongoDB-ga xos jihatlar
101
+
102
+ - **`id` ↔ `_id`.** Istalgan joyda `"id"` ishlating (filter, `columns`, `idKey`, `cursorKey`) — u Mongo'ning `_id`siga o'giriladi, wire kontrakti SQL adapter bilan bir xil. Frontend'дан kelган string id'lar avtomatik `ObjectId`ga cast bo'ladi:
103
+ ```ts
104
+ await usersRepository.findById("665f0e...b3a"); // string → ObjectId
105
+ await usersRepository.findAll({ filter: [{ key: "id", operation: "in", value: [id1, id2] }] });
106
+ ```
107
+ - **Qiymat cast.** Wire string'lar schema tomonidan cast qilinadi — ISO sana → `Date`, hex string → `ObjectId` — `find`, `aggregate` `$match` va cursor tokenlarда birxil.
108
+ - **O'qishlar `.lean()`** — oddiy POJO (hydrate qilinган Mongoose hujjati emas), SQL adapter qatorlari bilan mos.
109
+ - **Cursor** default `_id` bo'yicha (`cursorKey` bilan o'zgartiring) — insert'larда barqaror.
110
+ - **Tranzaksiya replica set talab qiladi** — lokal dev uchun bir-nodали yetarli (`mongod --replSet rs0` so'ng `rs.initiate()`, yoki testда [`mongodb-memory-server`](https://github.com/typegoose/mongodb-memory-server)).
111
+
112
+ ## Ilg'or filterlar
113
+
114
+ Filter — maydon shartlari va mantiqiy guruhlar (`and`/`or`/`not`) daraxti. Yassi massiv = implicit `AND`. `id` — `_id`ga o'giriladi.
115
+
116
+ ```ts
117
+ import { createFilters } from "@querykitjs/mongoose";
118
+ const f = createFilters<IUser>(); // maydon nomi autocomplete
119
+
120
+ await usersRepository.findAll({
121
+ filter: f.and(f.eq("status", "active"), f.or(f.gte("age", 18), f.in("role", ["admin", "owner"])), f.not(f.isNull("deletedAt"))),
122
+ });
123
+ ```
124
+
125
+ **Operatorlar:** `= != > >= < <=` (va `eq ne gt gte lt lte`), `like ilike notLike`, `contains startsWith endsWith` (case-insensitive), `in notIn`, `between notBetween` (`value: [min, max]`), `isNull isNotNull`. Istalgan joyga raw Mongo query obyekti tashlanadi. Qiymatlar o'zgartirilmasdan o'tadi; wire string'lar (sana / ObjectId) schema tomonidan cast qilinadi. Negatsiya operatorlari (`ne`/`notIn`/`notLike`/`notBetween` va bir-maydonли `not`) SQL uch-qiymatli mantiqiga mos ravishda null/yo'qni chetlaydi.
126
+
127
+ ## Ko'p-maydonli sort
128
+
129
+ ```ts
130
+ sort: [
131
+ { key: "name", direction: "asc" },
132
+ { key: "createdAt", direction: "desc" },
133
+ ]; // { name: 1, createdAt: -1 }
134
+ ```
135
+
136
+ Sort — `{ key, direction }[]` massivi, `@querykitjs/web` va barcha adapterlar bilan bir xil shakl.
137
+
138
+ Sort berilmasa `createdAt DESC` ga, u bo'lmasa `_id DESC` ga tushadi — pagination barqaror bo'lishi uchun deterministik tartib.
139
+
140
+ ## Paginatsiya — 3 strategiya
141
+
142
+ ```ts
143
+ // 1. Offset — total_items / total_pages
144
+ const page = await usersRepository.findList({ page: 2, perPage: 20, filter, sort });
145
+
146
+ // 2. Infinite scroll (limit + offset) — has_more / next_offset
147
+ const feed = await usersRepository.findInfinite({ limit: 20, offset: 40 });
148
+
149
+ // 3. Cursor / keyset (insert'larда barqaror, default `_id` bo'yicha) — next_cursor / prev_cursor
150
+ const p1 = await usersRepository.findCursor({ limit: 20, order: "asc" });
151
+ const p2 = await usersRepository.findCursor({ limit: 20, cursor: p1.meta.next_cursor });
152
+ const back = await usersRepository.findCursor({ cursor: p2.meta.prev_cursor, direction: "backward" });
153
+ ```
154
+
155
+ ## Tipli relation'lar va maydon tanlash
156
+
157
+ O'qish metodlari natija tipini `with` (populate) va `columns`dan chiqaradi — qo'lda generic yo'q:
158
+
159
+ ```ts
160
+ const post = await postsRepository.findById(id, { with: { author: true } });
161
+ post?.author.name; // populate qilingan
162
+
163
+ const rows = await usersRepository.findAll({ columns: { id: true, name: true } });
164
+ rows[0].id; // ObjectId
165
+ rows[0].email; // ❌ tip xatosi — tanlanmagan
166
+ ```
167
+
168
+ Populate qilingan relation'larni runtime'да tiplash uchun ref model'larni bering: `registry.repository(Post, { relations: { author: User } })`.
169
+
170
+ ## Tranzaksiya
171
+
172
+ ```ts
173
+ await registry.transaction(async () => {
174
+ await dispatchesRepository.create({ ... });
175
+ await stockRepository.updateById(stockId, { qty: next });
176
+ }); // throw -> to'liq rollback
177
+ ```
178
+
179
+ Ichida ishlatilgan repository'lar avtomatik tranzaksiya session'ini oladi (ambient context). **MongoDB tranzaksiyasi replica set talab qiladi** (lokal dev uchun bir-nodали replica set kifoya). Ichma-ich chaqiruv tashqi session'ni qayta ishlatadi.
180
+
181
+ ## Scoped repository (RBAC / multi-tenancy)
182
+
183
+ ```ts
184
+ const mine = roadmapsRepository.scoped({ supervisorId: user.id });
185
+ await mine.findList({ page: 1 }); // { supervisorId: user.id, ... }
186
+ await mine.create({ ... }); // supervisorId majburan user.id
187
+ ```
188
+
189
+ ## Soft-delete
190
+
191
+ Schema'да `deletedAt` path bo'lsa avtomatik yoqiladi. O'qishlar default'да soft-delete qilingan hujjatlarni chiqarib tashlaydi; `withDeleted: true` ularni ham qo'shadi. `{ timestamps: true }` bo'lmasa lekin qo'lда `updatedAt` path bo'lsa, har update'да bumps qilinadi.
192
+
193
+ ```ts
194
+ await postsRepository.softDelete(id);
195
+ await postsRepository.restore(id);
196
+ await postsRepository.findAll({ withDeleted: true });
197
+ ```
198
+
199
+ ## Upsert, bulk & aggregatsiya
200
+
201
+ ```ts
202
+ await usersRepository.upsert({ email: "a@b.com", name: "Ali" }, { target: "email" });
203
+ await productsRepository.upsertMany(rows, { target: "externalId" }); // chunk, input tartibda qaytadi
204
+ await visitsRepository.aggregate({ count: true, groupBy: "supervisorId" });
205
+ await ordersRepository.aggregate({ sum: "amount", groupBy: "region" });
206
+ ```
207
+
208
+ ## API ma'lumotnoma
209
+
210
+ | Metod | Qaytaradi |
211
+ | -------------------------------------------------------------- | ---------------------------- |
212
+ | `findAll(params?)` | `Row[]` |
213
+ | `findOne(params?)` / `findById(id, params?)` | `Row \| undefined` |
214
+ | `findList(params?)` | `{ data, meta }` offset |
215
+ | `findInfinite(params?)` | `{ data, meta }` infinite |
216
+ | `findCursor(params?)` | `{ data, meta }` cursor |
217
+ | `count(filter?)` / `exists(filter?)` | `number` / `boolean` |
218
+ | `aggregate(spec)` | `AggregateRow[]` |
219
+ | `create(values)` / `createMany(values)` | `Row` / `Row[]` |
220
+ | `upsert(values, opts)` / `upsertMany(values, opts)` | `Row` / `Row[]` |
221
+ | `updateById(id, patch, idKey?)` / `updateWhere(filter, patch)` | `Row \| undefined` / `Row[]` |
222
+ | `deleteById(id, idKey?)` / `deleteWhere(filter)` | `Row \| undefined` / `Row[]` |
223
+ | `softDelete(id)` / `restore(id)` | `Row \| undefined` |
224
+ | `scoped(scope)` | scoped `Repository` |
225
+ | `registry.transaction(fn)` | `fn` natijasi |
226
+
227
+ ## Ishlab chiqish
228
+
229
+ ```bash
230
+ bun install
231
+ bun run typecheck
232
+ bun run lint
233
+ bun run build # tsup -> dist (ESM + CJS + .d.ts)
234
+ bun run --filter @querykitjs/mongoose test:smoke # in-memory replica set (mongodb-memory-server)
235
+ ```
236
+
237
+ ## Litsenziya
238
+
239
+ MIT © Suhrobbek Soatov