@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.en.md +377 -104
- package/README.md +375 -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,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
|
|
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
|
-
|
|
14
|
+
// src/lib/query.ts — bir marta sozlang
|
|
15
|
+
import { createRegistry } from "@querykitjs/web";
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
-
|
|
34
|
-
- **
|
|
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
|
|
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
|
-
|
|
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>(); //
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
users.search("ali", ["buyerName", "email"]);
|
|
155
|
+
// → or(contains("buyerName","ali"), contains("email","ali"))
|
|
156
|
+
```
|
|
65
157
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
216
|
+
Runtime o'zgarmaydi (pass-through) — bu sof DX. Funksiya/SQL formalarини JSON'да yubora olmaganingiz uchun ular umuman taklif qilinmaydi.
|
|
102
217
|
|
|
103
|
-
|
|
218
|
+
## Declarative list schema — URL ↔ filter
|
|
104
219
|
|
|
105
|
-
|
|
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
|
-
|
|
222
|
+
```ts
|
|
223
|
+
const dispatchesSchema = dispatches.schema({
|
|
109
224
|
id: { operation: "=" },
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 {
|
|
122
|
-
import {
|
|
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
|
|
125
|
-
|
|
126
|
-
const { params, setParam, setPage, setSort } = useListParams({
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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",
|
|
135
|
-
// setPage(2), setSort("-createdAt")
|
|
272
|
+
// setParam("status","active") → URL yangilanadi, page reset; setPage(2); setSort("-createdAt")
|
|
136
273
|
}
|
|
137
274
|
```
|
|
138
275
|
|
|
139
|
-
|
|
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
|
-
|
|
278
|
+
**Router-agnostik** (Next.js, TanStack Router) — base hook'ga `searchParams` bering (`react-router-dom` talab qilmaydi):
|
|
142
279
|
|
|
143
|
-
|
|
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
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
body: JSON.stringify(
|
|
151
|
-
});
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
|
173
|
-
|
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
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
|
|