@querykitjs/web 1.0.0 → 2.0.1

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
@@ -6,47 +6,131 @@
6
6
 
7
7
  # @querykitjs/web
8
8
 
9
- > Frontend uchun **tipli query-building**: filter/sort/pagination payloadini quradi, javob meta'sini map qiladi, URL-state sync beradi. **So'rov yubormaydi** — chiqqan payload'ni o'z `fetch`/`axios`ingizga uzatasiz.
9
+ [![npm](https://img.shields.io/npm/v/@querykitjs/web.svg)](https://www.npmjs.com/package/@querykitjs/web) [![license](https://img.shields.io/npm/l/@querykitjs/web.svg)](./LICENSE)
10
10
 
11
- React dashboardlar har list sahifasida bir xil boilerplate'ni qo'lda yozadi: `searchParams`'dan `IFilter[]` qurish, bo'sh filterlarni tashlash, `sortType` ↔ `{name,direction}`, "har o'zgarishda page-reset", meta snake→camel. `@querykitjs/web` shularni bartaraf qiladi. Payload querykit backend (`@querykitjs/drizzle-pg`) qabul qiladigan formatda.
11
+ > Frontend uchun **tipli query-building**: filter/sort/pagination payloadini quradi, javob meta'sini map qiladi, URL-state sync beradi. **So'rov yubormaydi** — chiqqan payload'ni o'z `fetch`/`axios`ingizga uzatasiz. Zero-dependency.
12
+
13
+ React dashboardlar har list sahifasida bir xil boilerplate'ni qo'lda yozadi: `searchParams`'dan `IFilter[]` qurish, bo'sh filterlarni tashlash, `sortType` ↔ `{name,direction}`, "har o'zgarishda page-reset", meta snake→camel. `@querykitjs/web` shularni bartaraf qiladi. Payload querykit backend (`@querykitjs/drizzle-pg`, mongoose adapter, ...) qabul qiladigan formatда.
12
14
 
13
15
  ```ts
14
- import { buildListParams, f, mapMeta } from "@querykitjs/web";
16
+ // src/lib/query.ts — bir marta sozlang
17
+ import { createRegistry } from "@querykitjs/web";
15
18
 
16
- const params = buildListParams({
17
- filter: f.and(f.contains("buyerName", search), f.eq("status", status)),
18
- sort: "-createdAt",
19
- page,
20
- perPage: 20,
19
+ export const qk = createRegistry({
20
+ adapter: "drizzle-pg", // `with` intellisense'ini backend'ga moslaydi
21
+ defaults: { perPage: 20, sort: "-createdAt" },
21
22
  });
22
- const { data, meta } = (await http.post("/buyers/list", params)).data; // o'z axios/fetch
23
- setMeta(mapMeta(meta));
23
+ ```
24
+
25
+ ```tsx
26
+ // istalgan list sahifasi — butun boilerplate bir hookka jamlanadi
27
+ import { useQuery } from "@tanstack/react-query";
28
+ import { useListParams } from "@querykitjs/web/react";
29
+ import { qk } from "@/lib/query";
30
+
31
+ const users = qk.resource<IUser>("users");
32
+ const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
33
+
34
+ function UsersPage() {
35
+ const { params, setParam, setPage } = useListParams(users, { schema: usersSchema });
36
+ const { data } = useQuery({
37
+ queryKey: users.keys.list(params),
38
+ queryFn: async () => users.parseList((await axios.post("/users/list", params)).data),
39
+ });
40
+ // data = { data: IUser[], meta: { totalPages, currentPage, hasNext, … } }
41
+ }
24
42
  ```
25
43
 
26
44
  ## Imkoniyatlar
27
45
 
28
- - **Ikkala filter uslubi** — `f.eq()...` builder VA object/massiv (`{key, operation, value}`). Legacy `IFilter[]` bilan to'liq mos.
29
- - **Normalizatsiya** — bo'sh filterlarni prune, sort decode. Qiymatlar o'zgartirilmasdan o'tadi.
30
- - **Declarative schema** — `defineListSchema` bilan URL param → filter; har sahifadagi qo'lda qurishni yo'q qiladi.
31
- - **URL-state sync** — `searchParams` ↔ params, `sortType` encode/decode, page-reset. `searchParams` tashqaridan olinadi (router-agnostik).
32
- - **React hook** — `useListParams` (peer `react`, `./react` subpath).
33
- - **`mapMeta`** — snake→camel pagination meta.
34
- - **Zero dependency** — axios/react-query/react-router/dayjs YO'Q. `react` faqat hook uchun ixtiyoriy peer.
46
+ - **Registry** — `adapter` + default'larni bir joyda sozlang, `resource<T>()` entity'ga tipli, adapter-aware builder/filter/schema/keys/parser beradi.
47
+ - **Ikkala filter uslubi** — `f.eq()...` builder (maydon tipiga qarab operator) VA object/massiv (`{key, operation, value}`). Legacy `IFilter[]` bilan to'liq mos.
48
+ - **3 pagination** — offset (`list`), infinite (`infinite`), cursor/keyset (`cursor`); har birining meta'si alohida `parse*` bilan camelCase'ga o'giriladi.
49
+ - **Adapter-aware `with`** — `mongoose`/`drizzle-pg`/`drizzle-sqlite`/`prisma-pg` uchun faqat **wire-safe** (JSON) relation opsiyalari intellisense'да; funksiya/SQL formalarи ko'rsatilmaydi.
50
+ - **Declarative schema** — `resource.schema` / `defineListSchema` bilan URL param → filter; har sahifadagi qo'lda qurishni yo'q qiladi.
51
+ - **React hook** — `useListParams` (`react-router-dom` ichida), `useListParamsBase` (router-agnostik). Core framework'siz — Vue ham shu core'ga bog'lanadi.
52
+ - **SSR** — `resource.fromSearchParams(sp, {schema})` hook'siz, `URLSearchParams`'dan to'g'ridan-to'g'ri payload.
53
+ - **Query keys** — `resource.keys` TanStack/SWR uchun barqaror kalitlar.
54
+ - **Zero dependency** — axios/react-query/dayjs YO'Q. `react` (+ hook uchun `react-router-dom`) ixtiyoriy peer.
35
55
 
36
56
  ## O'rnatish
37
57
 
38
58
  ```bash
39
59
  bun add @querykitjs/web
40
- # React hook uchun loyihangizda react bo'lishi kifoya (peer)
60
+ # React hook uchun: react (peer). `useListParams` uchun qo'shimcha: react-router-dom (peer).
61
+ # Router-agnostik `useListParamsBase` react-router-dom talab qilmaydi.
41
62
  ```
42
63
 
43
- ## Filter — ikkala uslub
64
+ ## Ikki xil ishlatish
65
+
66
+ ```ts
67
+ // (A) Registry — bir marta sozlang, entity-tipli + adapter-aware builder'lar
68
+ import { createRegistry } from "@querykitjs/web";
69
+
70
+ // (B) Standalone — free-form, doim mavjud
71
+ import { buildListParams, f } from "@querykitjs/web";
72
+ ```
73
+
74
+ (A)ни — default'lar bir joyda, autocomplete, adapter `with`, query keys, meta parsing kerak bo'lsa. (B)ни — bir martalik/free-form qurish uchun.
75
+
76
+ ## `createRegistry(config)` — bir marta sozlash
77
+
78
+ ```ts
79
+ export const qk = createRegistry({
80
+ adapter: "mongoose", // "mongoose" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg"
81
+ defaults: {
82
+ perPage: 20, // list (offset) sahifa hajmi
83
+ limit: 20, // infinite + cursor hajmi
84
+ sort: "-createdAt", // list + infinite tartibi (boshidagi "-" = desc)
85
+ cursor: { order: "asc" }, // faqat cursor: kalit bo'ylab yurish tartibi
86
+ },
87
+ pruneEmpty: true, // bo'sh filterlarni tashlash (""/null/undefined/[]); 0/false qoladi
88
+ });
89
+ ```
90
+
91
+ | Config | Vazifasi |
92
+ | ----------------------- | --------------------------------------------------------------------------------- |
93
+ | `adapter` | Backend turi — `with` tipini moslaydi (faqat intellisense). |
94
+ | `defaults.perPage` | `list` uchun default `perPage`. |
95
+ | `defaults.limit` | `infinite` va `cursor` uchun default `limit`. |
96
+ | `defaults.sort` | `list`/`infinite` default sort (`"-field"` string yoki `{name,direction}`). |
97
+ | `defaults.cursor.order` | Default cursor tartibi (`"asc"`/`"desc"`). |
98
+ | `pruneEmpty` | Form input'lardan qurilgan bo'sh filterlarni avtomatik tashlash (default `true`). |
99
+
100
+ Har bir default har chaqiriqда override qilinadi.
101
+
102
+ ## `qk.resource<TEntity>(name)` — entity qatlami
103
+
104
+ ```ts
105
+ import type { IUser } from "@my/api-types";
106
+ export const users = qk.resource<IUser>("users"); // "users" = cache-key namespace (majburiy, unikal)
107
+ ```
108
+
109
+ | A'zo | Nima |
110
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
111
+ | `users.list(input)` | offset (page) payload quradi |
112
+ | `users.infinite(input)` | infinite (limit/offset) payload quradi |
113
+ | `users.cursor(input)` | cursor (keyset) payload quradi |
114
+ | `users.params(input)` | pagination'siz payload (filter/sort/columns/with) |
115
+ | `users.f` | `createFilters<IUser>()` — maydon-tipli builder |
116
+ | `users.search(term, fields)` | ko'p maydon bo'ylab OR-of-`contains` preset |
117
+ | `users.schema(desc)` | `defineListSchema` bog'langan (URL ↔ filter) |
118
+ | `users.fromSearchParams(sp, {schema})` | `URLSearchParams`'dan to'g'ridan-to'g'ri list payload (SSR / hook'siz) |
119
+ | `users.keys` | TanStack/SWR uchun barqaror query keys |
120
+ | `users.parseList / parseInfinite / parseCursor` | javob meta'sini snake→camel |
121
+ | `ListResult<T> / InfiniteResult<T> / CursorResult<T>` (tiplar) | `parse*` qaytaradigan tiplar |
122
+
123
+ Barcha builder'lar registry'ning `defaults` + `pruneEmpty`ini meros oladi; tiplar `IUser` va `adapter`dan kelib chiqadi.
124
+
125
+ > URL setter'lar (`setParam`/`setPage`/`setSort`/`reset`) config'siz — standalone `url` helper'lar yoki `useListParams` hook orqali. `users.url` yo'q.
126
+
127
+ ## Filter — `f` (maydon-tipli) va object/massiv
44
128
 
45
129
  ```ts
46
130
  import { createFilters } from "@querykitjs/web";
47
- const f = createFilters<Buyer>(); // maydon nomi autocomplete
131
+ const f = createFilters<Buyer>(); // yoki: const f = users.f;
48
132
 
49
- // 1) Builder
133
+ // 1) Builder — maydon nomi autocomplete + maydon tipiga qarab operator
50
134
  filter: f.and(f.contains("buyerName", s), f.eq("status", st));
51
135
 
52
136
  // 2) Object/massiv (idistr kabi) — flat massiv = implicit AND
@@ -56,137 +140,328 @@ filter: [
56
140
  ];
57
141
  ```
58
142
 
59
- **Operatorlar:** `= != > >= < <=`, `%_%`/`contains`, `%_`/`startsWith`, `_%`/`endsWith`, `like`, `ilike`, `notLike`, `in`, `notIn`, `between`, `isNull`, `isNotNull` (querykit backend bilan bir xil to'plam). Builder helperlar: `f.eq/ne/gt/gte/lt/lte`, `f.contains/startsWith/endsWith`, `f.like/ilike/notLike`, `f.in/notIn`, `f.between`, `f.range` (sana diapazoni → ikki shart), `f.isNull/isNotNull`, `f.and/or/not`.
143
+ **Operator-per-field-type:** `f.contains`/`like`/`ilike`/`startsWith`/... faqat **string** maydonlarга, `f.gt`/`gte`/`between`/... **taqqoslanadigan** (string/number/Date) maydonlarга taklif qilinadi:
144
+
145
+ ```ts
146
+ f.contains("buyerName", s); // ✓ buyerName: string
147
+ f.gte("age", 18); // ✓ age: number
148
+ f.contains("age", "x"); // ✗ compile-error — `contains` string-only
149
+ ```
150
+
151
+ **Operatorlar:** `= != > >= < <=`, `%_%`/`contains`, `%_`/`startsWith`, `_%`/`endsWith`, `like`, `ilike`, `notLike`, `in`, `notIn`, `between`, `isNull`, `isNotNull`. Builder helperlar: `f.eq/ne/gt/gte/lt/lte`, `f.contains/startsWith/endsWith`, `f.like/ilike/notLike`, `f.in/notIn`, `f.between`, `f.range` (diapazon → ikki shart), `f.isNull/isNotNull`, `f.and/or/not`.
60
152
 
61
- ## Payload qurish
153
+ **`search` preset** — bitta input, ko'p ustun (OR of contains):
62
154
 
63
155
  ```ts
64
- import { buildParams, buildListParams } from "@querykitjs/web";
156
+ users.search("ali", ["buyerName", "email"]);
157
+ // → or(contains("buyerName","ali"), contains("email","ali"))
158
+ ```
65
159
 
66
- const payload = buildListParams({
67
- filter, // builder yoki massiv
68
- sort: "-createdAt", // yoki { name, direction }
69
- page,
70
- perPage: 20,
71
- with: { supervisor: true }, // relations (default `with`)
72
- });
73
- // -> { filter:[...pruned], sort:{name,direction}, columns, with, page, perPage }
160
+ Tipsiz `f` ham bor: `import { f } from "@querykitjs/web"` (maydon-tip tekshiruvisiz, tez ishlatish).
161
+
162
+ ## Builder'lar — kirish va **nima qaytaradi**
163
+
164
+ Bular **sof, sinxron** funksiyalar. **Payload obyekt** qaytaradi (Promise emas, data emas). O'zingiz yuborasiz.
165
+
166
+ ```ts
167
+ // list — offset (page)
168
+ users.list({ filter?, sort?, columns?, with?, withDeleted?, page?, perPage? });
169
+ // → { filter, sort:{name,direction}, columns, with, page, perPage, withDeleted? }
170
+
171
+ // infinite — limit/offset
172
+ users.infinite({ filter?, sort?, columns?, with?, withDeleted?, limit?, offset? });
173
+ // → { filter, sort, columns, with, limit, offset }
174
+
175
+ // cursor — sort YO'Q, order + direction ishlatiladi
176
+ users.cursor({ filter?, columns?, with?, withDeleted?, limit?, cursor?, order?, direction?, cursorKey? });
177
+ // → { filter, columns, with, limit, cursor, order, direction, cursorKey? }
74
178
  ```
75
179
 
76
180
  Bo'sh qiymatli filterlar tashlanadi (`""`/`null`/`undefined`/`[]`), lekin `0`/`false` saqlanadi. Qiymatlar o'zgartirilmasdan yuboriladi.
77
181
 
78
- ## Paginatsiya rejimlari (offset / infinite / cursor)
182
+ ## Paginatsiya + `parse*` (meta)
79
183
 
80
- Backend'ning 3 rejimiga mos 3 builder. Har birining javob meta'si **har xil**, shuning uchun alohida mapper bor:
184
+ Backend'ning 3 rejimiga mos 3 builder. Har birining javob meta'si **har xil** (wire'да snake_case), shuning uchun alohida parser:
185
+
186
+ | Builder | Backend qaytaradi | `parse*` → camelCase |
187
+ | ---------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
188
+ | `list` | `{ data, meta: { total_items, total_pages, current_page, per_page, has_next, has_prev } }` | `{ totalCount, totalPages, currentPage, perPage, hasNext, hasPrev }` |
189
+ | `infinite` | `{ data, meta: { limit, offset, count, has_more, next_offset } }` | `{ limit, offset, count, hasMore, nextOffset }` |
190
+ | `cursor` | `{ data, meta: { limit, has_next, has_prev, next_cursor, prev_cursor } }` | `{ limit, hasNext, hasPrev, nextCursor, prevCursor }` |
81
191
 
82
192
  ```ts
83
- import { buildListParams, buildInfiniteParams, buildCursorParams, mapMeta, mapInfiniteMeta, mapCursorMeta } from "@querykitjs/web";
193
+ const r = users.parseList(res.data); // { data: IUser[], meta: { totalPages, currentPage, hasNext, … } }
194
+ users.parseInfinite(res.data); // { data, meta: { hasMore, nextOffset, … } }
195
+ users.parseCursor(res.data); // { data, meta: { nextCursor, hasNext, … } }
196
+ ```
84
197
 
85
- // 1) Offset — page / perPage
86
- const p = buildListParams({ filter, page: 2, perPage: 20 });
87
- mapMeta(res.meta); // { totalPages, totalCount, currentPage, perPage, hasNext, hasPrev }
198
+ `parse*` ikki darajaда bor (bitta umumiy implementatsiya — meta entity-agnostik):
199
+ `qk.parseList/...` (registry, `data: unknown[]`) — `resource` bo'lmasa; `users.parseList/...` (entity'ga tipli).
88
200
 
89
- // 2) Infinite — limit / offset
90
- const p = buildInfiniteParams({ filter, limit: 20, offset: 40 });
91
- mapInfiniteMeta(res.meta); // { limit, offset, count, hasMore, nextOffset }
201
+ ## Adapter-aware, wire-safe `with`
92
202
 
93
- // 3) Cursor — limit / cursor / order / direction (sort ishlatilmaydi)
94
- const p = buildCursorParams({ filter, limit: 20, cursor, order: "asc" });
95
- mapCursorMeta(res.meta); // { limit, hasNext, hasPrev, nextCursor, prevCursor }
203
+ Registry `with`ni adapter bo'yicha tiplaydi — **faqat serializable** opsiyalar:
96
204
 
97
- // withDeleted — soft-delete'lilarni ham ko'rish
98
- buildListParams({ filter, withDeleted: true });
205
+ | adapter | `with[rel]` intellisense | Ko'rsatilmaydi |
206
+ | ------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------- |
207
+ | `mongoose` | `boolean \| { select?, populate?, match?, options? }` | — |
208
+ | `drizzle-pg` / `drizzle-sqlite` | `boolean \| { columns?, with?, limit?, offset? }` | `where:(f,op)=>…`, `orderBy:(f,op)=>…`, raw `sql` |
209
+ | `prisma-pg` | `boolean \| { select?, include?, where?, orderBy?, take?, skip? }` | funksiya formalar |
210
+
211
+ ```ts
212
+ // adapter: "mongoose"
213
+ users.list({ with: { author: { select: "name email" } } }); // ✓ `select`
214
+ // adapter: "drizzle-pg"
215
+ users.list({ with: { author: { columns: { name: true } } } }); // ✓ `columns`; `where`-fn yashirin
99
216
  ```
100
217
 
101
- ## Declarative list schema
218
+ Runtime o'zgarmaydi (pass-through) — bu sof DX. Funksiya/SQL formalarини JSON'да yubora olmaganingiz uchun ular umuman taklif qilinmaydi.
102
219
 
103
- Har sahifada qo'lda `IFilter[]` qurish o'rniga bir marta e'lon qiling:
220
+ ## Declarative list schema — URL ↔ filter
104
221
 
105
- ```ts
106
- import { defineListSchema, searchParamsToPayload } from "@querykitjs/web";
222
+ Har sahifada qo'lда `IFilter[]` qurish o'rniga bir marta e'lon qiling. `useListParams` (va `schemaToFilter`) uni qo'llaydi: har param'ni o'qiydi, operatsiyani bajaradi, bo'shlarni tashlaydi, default'ларни to'ldiradi.
107
223
 
108
- const buyersSchema = defineListSchema({
224
+ ```ts
225
+ const dispatchesSchema = dispatches.schema({
109
226
  id: { operation: "=" },
110
- buyerName: { operation: "%_%", trim: true },
111
- status: { operation: "=" },
112
- createdAt: { range: ["fromDate", "toDate"] }, // → >= va <=
227
+ status: { operation: "=", default: "active" }, // ?status yo'q → "active"
228
+ productIds: { operation: "in", split: "," }, // ?productIds=1,2,3 → in [1,2,3]
229
+ q: { search: ["name", "imei", "appNumber"] }, // ?q=… → contains-OR ko'p maydon
230
+ name: { operation: "%_%", trim: true }, // qiymatni trim qiladi
231
+ createdAt: { range: ["fromDate", "toDate"] }, // ikki param → >= va <= (inclusive)
232
+ // price: { between: ["minPrice", "maxPrice"] }, // YOKI: bitta `between` shart (2-tuple)
113
233
  });
114
-
115
- const params = searchParamsToPayload(buyersSchema, searchParams);
116
234
  ```
117
235
 
236
+ ### FieldDescriptor rejimlari
237
+
238
+ | Kalit | Ma'nosi |
239
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `operation` | filter operatsiyasi (`"="`, `"%_%"`, `"in"`, …). **Aniq** — `true` shorthand yo'q. |
241
+ | `default` | URL param **yo'q** bo'lganда ishlatiladigan qiymat (qo'lda `\|\| "active"`ni almashtiradi). |
242
+ | `split` | massiv operatsiyalar (`in`/`notIn`) uchun: comma-string'ni massivga bo'ladi (default `","`). **Guarded** — yo'q/bo'sh → tashlanadi (hech qachon `[undefined]` emas). |
243
+ | `range: [from, to]` | ikki param → **ikki inclusive** shart (`>= from`, `<= to`); har chegara mustaqil. |
244
+ | `between: [from, to]` | ikki param → **bitta** `between` shart (2-tuple). **Ikkала chegара kerak** — bir tomonli → hech nima (mustaqil chegara uchun `range`). |
245
+ | `trim` | string qiymatni trim qiladi. |
246
+ | `search: [fields]` | bitta param → sanab o'tilган maydonlar bo'yicha `contains`-OR. |
247
+
248
+ **Har descriptor'га aynan bitta rejim** — Simple (`operation` + `trim`/`default`/`split`), `range`, `between`, `search` **o'zaro eksklyuziv** va compile-time'да majburlanadi (masalan `range` + `search` aralashsa — tip xato).
249
+
250
+ Bo'sh/**yo'q** paramlar tashlanadi; `default` faqat param **yo'q** bo'lganда to'ldiradi (bo'sh `?x=` = "tozalangan" → tashlanadi, default emas).
251
+
118
252
  ## React hook
119
253
 
120
254
  ```tsx
121
- import { useSearchParams } from "react-router-dom"; // yoki boshqa manba
122
- import { useListParams } from "@querykitjs/web/react";
255
+ import { useListParams } from "@querykitjs/web/react"; // react-router-dom ichida ishlatiladi
256
+ import { qk } from "@/lib/query";
123
257
 
124
- function BuyersList() {
125
- const [searchParams, setSearchParams] = useSearchParams();
126
- const { params, setParam, setPage, setSort } = useListParams({
127
- searchParams,
128
- setSearchParams,
129
- schema: buyersSchema,
258
+ const users = qk.resource<IUser>("users");
259
+ const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
260
+
261
+ function UsersListPage() {
262
+ // ?page/?size/?sortType/?name/... ni URL'дан o'qiydi (ichида useSearchParams)
263
+ const { params, setParam, setPage, setSort, reset } = useListParams(users, {
264
+ schema: usersSchema,
265
+ with: { manager: true }, // qat'iy relation'lar (URL'дан emas)
130
266
  });
131
267
 
132
- const { data, meta } = useMyList(params); // o'z fetch/react-query
268
+ const q = useQuery({
269
+ queryKey: users.keys.list(params),
270
+ queryFn: async () => users.parseList((await axios.post("/users/list", params)).data),
271
+ placeholderData: prev => prev, // yuklanayotganда oldingi sahifani ushlab turadi
272
+ });
133
273
 
134
- // setParam("status", "active") -> URL yangilanadi, page reset bo'ladi
135
- // setPage(2), setSort("-createdAt")
274
+ // setParam("status","active") → URL yangilanadi, page reset; setPage(2); setSort("-createdAt")
136
275
  }
137
276
  ```
138
277
 
139
- `searchParams` (`URLSearchParams`) va `setSearchParams` tashqaridan uzatiladi — kutubxona `react-router`ga bog'lanmaydi.
278
+ Input/sort/page o'zgarishi URL'ni o'zgartiradi → `params` o'zgaradi → `queryKey` o'zgaradi → TanStack qayta so'raydi (va har kombinatsiyani cache qiladi).
140
279
 
141
- ## So'rovni o'zingiz yuborasiz
280
+ **Router-agnostik** (Next.js, TanStack Router) — base hook'ga `searchParams` bering (`react-router-dom` talab qilmaydi):
142
281
 
143
- Kutubxona faqat **so'rovgacha** ishlaydi. Transport yo'q:
282
+ ```tsx
283
+ import { useListParamsBase } from "@querykitjs/web/react";
284
+ const { params, setPage } = useListParamsBase(users, { schema: usersSchema, searchParams, setSearchParams });
285
+ ```
286
+
287
+ ## SSR — `fromSearchParams` (hook'siz)
288
+
289
+ Server component / `getServerSideProps` uchun — URL'дан to'g'ridan-to'g'ri payload:
144
290
 
145
291
  ```ts
146
- // fetch
147
- const res = await fetch("/api/buyers/list", {
148
- method: "POST",
149
- headers: { "Content-Type": "application/json" },
150
- body: JSON.stringify(params),
151
- });
152
- const { data, meta } = await res.json();
292
+ // Next.js server component
293
+ export default async function Page({ searchParams }) {
294
+ const sp = new URLSearchParams(searchParams);
295
+ const body = users.fromSearchParams(sp, { schema: usersSchema });
296
+ const res = await fetch(`${API}/users/list`, { method: "POST", body: JSON.stringify(body) });
297
+ const { data, meta } = users.parseList(await res.json());
298
+ }
299
+ ```
300
+
301
+ ## Query keys — `users.keys`
153
302
 
154
- // axios
155
- const { data } = await http.post("/buyers/list", params);
156
- setMeta(mapMeta(data.meta));
303
+ TanStack Query / SWR cache uchun barqaror, strukturaли kalitlar:
304
+
305
+ ```ts
306
+ users.keys.all; // ["users"]
307
+ users.keys.list(payload); // ["users", "list", payload]
308
+ users.keys.infinite(payload); // ["users", "infinite", payload]
309
+ users.keys.cursor(payload); // ["users", "cursor", payload]
310
+ users.keys.detail(id); // ["users", "detail", id]
157
311
  ```
158
312
 
159
- ## Boshqa / legacy backend (custom field nomlari)
313
+ ## To'liq misollar
160
314
 
161
- Default querykit-canonical camelCase (`perPage`, `with`). Eski snake-backend (masalan idistr) uchun sozlang:
315
+ ### axios — offset / infinite / cursor
316
+
317
+ ```ts
318
+ import { qk } from "@/lib/query";
319
+ const users = qk.resource<IUser>("users");
320
+
321
+ // offset
322
+ const listRes = await axios.post("/users/list", users.list({ filter: users.f.eq("status", "active"), page }));
323
+ const { data, meta } = users.parseList(listRes.data); // meta.currentPage, meta.totalPages…
324
+
325
+ // infinite
326
+ const infRes = await axios.post("/users/infinite", users.infinite({ limit: 20, offset }));
327
+ const inf = users.parseInfinite(infRes.data); // inf.meta.hasMore, inf.meta.nextOffset
328
+
329
+ // cursor
330
+ const curRes = await axios.post("/users/cursor", users.cursor({ limit: 20, cursor }));
331
+ const cur = users.parseCursor(curRes.data); // cur.meta.nextCursor, cur.meta.hasNext
332
+ ```
333
+
334
+ ### TanStack `useQuery` (offset)
335
+
336
+ ```tsx
337
+ import { useQuery } from "@tanstack/react-query";
338
+ const users = qk.resource<IUser>("users");
339
+
340
+ function useUsers(filters: { status?: string; search?: string }, page: number) {
341
+ const body = users.list({
342
+ filter: [users.f.eq("status", filters.status), users.f.contains("name", filters.search)],
343
+ sort: "-createdAt",
344
+ page,
345
+ });
346
+ return useQuery({
347
+ queryKey: users.keys.list(body), // barqaror cache kaliti
348
+ queryFn: async () => users.parseList((await axios.post("/users/list", body)).data),
349
+ });
350
+ // data = { data: IUser[], meta: { totalPages, currentPage, hasNext, … } }
351
+ }
352
+ ```
353
+
354
+ ### Infinite scroll — `useInfiniteQuery`
355
+
356
+ ```tsx
357
+ import { useInfiniteQuery } from "@tanstack/react-query";
358
+ const feed = qk.resource<IPost>("feed");
359
+
360
+ function useFeed(filter: IPostFilterInput) {
361
+ return useInfiniteQuery({
362
+ queryKey: feed.keys.infinite({ filter }),
363
+ initialPageParam: 0,
364
+ queryFn: async ({ pageParam }) => feed.parseInfinite((await axios.post("/feed/infinite", feed.infinite({ filter, offset: pageParam, limit: 20 }))).data),
365
+ getNextPageParam: last => (last.meta.hasMore ? last.meta.nextOffset : undefined),
366
+ });
367
+ // const rows = data?.pages.flatMap(p => p.data) ?? [];
368
+ }
369
+ ```
370
+
371
+ ### Cursor (keyset) — `useInfiniteQuery`, ikki tomonlама
372
+
373
+ ```tsx
374
+ const feed = qk.resource<IPost>("feed");
375
+
376
+ function useCursorFeed(filter: IPostFilterInput) {
377
+ return useInfiniteQuery({
378
+ queryKey: feed.keys.cursor({ filter }),
379
+ initialPageParam: null as string | null,
380
+ queryFn: async ({ pageParam, direction }) =>
381
+ feed.parseCursor(
382
+ (
383
+ await axios.post(
384
+ "/feed/cursor",
385
+ feed.cursor({
386
+ filter,
387
+ cursor: pageParam,
388
+ limit: 20,
389
+ order: "asc", // barqaror feed tartibi
390
+ direction: direction === "backward" ? "backward" : "forward",
391
+ }),
392
+ )
393
+ ).data,
394
+ ),
395
+ getNextPageParam: last => (last.meta.hasNext ? last.meta.nextCursor : undefined),
396
+ getPreviousPageParam: first => (first.meta.hasPrev ? first.meta.prevCursor : undefined),
397
+ });
398
+ }
399
+ ```
400
+
401
+ `order` — barqaror feed tartibi (konstant); TanStack'ning forward/backward'i payload'ning `direction`iga o'giriladi, cursor tokenlari (`nextCursor`/`prevCursor`) map qilingan meta'дан keladi.
402
+
403
+ ## Standalone (registry'siz) — free-form
404
+
405
+ Registry shart emas — builder'lar to'g'ridan-to'g'ri ishlaydi:
406
+
407
+ ```ts
408
+ import { buildListParams, buildInfiniteParams, buildCursorParams, f } from "@querykitjs/web";
409
+
410
+ const body = buildListParams({
411
+ filter: f.and(f.contains("name", search), f.eq("status", status)),
412
+ sort: "-createdAt",
413
+ page,
414
+ perPage: 20,
415
+ with: { supervisor: true },
416
+ });
417
+ const res = await axios.post("/users/list", body);
418
+ const { data, meta } = qk.parseList(res.data); // meta uchun registry parse* (entity-agnostik)
419
+ ```
420
+
421
+ `createQuery(config)` — registry'siz default'lar/`pruneEmpty` kerak bo'lsa. Wire maydon nomlari querykit kontrakti bilan qat'iy (`filter`/`sort`/`columns`/`with`/`page`/`perPage`/`limit`/`offset`/`cursor`/`withDeleted`).
162
422
 
163
423
  ```ts
164
424
  import { createQuery } from "@querykitjs/web";
425
+ const q = createQuery({ defaultPerPage: 20, pruneEmpty: true });
426
+ const params = q.list({ filter, page });
427
+ ```
428
+
429
+ ## So'rovni o'zingiz yuborasiz
165
430
 
166
- const q = createQuery({ perPageField: "per_page", withField: "withPopulates" });
167
- const params = q.list({ filter, page, perPage });
431
+ Kutubxona faqat **so'rovgacha** ishlaydi. Transport yo'q:
432
+
433
+ ```ts
434
+ const res = await fetch("/api/users/list", {
435
+ method: "POST",
436
+ headers: { "Content-Type": "application/json" },
437
+ body: JSON.stringify(params),
438
+ });
439
+ const { data, meta } = users.parseList(await res.json());
168
440
  ```
169
441
 
170
442
  ## API ma'lumotnoma
171
443
 
172
- | Funksiya | Vazifasi |
173
- | ---------------------------------------------- | -------------------------------------- |
174
- | `createFilters<T>()` / `f` | tipli filter builder / tipsiz |
175
- | `buildParams(input)` | params normalizatsiya (paginatsiyasiz) |
176
- | `buildListParams(input)` | + `page`/`perPage` (offset) |
177
- | `buildInfiniteParams(input)` | + `limit`/`offset` (infinite) |
178
- | `buildCursorParams(input)` | + `limit`/`cursor`/`order`/`direction` |
179
- | `createQuery(config)` | custom field nomlari bilan builder |
180
- | `defineListSchema(schema)` | URL param → filter tavsifi |
181
- | `schemaToFilter(schema, sp)` | searchParams → `FieldCondition[]` |
182
- | `searchParamsToPayload(schema, sp)` | searchParams → to'liq payload |
183
- | `readListParams(schema, sp)` | searchParams → `ListParams` |
184
- | `setParam/setPage/setSize/setSort/resetParams` | URL yozish (immutable, page-reset) |
185
- | `encodeSort/decodeSort` | `{name,direction}` ↔ `"-createdAt"` |
186
- | `mapMeta(raw)` | offset meta → camel |
187
- | `mapInfiniteMeta(raw)` | infinite meta → camel |
188
- | `mapCursorMeta(raw)` | cursor meta → camel |
189
- | `useListParams(opts)` (`/react`) | URL-sync hook |
444
+ | Funksiya / a'zo | Vazifasi |
445
+ | --------------------------------------------------------------------------------- | ------------------------------------------------------ |
446
+ | `createRegistry(config)` | adapter + default'lar bilan registry |
447
+ | `registry.resource<T>(name)` | entity-tipli builder/filter/schema/keys/parser |
448
+ | `resource.list/infinite/cursor/params(input)` | payload qurish (offset/infinite/cursor/pagination'siz) |
449
+ | `resource.f` / `createFilters<T>()` / `f` | tipli filter builder / tipli / tipsiz |
450
+ | `resource.search(term, fields)` | OR-of-contains preset |
451
+ | `resource.schema(desc)` / `defineListSchema(desc)` | URL param → filter tavsifi |
452
+ | `resource.fromSearchParams(sp, {schema})` | searchParams → list payload (SSR) |
453
+ | `resource.keys` | TanStack/SWR query keys |
454
+ | `resource.parseList/parseInfinite/parseCursor` | javob meta'sini camelCase |
455
+ | `qk.parseList/parseInfinite/parseCursor` | registry-level parse (entity-agnostik) |
456
+ | `buildParams / buildListParams / buildInfiniteParams / buildCursorParams` | standalone builder'lar |
457
+ | `createQuery(config)` | registry'siz builder (default'lar/pruneEmpty) |
458
+ | `schemaToFilter(schema, sp)` | searchParams → `FieldCondition[]` |
459
+ | `searchParamsToPayload(schema, sp)` | searchParams → to'liq payload |
460
+ | `readListParams(schema, sp)` | searchParams → `ListParams` |
461
+ | `setParam/setPage/setSize/setSort/resetParams` | URL yozish (immutable, page-reset) |
462
+ | `encodeSort/decodeSort` | `{name,direction}` ↔ `"-createdAt"` |
463
+ | `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router-dom) |
464
+ | `useListParamsBase(resource, {schema, searchParams, setSearchParams})` (`/react`) | router-agnostik hook |
190
465
 
191
466
  ## Ishlab chiqish
192
467