@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 +21 -0
- package/README.en.md +239 -0
- package/README.md +239 -0
- package/dist/index.cjs +514 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +287 -0
- package/dist/index.d.ts +287 -0
- package/dist/index.js +507 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
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
|
+
[](https://www.npmjs.com/package/@querykitjs/mongoose) [](./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
|
+
[](https://www.npmjs.com/package/@querykitjs/mongoose) [](./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
|