@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.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
- `defaultPerPage` (findList) / `defaultLimit` (infinite/cursor) berilmasa `@querykitjs/core`ning **20**siga tushadi. `connection` tashqaridan beriladi (tranzaksiya to'g'ri session'ni ulashsin uchun).
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 runtime'да tiplash uchun ref model'larni bering: `registry.repository(Post, { relations: { author: User } })`.
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 | 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 |
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