@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.en.md +379 -104
- package/README.md +377 -102
- package/dist/index.cjs +80 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -70
- package/dist/index.d.ts +3 -70
- package/dist/index.js +81 -17
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +51 -105
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +40 -19
- package/dist/react/index.d.ts +40 -19
- package/dist/react/index.js +51 -106
- package/dist/react/index.js.map +1 -1
- package/dist/registry-DPIEOvLW.d.cts +417 -0
- package/dist/registry-DPIEOvLW.d.ts +417 -0
- package/package.json +9 -4
- package/dist/url-BpslCTRx.d.cts +0 -180
- package/dist/url-BpslCTRx.d.ts +0 -180
package/README.md
CHANGED
|
@@ -6,47 +6,131 @@
|
|
|
6
6
|
|
|
7
7
|
# @querykitjs/web
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/@querykitjs/web) [](./LICENSE)
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
16
|
+
// src/lib/query.ts — bir marta sozlang
|
|
17
|
+
import { createRegistry } from "@querykitjs/web";
|
|
15
18
|
|
|
16
|
-
const
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
-
|
|
34
|
-
- **
|
|
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
|
|
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
|
-
##
|
|
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>(); //
|
|
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
|
-
**
|
|
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
|
-
|
|
153
|
+
**`search` preset** — bitta input, ko'p ustun (OR of contains):
|
|
62
154
|
|
|
63
155
|
```ts
|
|
64
|
-
|
|
156
|
+
users.search("ali", ["buyerName", "email"]);
|
|
157
|
+
// → or(contains("buyerName","ali"), contains("email","ali"))
|
|
158
|
+
```
|
|
65
159
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
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
|
|
182
|
+
## Paginatsiya + `parse*` (meta)
|
|
79
183
|
|
|
80
|
-
Backend'ning 3 rejimiga mos 3 builder. Har birining javob meta'si **har xil
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
218
|
+
Runtime o'zgarmaydi (pass-through) — bu sof DX. Funksiya/SQL formalarини JSON'да yubora olmaganingiz uchun ular umuman taklif qilinmaydi.
|
|
102
219
|
|
|
103
|
-
|
|
220
|
+
## Declarative list schema — URL ↔ filter
|
|
104
221
|
|
|
105
|
-
|
|
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
|
-
|
|
224
|
+
```ts
|
|
225
|
+
const dispatchesSchema = dispatches.schema({
|
|
109
226
|
id: { operation: "=" },
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 {
|
|
122
|
-
import {
|
|
255
|
+
import { useListParams } from "@querykitjs/web/react"; // react-router-dom ichida ishlatiladi
|
|
256
|
+
import { qk } from "@/lib/query";
|
|
123
257
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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",
|
|
135
|
-
// setPage(2), setSort("-createdAt")
|
|
274
|
+
// setParam("status","active") → URL yangilanadi, page reset; setPage(2); setSort("-createdAt")
|
|
136
275
|
}
|
|
137
276
|
```
|
|
138
277
|
|
|
139
|
-
|
|
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
|
-
|
|
280
|
+
**Router-agnostik** (Next.js, TanStack Router) — base hook'ga `searchParams` bering (`react-router-dom` talab qilmaydi):
|
|
142
281
|
|
|
143
|
-
|
|
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
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
body: JSON.stringify(
|
|
151
|
-
});
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
##
|
|
313
|
+
## To'liq misollar
|
|
160
314
|
|
|
161
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
|
173
|
-
|
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
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
|
|