@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.md
CHANGED
|
@@ -69,6 +69,15 @@ bun add @querykitjs/mongoose mongoose
|
|
|
69
69
|
|
|
70
70
|
`mongoose` (>= 8) — peer dependency.
|
|
71
71
|
|
|
72
|
+
### Talablar
|
|
73
|
+
|
|
74
|
+
| | Versiya | Tekshiriladi (`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 endi qo'llab-quvvatlanmaydi (`engines`: `>=22.12`). Peer diapazoni — oxirgi ikki major; ikkala cheti testda.
|
|
80
|
+
|
|
72
81
|
## Sozlash
|
|
73
82
|
|
|
74
83
|
### 1. Registry (bir marta)
|
|
@@ -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
|
+
`connection` tashqaridan beriladi (tranzaksiya to'g'ri session'ni ulashsin uchun).
|
|
96
|
+
|
|
97
|
+
**`createRegistry` opsiyalari** — drizzle-pg adapteri bilan **aynan bir xil**:
|
|
98
|
+
|
|
99
|
+
| Opsiya | Default | Ma'nosi |
|
|
100
|
+
| -------------------- | --------------------------------- | --------------------------------------------------------- |
|
|
101
|
+
| `defaultPerPage` | core `DEFAULT_PER_PAGE` (20) | `findList` sahifa o'lchami |
|
|
102
|
+
| `defaultLimit` | core `DEFAULT_LIMIT` (20) | `findInfinite`/`findCursor` `limit`i |
|
|
103
|
+
| `maxPerPage` | core `DEFAULT_MAX_PER_PAGE` (200) | `perPage` yuqori chegarasi (`Infinity` — o'chirish) |
|
|
104
|
+
| `maxLimit` | core `DEFAULT_MAX_LIMIT` (200) | `limit` yuqori chegarasi |
|
|
105
|
+
| `strict` | `false` | noma'lum kalit → `QueryKitError` (400), jimgina skip emas |
|
|
106
|
+
| `onSkippedCondition` | — | tashlab yuborilgan har bir shart uchun callback |
|
|
107
|
+
|
|
108
|
+
`maxPerPage`/`maxLimit` — **ikkinchi himoya qatlami**: validatsiya
|
|
109
|
+
(`@querykitjs/zod` factory'lari) chetlab o'tilsa ham repository o'zi clamp qiladi.
|
|
110
|
+
|
|
111
|
+
`timestamps` — createdAt / deletedAt maydon nomlari (masalan snake_case loyihada): default sort va default `sortable` shu `createdAt`ni, soft-delete (`softDelete`, `restore`, `withDeleted`) shu `deletedAt`ni ishlatadi.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export const registry = createRegistry(mongoose.connection, { timestamps: { createdAt: "created_at", deletedAt: "deleted_at" } });
|
|
115
|
+
```
|
|
87
116
|
|
|
88
117
|
### 2. Har model uchun repository
|
|
89
118
|
|
|
@@ -97,6 +126,82 @@ export const usersRepository = registry.repository(User, base => ({
|
|
|
97
126
|
}));
|
|
98
127
|
```
|
|
99
128
|
|
|
129
|
+
`repository()` to'rt shaklda chaqiriladi (drizzle-pg adapteri bilan bir xil):
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
registry.repository(User); // sof
|
|
133
|
+
registry.repository(User, base => ({ … })); // + custom metodlar
|
|
134
|
+
registry.repository(User, options); // + per-repo opsiyalar
|
|
135
|
+
registry.repository(User, options, base => ({ … })); // ikkisi ham
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Projection himoyasi (`RepositoryOptions`)
|
|
139
|
+
|
|
140
|
+
`columns` clientdan kelishi mumkin, shuning uchun xavfsiz tanlov **repository'da**
|
|
141
|
+
belgilanadi — `scope` (RBAC) allaqachon shu yo'lda:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
export const usersRepository = registry.repository(User, {
|
|
145
|
+
forcedColumns: { _id: true, fullName: true, email: true }, // parol hech qachon chiqmaydi
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// yoki yumshoqroq: client faqat shu ro'yxatdan tanlaydi
|
|
149
|
+
export const postsRepository = registry.repository(Post, {
|
|
150
|
+
allowedColumns: ["_id", "title", "createdAt"],
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
| Opsiya | Xulqi |
|
|
155
|
+
| ---------------- | ------------------------------------------------------------------------------------- |
|
|
156
|
+
| `forcedColumns` | client `columns`i **butunlay e'tiborsiz** |
|
|
157
|
+
| `allowedColumns` | client tanlovi allowlist bilan **kesiladi**; kesishma bo'sh bo'lsa → allowlist |
|
|
158
|
+
| `scope` | har bir o'qish/yozishga doimiy tenglik filtri (`scoped()` bilan ham berilishi mumkin) |
|
|
159
|
+
| `relations` | populate qilinadigan relation'lar (faqat tip — `with` inference uchun) |
|
|
160
|
+
|
|
161
|
+
Muhim nozikliklar:
|
|
162
|
+
|
|
163
|
+
- Kesishma bo'sh bo'lganda natija **to'liq hujjat emas** — allowlistning o'zi.
|
|
164
|
+
- `aggregate` ham shu guard ostida: `min("password")` yoki
|
|
165
|
+
`groupBy: "password"` projection bilan bir xil miqdorda ma'lumot chiqaradi.
|
|
166
|
+
- `cursorKey` clientdan keladi va cursor maydoni paginatsiya uchun majburan
|
|
167
|
+
tanlanadi — guard bor bo'lsa u maydon so'rovda qoladi, lekin qaytariladigan
|
|
168
|
+
hujjatlardan **olib tashlanadi**.
|
|
169
|
+
- `forcedColumns: {}` yoki `allowedColumns: []` — repository yaratilishida **xato**.
|
|
170
|
+
- ⚠️ Guard **faqat o'qish** metodlariga ta'sir qiladi (`findAll`/`findOne`/
|
|
171
|
+
`findById`/`findList`/`findInfinite`/`findCursor` + `aggregate`). Yozish
|
|
172
|
+
metodlari (`create`, `upsert`, `updateById`, `softDelete`, …) tip kontrakti
|
|
173
|
+
bo'yicha **to'liq hujjatni** qaytaradi. Yozish natijasini clientga
|
|
174
|
+
qaytarishdan oldin `findById` bilan qayta o'qing yoki o'zingiz map qiling.
|
|
175
|
+
|
|
176
|
+
### 4. Noto'g'ri shartlar: kuzatish yoki rad etish
|
|
177
|
+
|
|
178
|
+
Noma'lum filter/sort kaliti default'da **jimgina tashlab yuboriladi**. Muammosi:
|
|
179
|
+
filter natijani _cheklash_ uchun ishlatiladi, shuning uchun typo qilingan kalit
|
|
180
|
+
yo'qolsa endpoint kutilganidan **ko'proq** data qaytaradi.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// 1-qadam: kuzatish (xulq o'zgarmaydi)
|
|
184
|
+
createRegistry(mongoose.connection, {
|
|
185
|
+
onSkippedCondition: info => logger.warn({ querykit: info }, "shart tashlab yuborildi"),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 2-qadam: log tozalanganda — qattiq rejim
|
|
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
|
+
`scope` va `cursorKey` kaliti **har doim** fatal (`strict`dan qat'i nazar).
|
|
204
|
+
|
|
100
205
|
## MongoDB-ga xos jihatlar
|
|
101
206
|
|
|
102
207
|
- **`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:
|
|
@@ -109,6 +214,41 @@ export const usersRepository = registry.repository(User, base => ({
|
|
|
109
214
|
- **Cursor** default `_id` bo'yicha (`cursorKey` bilan o'zgartiring) — insert'larда barqaror.
|
|
110
215
|
- **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
216
|
|
|
217
|
+
- **Indekslar va operatorlar.** `startsWith` — `^` bilan bog'langan, lekin registrga sezgirsiz (`i`) regex: MongoDB uni indeks bilan yomon ishlatadi. Katta kolleksiyada kichik harfli soya maydon (`nameLower`) saqlang va unga `=` / `startsWith` bering. `contains`, `endsWith`, `!=`, `notIn` indeksdan foydalana olmaydi (COLLSCAN) — default `filterable`da yopiq, faqat kichik kolleksiyalar uchun oching. Enum maydonlarida `in` ishlating.
|
|
218
|
+
|
|
219
|
+
## Safe by default — `filterable`, `sortable`, `limits`
|
|
220
|
+
|
|
221
|
+
Mijoz faqat server ruxsat bergan path bo'yicha, faqat ruxsat bergan operator bilan filtrlaydi va saralaydi. Ruxsatsiz so'rov MongoDB'ga yetmasdan `QueryKitError` bilan to'xtaydi — `strict` va `onSkippedCondition`dan qat'i nazar. Opsiya nomlari, default'lar, `reason`/`site` — drizzle-pg va prisma-pg bilan bir xil (`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"], // faqat shu yerda ochilgan
|
|
230
|
+
},
|
|
231
|
+
sortable: ["_id", "createdAt", "name"],
|
|
232
|
+
limits: { maxInItems: 500 },
|
|
233
|
+
maxTimeMS: 1000,
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Opsiya | Berilmasa (default) | Rad etilsa |
|
|
238
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
239
|
+
| `filterable` | `allowedColumns` (yo'q bo'lsa `forcedColumns` kalitlari, u ham yo'q bo'lsa faqat primary key), faqat `"="` va `"in"` | `reason: "forbidden-key"` / `"forbidden-operator"`, `site: "filter"` |
|
|
240
|
+
| `sortable` | primary key va `createdAt` (jadvalda bo'lsa); mijoz yuborgan `cursorKey` ham shu ro'yxatdan | `reason: "forbidden-key"`, `site: "sort"` / `"cursorKey"` |
|
|
241
|
+
| `limits` | core `DEFAULT_QUERY_LIMITS` (chuqurlik 5, shartlar 30, massiv 200, satr 256, offset 10 000, sort 3) | `reason: "limit-exceeded"`, `site: "filter"` / `"sort"` / `"page"` |
|
|
242
|
+
|
|
243
|
+
- `aggregate`: `filter` va har `groupBy`/`sum`/`avg`/`min`/`max` ustuni `filterable` ichida bo'lishi shart (`site: "aggregate"`).
|
|
244
|
+
- `findById`: faqat mijoz `filter`i tekshiriladi; `idKey` — server tanlovi.
|
|
245
|
+
- Jadvalda **yo'q** kalit (`nope`) avvalgidek `unknown-key` — skip yoki `strict`da xato.
|
|
246
|
+
- Mutatsiya metodlari (`updateMany`, `deleteMany`, …) filtri server kodi deb hisoblanadi va tekshirilmaydi.
|
|
247
|
+
- Operator aliaslari bitta: `["="]` `eq`ni ham, `["contains"]` `%_%`ni ham ochadi. `"*"` — barcha operatorlar.
|
|
248
|
+
- Brendsiz xom Mongo filtri (`{ $where: … }`, `{ secret: { $regex: … } }`, `{ $expr: … }`) JSON'dan kelishi mumkin — `forbidden-key`, so'rov yuborilmaydi (R19). Server kodi `raw({...})` bilan belgilaydi; ⚠️ `raw()` `filterable` bilan tekshirilmaydi.
|
|
249
|
+
- LIKE naqshi: `maxLikeWildcards` (2) dan ko'p `%`/`_` → `limit-exceeded`; ketma-ket `%%` bitta hisoblanadi.
|
|
250
|
+
- Har o'qish (`find`, `findOne`, `countDocuments`, `aggregate`) `maxTimeMS` (default 2 000) bilan; oshsa `QueryKitTimeoutError` (`code: "QUERYKIT_TIMEOUT"`) — uni 503/504 qiling, 400 emas.
|
|
251
|
+
|
|
112
252
|
## Ilg'or filterlar
|
|
113
253
|
|
|
114
254
|
Filter — maydon shartlari va mantiqiy guruhlar (`and`/`or`/`not`) daraxti. Yassi massiv = implicit `AND`. `id` — `_id`ga o'giriladi.
|
|
@@ -142,6 +282,8 @@ Sort berilmasa `createdAt DESC` ga, u bo'lmasa `_id DESC` ga tushadi — paginat
|
|
|
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 — count so'ralmaydi (katta jadval): perPage + 1 qator, 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: teng qiymatlar va imzo
|
|
298
|
+
|
|
299
|
+
- Seek kaliti `(cursorKey, _id)`: `cursorKey` unikal bo'lmasa ham (masalan bir xil `createdAt`) hech bir qator tushib qolmaydi va takrorlanmaydi. `cursorKey` primary key'ning o'zi bo'lsa — bitta maydonli seek.
|
|
300
|
+
- Token (v2) ichida faqat skalyar qiymat (satr, son, boolean, ISO sana) bo'ladi va maydon tipiga cast qilinadi; obyekt, massiv yoki mos kelmaydigan qiymat → `QueryKitError` `reason: "invalid-value"`, `site: "cursorKey"` (drayver chaqirilmaydi).
|
|
301
|
+
- Registry opsiyasi `cursorSecret` token'ni HMAC-SHA256 bilan imzolaydi; imzosiz yoki o'zgartirilgan token → `invalid-value`. Usiz token imzosiz — mijoz uni o'zi yasay oladi (faqat ruxsat etilgan `sortable` kalit va filter doirasida).
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
export const registry = createRegistry(mongoose.connection, { cursorSecret: process.env.CURSOR_SECRET });
|
|
305
|
+
```
|
|
306
|
+
|
|
155
307
|
## Tipli relation'lar va maydon tanlash
|
|
156
308
|
|
|
309
|
+
Mijoz `with` bilan faqat `relations`da yozilgan relation'larni ochadi (task 014, R21):
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const postsRepository = registry.repository(Post, {
|
|
313
|
+
relations: { author: { model: User, columns: ["name"] } }, // model — faqat tip uchun
|
|
314
|
+
});
|
|
315
|
+
await postsRepository.findAll({ with: { author: true } }); // author faqat name bilan
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- `columns` majburiy — ro'yxat yoki aniq `"*"`. `with: { rel: true }` ham faqat shu ustunlarni tanlaydi.
|
|
319
|
+
- Ro'yxatda yo'q relation, ustun, filtr maydoni yoki ichki relation → `QueryKitError` `reason: "forbidden-key"`, `site: "with"`, `key` — `relation` yoki `relation.field`; drayver chaqirilmaydi.
|
|
320
|
+
- Ruxsat etilgan opsiyalar: `select`, ichki `populate` (`nested` bilan), `match` (maydonlari `filterable`da; `$where` / `$expr` yo'q), `options.limit` / `options.skip`.
|
|
321
|
+
- Qisqa yozuv `{ author: User }` faqat tip uchun: `with: { author: … }` → `forbidden-key`.
|
|
322
|
+
|
|
157
323
|
O'qish metodlari natija tipini `with` (populate) va `columns`dan chiqaradi — qo'lda generic yo'q:
|
|
158
324
|
|
|
159
325
|
```ts
|
|
@@ -165,10 +331,31 @@ rows[0].id; // ObjectId
|
|
|
165
331
|
rows[0].email; // ❌ tip xatosi — tanlanmagan
|
|
166
332
|
```
|
|
167
333
|
|
|
168
|
-
Populate qilingan relation'larni
|
|
334
|
+
Populate qilingan relation'larni tiplash va ochish uchun ref model'ni rule ichida bering: `registry.repository(Post, { relations: { author: { model: User, columns: ["name"] } } })` (qisqa `{ author: User }` faqat tip — `with`ga ruxsat bermaydi).
|
|
335
|
+
|
|
336
|
+
## Ommaviy yangilash va o'chirish
|
|
337
|
+
|
|
338
|
+
`updateWhere` / `deleteWhere` — bitta yozish so'rovi (oldin o'qish yo'q), natija `{ 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 }`, ko'pi bilan limit ta qator (maxLimit, default 200)
|
|
345
|
+
const { rows } = await usersRepository.updateWhere(filter, patch, { returning: { limit: 50 } });
|
|
346
|
+
|
|
347
|
+
// filtrsiz — faqat aniq ko'rsatilsa (scope baribir qo'llanadi)
|
|
348
|
+
await usersRepository.deleteWhere({ all: true });
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- Bo'sh filtr (`undefined`, `{}`, `[]`) yoki hamma sharti tashlab yuborilgan filtr (masalan faqat noma'lum kalit) → `QueryKitError` `reason: "empty-filter"`, `site: "mutation"` — drayver chaqirilmaydi, jadval jim o'chmaydi.
|
|
352
|
+
- Mutatsiya filtri o'qishdagidek `filterable` va `limits`dan o'tadi (`forbidden-key`, `forbidden-operator`, `limit-exceeded`). Server'ning o'z filtri ham.
|
|
353
|
+
- `rows` atomar **emas**: `updateWhere` — yangilashdan **keyin** shu filtr bilan o'qiladi (patch filtrdagi maydonni o'zgartirsa, boshqa hujjatlar chiqishi mumkin); `deleteWhere` — o'chirishdan **oldin** o'qiladi. `count` — `matchedCount` / `deletedCount`.
|
|
169
354
|
|
|
170
355
|
## Tranzaksiya
|
|
171
356
|
|
|
357
|
+
> ⚠️ Callback ichida **faqat DB amallari**. Tashqi amallar (HTTP, xabar, email) — outbox orqali: drayver tranzaksiyani qayta urinishi mumkin, tashqi amal esa takrorlanadi va qaytarib bo'lmaydi.
|
|
358
|
+
|
|
172
359
|
```ts
|
|
173
360
|
await registry.transaction(async () => {
|
|
174
361
|
await dispatchesRepository.create({ ... });
|
|
@@ -186,6 +373,86 @@ await mine.findList({ page: 1 }); // { supervisorId: user.id, ... }
|
|
|
186
373
|
await mine.create({ ... }); // supervisorId majburan user.id
|
|
187
374
|
```
|
|
188
375
|
|
|
376
|
+
Scope'ni `registry.repository(Model, { scope })` bilan ham berish mumkin.
|
|
377
|
+
Noma'lum scope kaliti — repository yaratilishida **xato** (jimgina tashlansa
|
|
378
|
+
RBAC filtri yo'qolib ketardi).
|
|
379
|
+
|
|
380
|
+
Scope qiymati ro'yxat ham bo'lishi mumkin (task 014): `{ shopId: { in: [1, 2] } }` — har o'qish va yozish shu qiymatlar bilan cheklanadi; bo'sh ro'yxat (`{ in: [] }`) — hech qanday qator (xato emas). `create` faqat tenglik scope'larini to'ldiradi.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
const mine = repository.scoped({ shopId: { in: user.shopIds } });
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Qiymatlar, NULL tartibi va bir xillik
|
|
387
|
+
|
|
388
|
+
Uchala adapter (drizzle-pg, prisma-pg, mongoose) bir xil natija beradi (task 021):
|
|
389
|
+
|
|
390
|
+
- **Literal qiymat operatorlari.** `contains`, `startsWith`, `endsWith` qiymatni harfma-harf qidiradi: `startsWith "%"` faqat `%` bilan boshlanuvchi qatorlar, `contains "_"` — haqiqiy `_`. Wildcard faqat `like` / `ilike` / `notLike` da.
|
|
391
|
+
- **Tip cast'i.** Har qiymat maydon tipiga keltiriladi (son, boolean, sana, ObjectId, enum). Mos kelmasa (`age = "abc"`) — `invalid-value`: shart tashlanadi va `onSkippedCondition` chaqiriladi, `strict` da `QueryKitError`; drayver chaqirilmaydi.
|
|
392
|
+
- **`not`** — SQL uch qiymatli mantiq: NULL maydon bo'yicha `not(and(…))` / `not(or(…))` hamma adapterda bir xil.
|
|
393
|
+
- **NULL tartibi.** `asc` — NULL birinchi, `desc` — NULL oxirida (MongoDB tartibi).
|
|
394
|
+
- **Buzuq sort elementi** (`null`, `{}`) — tashlanadi va `invalid-value` (`site: "sort"`) bilan xabar qilinadi, `strict` da xato.
|
|
395
|
+
|
|
396
|
+
## Migrating to 3.0
|
|
397
|
+
|
|
398
|
+
3.0 da siyosatsiz repository **tor**: faqat `allowedColumns` / `forcedColumns` / `_id` bo'yicha `=` va `in`, sort — `_id` va `createdAt`. 2.x xulqini bitta repository uchun ataylab qaytarish:
|
|
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
|
+
Xom Mongo filtri endi `raw()` bilan (R19):
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { raw } from "@querykitjs/core";
|
|
411
|
+
|
|
412
|
+
// 2.x — 3.0 da forbidden-key:
|
|
413
|
+
// await usersRepository.findAll({ filter: { age: { $gt: 18 } } });
|
|
414
|
+
|
|
415
|
+
// 3.0:
|
|
416
|
+
await usersRepository.findAll({ filter: raw({ age: { $gt: 18 } }) });
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- Yangi `reason`lar: `forbidden-key`, `forbidden-operator`, `limit-exceeded`; yangi `site`: `page` — 400 qiling.
|
|
420
|
+
- Offset 10 000 dan chuqur sahifalar rad etiladi — `findCursor` ishlating yoki `limits: { maxOffset }` bilan oshiring.
|
|
421
|
+
- Har o'qish `maxTimeMS: 2000` bilan; uzoqroq hisobot so'rovlari uchun repository'da `maxTimeMS`ni oshiring. Vaqt tugasa `QueryKitTimeoutError`.
|
|
422
|
+
- LIKE naqshida 2 tadan ko'p wildcard rad etiladi (`maxLikeWildcards`).
|
|
423
|
+
- Cursor token formati v2 (task 012): eski (v1) `next_cursor` / `prev_cursor` endi `invalid-value` bilan rad etiladi — yangilashdan keyin paginatsiyani birinchi sahifadan boshlang.
|
|
424
|
+
- `updateWhere` / `deleteWhere` endi `Row[]` emas, `{ count }` qaytaradi (task 013); qatorlar kerak bo'lsa `{ returning: { limit } }` → `{ count, rows }`.
|
|
425
|
+
- Filtrsiz `updateWhere` / `deleteWhere` → `empty-filter`; hamma qatorlar uchun `{ all: true }` yozing. Mutatsiya filtri endi `filterable` siyosatidan o'tadi.
|
|
426
|
+
- `OffsetMeta.total_items` / `total_pages` tipi `number | null` (`withTotal: false` bilan `null`).
|
|
427
|
+
- `with` endi faqat `relations` allow-list bilan ishlaydi (task 014): ro'yxatsiz repository'da `with` → `forbidden-key`, `site: "with"`.
|
|
428
|
+
- `relations: { author: User }` → `relations: { author: { model: User, columns: ["name"] } }` (qisqa yozuv endi `with`ga ruxsat bermaydi).
|
|
429
|
+
- Scope qiymati `{ in: [...] }` bo'lishi mumkin; `timestamps` registry opsiyasi createdAt / deletedAt nomlarini beradi.
|
|
430
|
+
|
|
431
|
+
## Wire (JSON) qiymatlari va DbService'dan migratsiya
|
|
432
|
+
|
|
433
|
+
Backend so'rov body'sini JSON'da oladi, ya'ni sana har doim **string** bo'lib
|
|
434
|
+
keladi. Adapter uni schema path tipiga qarab avtomatik cast qiladi:
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{ "key": "createdAt", "operation": "<=", "value": "2026-07-28T12:00:00.000Z" }
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- `Date` path'lari → ISO string avtomatik `Date`ga aylanadi. Qamrov **ataylab**
|
|
441
|
+
drizzle-pg bilan bir xil: u ham faqat JS orqali map qilinadigan tiplarni
|
|
442
|
+
hukm qiladi, qolganini bazaga qoldiradi.
|
|
443
|
+
- `in`/`notIn` massivlari, `between`/`notBetween` tuple'lari va cursor token ham
|
|
444
|
+
qamraladi (`cursorKey: "createdAt"` ishlaydi).
|
|
445
|
+
- Mongo `Date` path'da `$regex` qila olmaydi (`Can't use $options with Date`),
|
|
446
|
+
shuning uchun date maydonidagi text-pattern operatori (`contains`/`ilike`/…)
|
|
447
|
+
`invalid-value` sifatida **tashlab yuboriladi** va `onSkippedCondition`da ko'rinadi —
|
|
448
|
+
SQL adapterlar ham shunday qiladi.
|
|
449
|
+
- Parse bo'lmaydigan qiymat (`"not-a-date"`) shartni bekor qiladi (`strict`da —
|
|
450
|
+
400). `in` ro'yxatidagi **bitta** yaroqsiz element butun shartni bekor qiladi.
|
|
451
|
+
|
|
452
|
+
**DbService migratsiyasi:** eski wire'dagi `type: "date"` maydoni endi keraksiz —
|
|
453
|
+
`@querykitjs/zod` uni strip qiladi (xato bermaydi), coercion esa server tomonda
|
|
454
|
+
path tipidan avtomatik.
|
|
455
|
+
|
|
189
456
|
## Soft-delete
|
|
190
457
|
|
|
191
458
|
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.
|
|
@@ -207,22 +474,22 @@ await ordersRepository.aggregate({ sum: "amount", groupBy: "region" });
|
|
|
207
474
|
|
|
208
475
|
## API ma'lumotnoma
|
|
209
476
|
|
|
210
|
-
| Metod
|
|
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)`
|
|
477
|
+
| Metod | Qaytaradi |
|
|
478
|
+
| ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
|
|
479
|
+
| `findAll(params?)` | `Row[]` |
|
|
480
|
+
| `findOne(params?)` / `findById(id, params?)` | `Row \| undefined` |
|
|
481
|
+
| `findList(params?)` | `{ data, meta }` offset |
|
|
482
|
+
| `findInfinite(params?)` | `{ data, meta }` infinite |
|
|
483
|
+
| `findCursor(params?)` | `{ data, meta }` cursor |
|
|
484
|
+
| `count(filter?)` / `exists(filter?)` | `number` / `boolean` |
|
|
485
|
+
| `aggregate(spec)` | `AggregateRow[]` |
|
|
486
|
+
| `create(values)` / `createMany(values)` | `Row` / `Row[]` |
|
|
487
|
+
| `upsert(values, opts)` / `upsertMany(values, opts)` | `Row` / `Row[]` |
|
|
488
|
+
| `updateById(id, patch, idKey?)` / `updateWhere(filter, patch, options?)` | `Row \| undefined` / `{ count }` (`{ count, rows }` with `returning`) |
|
|
489
|
+
| `deleteById(id, idKey?)` / `deleteWhere(filter, options?)` | `Row \| undefined` / `{ count }` (`{ count, rows }` with `returning`) |
|
|
490
|
+
| `softDelete(id)` / `restore(id)` | `Row \| undefined` |
|
|
491
|
+
| `scoped(scope)` | scoped `Repository` |
|
|
492
|
+
| `registry.transaction(fn)` | `fn` natijasi |
|
|
226
493
|
|
|
227
494
|
## Ishlab chiqish
|
|
228
495
|
|