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