@querykitjs/web 3.1.0 → 4.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 +115 -31
- package/README.md +115 -31
- package/dist/index.cjs +1 -360
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -3
- package/dist/index.d.ts +31 -3
- package/dist/index.js +1 -336
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +1 -147
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +11 -4
- package/dist/react/index.d.ts +11 -4
- package/dist/react/index.js +1 -144
- package/dist/react/index.js.map +1 -1
- package/dist/{registry-B1WohYab.d.cts → registry-DOVi_zCU.d.cts} +67 -11
- package/dist/{registry-B1WohYab.d.ts → registry-DOVi_zCU.d.ts} +67 -11
- package/package.json +39 -15
- package/skills/querykit-web/SKILL.md +181 -0
package/README.en.md
CHANGED
|
@@ -48,19 +48,31 @@ function UsersPage() {
|
|
|
48
48
|
- **3 pagination modes** — offset (`list`), infinite (`infinite`), cursor/keyset (`cursor`); each has its own `parse*` to camelCase the meta.
|
|
49
49
|
- **Adapter-aware `with`** — only **wire-safe** (JSON) per-relation options are suggested for `mongoose`/`drizzle-pg`/`drizzle-sqlite`/`prisma-pg`; function/SQL forms are never offered.
|
|
50
50
|
- **Declarative schema** — `resource.schema` / `defineListSchema` maps URL params → filter; kills the per-page hand-building.
|
|
51
|
-
- **React hooks** — `useListParams` (react-router
|
|
51
|
+
- **React hooks** — `useListParams` (react-router inside), `useListParamsBase` (router-agnostic). Framework-agnostic core — Vue can bind the same core.
|
|
52
52
|
- **SSR** — `resource.fromSearchParams(sp, {schema})` builds a payload straight from `URLSearchParams`, no hook.
|
|
53
53
|
- **Query keys** — `resource.keys` gives stable keys for TanStack/SWR.
|
|
54
|
-
- **Zero dependency** — no axios/react-query/dayjs. `react` (+ `react-router
|
|
54
|
+
- **Zero dependency** — no axios/react-query/dayjs. `react` (+ `react-router` for the hook) are optional peers.
|
|
55
55
|
|
|
56
56
|
## Install
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
bun add @querykitjs/web
|
|
60
|
-
# React hook: react (peer). `useListParams` also peers react-router
|
|
61
|
-
# The router-agnostic `useListParamsBase` needs no react-router
|
|
60
|
+
# React hook: react (peer). `useListParams` also peers react-router.
|
|
61
|
+
# The router-agnostic `useListParamsBase` needs no react-router.
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Requirements
|
|
65
|
+
|
|
66
|
+
| | Version | Tested (`bun run test:matrix`) |
|
|
67
|
+
| ---------------------------------------------------- | -------------------- | ------------------------------ |
|
|
68
|
+
| Node.js | `>=22.12` | 24 |
|
|
69
|
+
| `react` (optional peer) | `^18.3 \|\| ^19.2.7` | 18.3.1, 19.3.1 |
|
|
70
|
+
| `react-router` (optional peer, `useListParams` only) | `^7.0 \|\| ^8.0` | 7.18.5, 8.5.1 |
|
|
71
|
+
|
|
72
|
+
Node 20 is no longer supported (`engines`: `>=22.12`). Peer ranges cover the last two majors; both ends are tested.
|
|
73
|
+
|
|
74
|
+
**`react-router-dom` → `react-router`.** `useListParams` now imports from `react-router` (v7 and v8 both export `useSearchParams`). A project that has `react-router-dom` installs `react-router` too — on v7 it is already a dependency of `react-router-dom`: `npm i react-router`.
|
|
75
|
+
|
|
64
76
|
## Two ways to use it
|
|
65
77
|
|
|
66
78
|
```ts
|
|
@@ -84,18 +96,20 @@ export const qk = createRegistry({
|
|
|
84
96
|
sort: ["-createdAt"], // list + infinite ordering (leading "-" = desc)
|
|
85
97
|
cursor: { order: "asc" }, // cursor-only key traversal order
|
|
86
98
|
},
|
|
99
|
+
timestamps: { createdAt: "created_at" }, // same as the backend registry; the default sort is this field, desc
|
|
87
100
|
pruneEmpty: true, // drop empty filter values (""/null/undefined/[]); keep 0/false
|
|
88
101
|
});
|
|
89
102
|
```
|
|
90
103
|
|
|
91
|
-
| Config | Meaning
|
|
92
|
-
| ----------------------- |
|
|
93
|
-
| `adapter` | Target backend — tailors `with` typing (intellisense only).
|
|
94
|
-
| `defaults.perPage` | Default `perPage` for `list`.
|
|
95
|
-
| `defaults.limit` | Default `limit` for `infinite` and `cursor`.
|
|
96
|
-
| `defaults.sort` | Default sort for `list`/`infinite` — `[{key,direction}]` or `["-field"]` shorthand.
|
|
97
|
-
| `defaults.cursor.order` | Default cursor traversal order (`"asc"`/`"desc"`).
|
|
98
|
-
| `
|
|
104
|
+
| Config | Meaning |
|
|
105
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `adapter` | Target backend — tailors `with` typing (intellisense only). |
|
|
107
|
+
| `defaults.perPage` | Default `perPage` for `list`. |
|
|
108
|
+
| `defaults.limit` | Default `limit` for `infinite` and `cursor`. |
|
|
109
|
+
| `defaults.sort` | Default sort for `list`/`infinite` — `[{key,direction}]` or `["-field"]` shorthand. |
|
|
110
|
+
| `defaults.cursor.order` | Default cursor traversal order (`"asc"`/`"desc"`). |
|
|
111
|
+
| `timestamps.createdAt` | Same as the backend `timestamps.createdAt`; without `defaults.sort` the default sort is `[{ key: createdAt, direction: "desc" }]` (default `"createdAt"`). |
|
|
112
|
+
| `pruneEmpty` | Auto-drop empty filter conditions built from form inputs (default `true`). |
|
|
99
113
|
|
|
100
114
|
Every default is overridable per call.
|
|
101
115
|
|
|
@@ -106,19 +120,19 @@ import type { IUser } from "@my/api-types";
|
|
|
106
120
|
export const users = qk.resource<IUser>("users"); // "users" = cache-key namespace (required, unique)
|
|
107
121
|
```
|
|
108
122
|
|
|
109
|
-
| Member | What it is
|
|
110
|
-
| ------------------------------------------------------------- |
|
|
111
|
-
| `users.list(input)` | build an offset (page) payload
|
|
112
|
-
| `users.infinite(input)` | build an infinite (limit/offset) payload
|
|
113
|
-
| `users.cursor(input)` | build a cursor (keyset) payload
|
|
114
|
-
| `users.params(input)` | build a paginationless payload (filter/sort/columns/with)
|
|
115
|
-
| `users.f` | `createFilters<IUser>()` — field-typed builder
|
|
116
|
-
| `users.search(term, fields)`
|
|
117
|
-
| `users.schema(desc)` | `defineListSchema` bound (URL ↔ filter)
|
|
118
|
-
| `users.fromSearchParams(sp, {schema})` | build a list payload straight from `URLSearchParams` (SSR / no hook)
|
|
119
|
-
| `users.keys` | stable query keys for TanStack/SWR
|
|
120
|
-
| `users.parseList / parseInfinite / parseCursor` | map response meta snake→camel
|
|
121
|
-
| `ListResult<T> / InfiniteResult<T> / CursorResult<T>` (types) | `parse*` return types
|
|
123
|
+
| Member | What it is |
|
|
124
|
+
| ------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
125
|
+
| `users.list(input)` | build an offset (page) payload |
|
|
126
|
+
| `users.infinite(input)` | build an infinite (limit/offset) payload |
|
|
127
|
+
| `users.cursor(input)` | build a cursor (keyset) payload |
|
|
128
|
+
| `users.params(input)` | build a paginationless payload (filter/sort/columns/with) |
|
|
129
|
+
| `users.f` | `createFilters<IUser>()` — field-typed builder |
|
|
130
|
+
| `users.search(term, fields, { operation? })` | OR preset across `fields` (`contains` by default, `startsWith`, `endsWith`) |
|
|
131
|
+
| `users.schema(desc)` | `defineListSchema` bound (URL ↔ filter) |
|
|
132
|
+
| `users.fromSearchParams(sp, {schema})` | build a list payload straight from `URLSearchParams` (SSR / no hook) |
|
|
133
|
+
| `users.keys` | stable query keys for TanStack/SWR |
|
|
134
|
+
| `users.parseList / parseInfinite / parseCursor` | map response meta snake→camel |
|
|
135
|
+
| `ListResult<T> / InfiniteResult<T> / CursorResult<T>` (types) | `parse*` return types |
|
|
122
136
|
|
|
123
137
|
All builders inherit the registry's `defaults` + `pruneEmpty`; all types are driven by `IUser` and the `adapter`.
|
|
124
138
|
|
|
@@ -155,8 +169,13 @@ f.contains("age", "x"); // ✗ compile error — `contains` is string-only
|
|
|
155
169
|
```ts
|
|
156
170
|
users.search("ali", ["buyerName", "email"]);
|
|
157
171
|
// → or(contains("buyerName","ali"), contains("email","ali"))
|
|
172
|
+
users.search("ali", ["buyerName"], { operation: "startsWith" });
|
|
173
|
+
// → or(startsWith("buyerName","ali")) — can use an index
|
|
158
174
|
```
|
|
159
175
|
|
|
176
|
+
- `contains` is expensive: `%ali%` cannot use a B-tree index (a full scan on Postgres, a regex on MongoDB). On a large table prefer `startsWith` (`ali%`) or full-text search.
|
|
177
|
+
- Every field must be `filterable` on the server with that operator (`{ buyerName: ["%_%"] }` or `["%_"]`); otherwise the server answers 400 with `forbidden-key` / `forbidden-operator`.
|
|
178
|
+
|
|
160
179
|
An untyped `f` is also exported: `import { f } from "@querykitjs/web"` (no field-type checks, quick use).
|
|
161
180
|
|
|
162
181
|
## Sort — always `[{ key, direction }]` (multi-field)
|
|
@@ -219,6 +238,15 @@ users.parseInfinite(res.data); // { data, meta: { hasMore, nextOffset, … } }
|
|
|
219
238
|
users.parseCursor(res.data); // { data, meta: { nextCursor, hasNext, … } }
|
|
220
239
|
```
|
|
221
240
|
|
|
241
|
+
**`withTotal: false`** — when you do not need the total (an infinite-like list, a "next" button), the server skips the count query:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const body = users.list({ page, perPage: 20, withTotal: false }); // the payload carries withTotal: false (absent when not given)
|
|
245
|
+
const { meta } = users.parseList(res.data); // meta.totalCount === null, meta.totalPages === null; hasNext still works
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`Meta.totalCount` / `Meta.totalPages` are typed `number | null`: `null` means no count ran; a field the server did not send at all is `0` as before. zod (`makeOffsetParamsSchema`) and class-validator (`makeOffsetParamsDto`) accept `withTotal`.
|
|
249
|
+
|
|
222
250
|
`parse*` exists at two levels (one shared implementation — meta is entity-agnostic):
|
|
223
251
|
`qk.parseList/...` (registry, `data: unknown[]`) when you don't have a `resource`; `users.parseList/...` (entity-typed).
|
|
224
252
|
|
|
@@ -276,7 +304,7 @@ Empty/**absent** params are pruned; `default` fills only when the param is **abs
|
|
|
276
304
|
## React hook
|
|
277
305
|
|
|
278
306
|
```tsx
|
|
279
|
-
import { useListParams } from "@querykitjs/web/react"; // uses react-router
|
|
307
|
+
import { useListParams } from "@querykitjs/web/react"; // uses react-router internally
|
|
280
308
|
import { qk } from "@/lib/query";
|
|
281
309
|
|
|
282
310
|
const users = qk.resource<IUser>("users");
|
|
@@ -299,9 +327,11 @@ function UsersListPage() {
|
|
|
299
327
|
}
|
|
300
328
|
```
|
|
301
329
|
|
|
330
|
+
The hook also returns `clamped: boolean` — the URL's `page` or `sort` was past a limit and was cut (see «Limits» below). To fix the URL: `if (clamped) setPage(params.page)`.
|
|
331
|
+
|
|
302
332
|
An input/sort/page change mutates the URL → `params` changes → `queryKey` changes → TanStack refetches (and caches each combo).
|
|
303
333
|
|
|
304
|
-
**Router-agnostic** (Next.js, TanStack Router) — pass `searchParams` to the base hook (no react-router
|
|
334
|
+
**Router-agnostic** (Next.js, TanStack Router) — pass `searchParams` to the base hook (no react-router):
|
|
305
335
|
|
|
306
336
|
```tsx
|
|
307
337
|
import { useListParamsBase } from "@querykitjs/web/react";
|
|
@@ -465,6 +495,59 @@ createRegistry({ adapter: "drizzle-pg", defaults: { maxPerPage: 50, maxLimit: 25
|
|
|
465
495
|
createQuery({ maxPerPage: Infinity });
|
|
466
496
|
```
|
|
467
497
|
|
|
498
|
+
## Limits
|
|
499
|
+
|
|
500
|
+
`perPage`/`limit` are clamped (above), but **page depth** and the **number of sort fields** are not — the builder throws an `Error`, because silently cutting them would change what the request means (another page, another order). The values come from `@querykitjs/core`, the same as in zod and class-validator.
|
|
501
|
+
|
|
502
|
+
| Limit | Default | What counts | Past it |
|
|
503
|
+
| --------------- | -------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
|
|
504
|
+
| `maxOffset` | `10 000` | `list`: `(page - 1) × perPage`; `infinite`: `offset` | `Error: querykit: page 502 × perPage 20 exceeds maxOffset (10000); …` |
|
|
505
|
+
| `maxSortFields` | `3` | `sort` fields after normalization | `Error: querykit: sort with 4 fields exceeds maxSortFields (3)` |
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
buildListParams({ page: 501, perPage: 20 }); // ✅ offset 10 000
|
|
509
|
+
buildListParams({ page: 502, perPage: 20 }); // ❌ Error — use buildCursorParams for deeper pages
|
|
510
|
+
|
|
511
|
+
createQuery({ maxOffset: 50_000, maxSortFields: 4 });
|
|
512
|
+
createRegistry({ adapter: "drizzle-pg", defaults: { maxOffset: 50_000 } });
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
- Filter limits (depth, conditions, `in` items, string length) are **not** checked in web — the server (zod / class-validator) rejects them with a 400.
|
|
516
|
+
- **The URL readers clamp.** The URL is in the user's hands, so `readListParams`, `searchParamsToPayload`, `resource.fromSearchParams` and the hooks (`useListParams`, `useListParamsBase`) never throw: `page` becomes the last allowed page (`floor(maxOffset / perPage) + 1`), `sort` keeps its first `maxSortFields` fields, and the result has `clamped: true`. `?page=9999&size=20` → `page: 501`, `clamped: true`; `?sortType=a,b,c,d` → 3 fields, `clamped: true`. Through a registry, the registry's `defaults` limits apply.
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
const read = readListParams(schema, new URLSearchParams("page=9999&size=20")); // { page: 501, perPage: 20, clamped: true, … }
|
|
520
|
+
const body = users.fromSearchParams(sp, { schema }); // body.clamped === true
|
|
521
|
+
JSON.stringify(body); // no clamped — it is non-enumerable on the payload and never reaches the server
|
|
522
|
+
const { params, clamped } = useListParams(users, { schema }); // no error during render
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Builders the developer calls directly (`buildListParams`, `resource.list`, …) still throw an `Error`.
|
|
526
|
+
|
|
527
|
+
## Migrating (limits)
|
|
528
|
+
|
|
529
|
+
- `buildListParams` / `buildInfiniteParams` / `createQuery` / registry `list`/`infinite` now throw for an offset deeper than 10 000 or more than 3 sort fields (they used to build the payload and the server accepted it). Use a cursor for deep pages; raise `maxOffset` / `maxSortFields` where you must (and raise the same limit on the server).
|
|
530
|
+
|
|
531
|
+
- `Meta.totalCount` / `Meta.totalPages` are now `number | null` (`null` in a `withTotal: false` response). Code like `meta.totalCount + 1` needs a `null` check in TS; in an app that never sends `withTotal` the value is a number as before.
|
|
532
|
+
- A deep `page` read from the URL is now clamped (`clamped: true`), not an error — no error boundary or manual check needed.
|
|
533
|
+
|
|
534
|
+
## Errors — `isQueryKitError`
|
|
535
|
+
|
|
536
|
+
When the server rejects a request (`forbidden-key`, `forbidden-operator`, `limit-exceeded`, `empty-filter`, …), the backend may return `QueryKitError.info` in its 400 response. The envelope is the project's own; web only types the `info` shape:
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
import { isQueryKitError, type QueryKitErrorBody } from "@querykitjs/web";
|
|
540
|
+
|
|
541
|
+
// backend: catch (e) { if (e instanceof QueryKitError) return res.status(400).json({ error: e.info }); }
|
|
542
|
+
const body = error.response?.data?.error;
|
|
543
|
+
if (isQueryKitError(body)) {
|
|
544
|
+
// body: { reason, site, key, source?, operation? }
|
|
545
|
+
if (body.reason === "forbidden-key" && body.site === "filter") showFieldError(body.key);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
`isQueryKitError` checks `reason` and `site` against the core lists and `key` is a string; any other object (e.g. `{ statusCode: 400, message }`) → `false`.
|
|
550
|
+
|
|
468
551
|
## You send the request yourself
|
|
469
552
|
|
|
470
553
|
The library works only **up to the request**. No transport:
|
|
@@ -486,7 +569,7 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
486
569
|
| `registry.resource<T>(name)` | entity-typed builders/filters/schema/keys/parsers |
|
|
487
570
|
| `resource.list/infinite/cursor/params(input)` | build a payload (offset/infinite/cursor/paginationless) |
|
|
488
571
|
| `resource.f` / `createFilters<T>()` / `f` | typed filter builder / typed / untyped |
|
|
489
|
-
| `resource.search(term, fields)`
|
|
572
|
+
| `resource.search(term, fields, { operation? })` | OR preset (`contains` / `startsWith` / `endsWith`) |
|
|
490
573
|
| `resource.schema(desc)` / `defineListSchema(desc)` | URL param → filter descriptor |
|
|
491
574
|
| `resource.fromSearchParams(sp, {schema})` | searchParams → list payload (SSR) |
|
|
492
575
|
| `resource.keys` | TanStack/SWR query keys |
|
|
@@ -495,11 +578,12 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
495
578
|
| `buildParams / buildListParams / buildInfiniteParams / buildCursorParams` | standalone builders |
|
|
496
579
|
| `createQuery(config)` | builder without a registry (defaults/pruneEmpty) |
|
|
497
580
|
| `schemaToFilter(schema, sp)` | searchParams → `FieldCondition[]` |
|
|
498
|
-
| `searchParamsToPayload(schema, sp)` | searchParams → full payload
|
|
499
|
-
| `readListParams(schema, sp)`
|
|
581
|
+
| `searchParamsToPayload(schema, sp)` | searchParams → full payload (clamped, `clamped`) |
|
|
582
|
+
| `readListParams(schema, sp, url?, limits?)` | searchParams → `ListParams` + `clamped` |
|
|
583
|
+
| `isQueryKitError(body)` / `QueryKitErrorBody` | recognise a querykit error in a 400 response |
|
|
500
584
|
| `setParam/setPage/setSize/setSort/resetParams` | write URL (immutable, page-reset) |
|
|
501
585
|
| `encodeSort/decodeSort` | `[{key,direction}]` ↔ `"-createdAt,id"` (multi-field) |
|
|
502
|
-
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router
|
|
586
|
+
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router) |
|
|
503
587
|
| `useListParamsBase(resource, {schema, searchParams, setSearchParams})` (`/react`) | router-agnostic hook |
|
|
504
588
|
|
|
505
589
|
## Development
|
package/README.md
CHANGED
|
@@ -48,19 +48,31 @@ function UsersPage() {
|
|
|
48
48
|
- **3 pagination** — offset (`list`), infinite (`infinite`), cursor/keyset (`cursor`); har birining meta'si alohida `parse*` bilan camelCase'ga o'giriladi.
|
|
49
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
50
|
- **Declarative schema** — `resource.schema` / `defineListSchema` bilan URL param → filter; har sahifadagi qo'lda qurishni yo'q qiladi.
|
|
51
|
-
- **React hook** — `useListParams` (`react-router
|
|
51
|
+
- **React hook** — `useListParams` (`react-router` ichida), `useListParamsBase` (router-agnostik). Core framework'siz — Vue ham shu core'ga bog'lanadi.
|
|
52
52
|
- **SSR** — `resource.fromSearchParams(sp, {schema})` hook'siz, `URLSearchParams`'dan to'g'ridan-to'g'ri payload.
|
|
53
53
|
- **Query keys** — `resource.keys` TanStack/SWR uchun barqaror kalitlar.
|
|
54
|
-
- **Zero dependency** — axios/react-query/dayjs YO'Q. `react` (+ hook uchun `react-router
|
|
54
|
+
- **Zero dependency** — axios/react-query/dayjs YO'Q. `react` (+ hook uchun `react-router`) ixtiyoriy peer.
|
|
55
55
|
|
|
56
56
|
## O'rnatish
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
bun add @querykitjs/web
|
|
60
|
-
# React hook uchun: react (peer). `useListParams` uchun qo'shimcha: react-router
|
|
61
|
-
# Router-agnostik `useListParamsBase` react-router
|
|
60
|
+
# React hook uchun: react (peer). `useListParams` uchun qo'shimcha: react-router (peer).
|
|
61
|
+
# Router-agnostik `useListParamsBase` react-router talab qilmaydi.
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Talablar
|
|
65
|
+
|
|
66
|
+
| | Versiya | Tekshiriladi (`bun run test:matrix`) |
|
|
67
|
+
| ------------------------------------------------------ | -------------------- | ------------------------------------ |
|
|
68
|
+
| Node.js | `>=22.12` | 24 |
|
|
69
|
+
| `react` (ixtiyoriy peer) | `^18.3 \|\| ^19.2.7` | 18.3.1, 19.3.1 |
|
|
70
|
+
| `react-router` (ixtiyoriy peer, faqat `useListParams`) | `^7.0 \|\| ^8.0` | 7.18.5, 8.5.1 |
|
|
71
|
+
|
|
72
|
+
Node 20 endi qo'llab-quvvatlanmaydi (`engines`: `>=22.12`). Peer diapazoni — oxirgi ikki major; ikkala cheti testda.
|
|
73
|
+
|
|
74
|
+
**`react-router-dom` → `react-router`.** `useListParams` endi `react-router`dan import qiladi (v7 va v8 ikkalasi ham `useSearchParams` eksport qiladi). `react-router-dom` o'rnatilgan loyiha `react-router` ni ham o'rnatadi — v7 da u allaqachon `react-router-dom`ning bog'liqligi: `npm i react-router`.
|
|
75
|
+
|
|
64
76
|
## Ikki xil ishlatish
|
|
65
77
|
|
|
66
78
|
```ts
|
|
@@ -84,18 +96,20 @@ export const qk = createRegistry({
|
|
|
84
96
|
sort: ["-createdAt"], // list + infinite tartibi (boshidagi "-" = desc)
|
|
85
97
|
cursor: { order: "asc" }, // faqat cursor: kalit bo'ylab yurish tartibi
|
|
86
98
|
},
|
|
99
|
+
timestamps: { createdAt: "created_at" }, // backend registry'dagi bilan bir xil; default sort shu maydon bo'yicha desc
|
|
87
100
|
pruneEmpty: true, // bo'sh filterlarni tashlash (""/null/undefined/[]); 0/false qoladi
|
|
88
101
|
});
|
|
89
102
|
```
|
|
90
103
|
|
|
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 — `[{key,direction}]` yoki `["-field"]` shorthand.
|
|
97
|
-
| `defaults.cursor.order` | Default cursor tartibi (`"asc"`/`"desc"`).
|
|
98
|
-
| `
|
|
104
|
+
| Config | Vazifasi |
|
|
105
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `adapter` | Backend turi — `with` tipini moslaydi (faqat intellisense). |
|
|
107
|
+
| `defaults.perPage` | `list` uchun default `perPage`. |
|
|
108
|
+
| `defaults.limit` | `infinite` va `cursor` uchun default `limit`. |
|
|
109
|
+
| `defaults.sort` | `list`/`infinite` default sort — `[{key,direction}]` yoki `["-field"]` shorthand. |
|
|
110
|
+
| `defaults.cursor.order` | Default cursor tartibi (`"asc"`/`"desc"`). |
|
|
111
|
+
| `timestamps.createdAt` | Backend `timestamps.createdAt` bilan bir xil; `defaults.sort` yo'q bo'lsa default sort `[{ key: createdAt, direction: "desc" }]` (default `"createdAt"`). |
|
|
112
|
+
| `pruneEmpty` | Form input'lardan qurilgan bo'sh filterlarni avtomatik tashlash (default `true`). |
|
|
99
113
|
|
|
100
114
|
Har bir default har chaqiriqда override qilinadi.
|
|
101
115
|
|
|
@@ -106,19 +120,19 @@ import type { IUser } from "@my/api-types";
|
|
|
106
120
|
export const users = qk.resource<IUser>("users"); // "users" = cache-key namespace (majburiy, unikal)
|
|
107
121
|
```
|
|
108
122
|
|
|
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)`
|
|
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
|
|
123
|
+
| A'zo | Nima |
|
|
124
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
125
|
+
| `users.list(input)` | offset (page) payload quradi |
|
|
126
|
+
| `users.infinite(input)` | infinite (limit/offset) payload quradi |
|
|
127
|
+
| `users.cursor(input)` | cursor (keyset) payload quradi |
|
|
128
|
+
| `users.params(input)` | pagination'siz payload (filter/sort/columns/with) |
|
|
129
|
+
| `users.f` | `createFilters<IUser>()` — maydon-tipli builder |
|
|
130
|
+
| `users.search(term, fields, { operation? })` | ko'p maydon bo'ylab OR preset (`contains` default, `startsWith`, `endsWith`) |
|
|
131
|
+
| `users.schema(desc)` | `defineListSchema` bog'langan (URL ↔ filter) |
|
|
132
|
+
| `users.fromSearchParams(sp, {schema})` | `URLSearchParams`'dan to'g'ridan-to'g'ri list payload (SSR / hook'siz) |
|
|
133
|
+
| `users.keys` | TanStack/SWR uchun barqaror query keys |
|
|
134
|
+
| `users.parseList / parseInfinite / parseCursor` | javob meta'sini snake→camel |
|
|
135
|
+
| `ListResult<T> / InfiniteResult<T> / CursorResult<T>` (tiplar) | `parse*` qaytaradigan tiplar |
|
|
122
136
|
|
|
123
137
|
Barcha builder'lar registry'ning `defaults` + `pruneEmpty`ini meros oladi; tiplar `IUser` va `adapter`dan kelib chiqadi.
|
|
124
138
|
|
|
@@ -155,8 +169,13 @@ f.contains("age", "x"); // ✗ compile-error — `contains` string-only
|
|
|
155
169
|
```ts
|
|
156
170
|
users.search("ali", ["buyerName", "email"]);
|
|
157
171
|
// → or(contains("buyerName","ali"), contains("email","ali"))
|
|
172
|
+
users.search("ali", ["buyerName"], { operation: "startsWith" });
|
|
173
|
+
// → or(startsWith("buyerName","ali")) — indeks ishlatishi mumkin
|
|
158
174
|
```
|
|
159
175
|
|
|
176
|
+
- `contains` qimmat: `%ali%` B-tree indeksini ishlata olmaydi (Postgres'da to'liq skan, MongoDB'da regex). Katta jadvalda `startsWith` (`ali%`) yoki full-text qidiruvni tanlang.
|
|
177
|
+
- Har maydon serverda `filterable` bo'lishi kerak, o'sha operator bilan (`{ buyerName: ["%_%"] }` yoki `["%_"]`); aks holda server `forbidden-key` / `forbidden-operator` bilan 400 qaytaradi.
|
|
178
|
+
|
|
160
179
|
Tipsiz `f` ham bor: `import { f } from "@querykitjs/web"` (maydon-tip tekshiruvisiz, tez ishlatish).
|
|
161
180
|
|
|
162
181
|
## Sort — hamisha `[{ key, direction }]` (ko'p-maydon)
|
|
@@ -219,6 +238,15 @@ users.parseInfinite(res.data); // { data, meta: { hasMore, nextOffset, … } }
|
|
|
219
238
|
users.parseCursor(res.data); // { data, meta: { nextCursor, hasNext, … } }
|
|
220
239
|
```
|
|
221
240
|
|
|
241
|
+
**`withTotal: false`** — umumiy son kerak bo'lmasa (infinite-ga o'xshash ro'yxat, "keyingi" tugmasi), server count so'rovini ishlatmaydi:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const body = users.list({ page, perPage: 20, withTotal: false }); // payload'da withTotal: false (berilmasa — kalit yo'q)
|
|
245
|
+
const { meta } = users.parseList(res.data); // meta.totalCount === null, meta.totalPages === null; hasNext ishlaydi
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`Meta.totalCount` / `Meta.totalPages` tipi `number | null`: `null` — count ishlamagan; server umuman bermagan maydon avvalgidek `0`. zod (`makeOffsetParamsSchema`) va class-validator (`makeOffsetParamsDto`) `withTotal`ni qabul qiladi.
|
|
249
|
+
|
|
222
250
|
`parse*` ikki darajaда bor (bitta umumiy implementatsiya — meta entity-agnostik):
|
|
223
251
|
`qk.parseList/...` (registry, `data: unknown[]`) — `resource` bo'lmasa; `users.parseList/...` (entity'ga tipli).
|
|
224
252
|
|
|
@@ -276,7 +304,7 @@ Bo'sh/**yo'q** paramlar tashlanadi; `default` faqat param **yo'q** bo'lganда t
|
|
|
276
304
|
## React hook
|
|
277
305
|
|
|
278
306
|
```tsx
|
|
279
|
-
import { useListParams } from "@querykitjs/web/react"; // react-router
|
|
307
|
+
import { useListParams } from "@querykitjs/web/react"; // react-router ichida ishlatiladi
|
|
280
308
|
import { qk } from "@/lib/query";
|
|
281
309
|
|
|
282
310
|
const users = qk.resource<IUser>("users");
|
|
@@ -301,7 +329,9 @@ function UsersListPage() {
|
|
|
301
329
|
|
|
302
330
|
Input/sort/page o'zgarishi URL'ni o'zgartiradi → `params` o'zgaradi → `queryKey` o'zgaradi → TanStack qayta so'raydi (va har kombinatsiyani cache qiladi).
|
|
303
331
|
|
|
304
|
-
|
|
332
|
+
Hook `clamped: boolean` ham qaytaradi — URL'dagi `page` yoki `sort` limitdan oshgani uchun kesilgan (pastda «Limits»). URL'ni tuzatish uchun: `if (clamped) setPage(params.page)`.
|
|
333
|
+
|
|
334
|
+
**Router-agnostik** (Next.js, TanStack Router) — base hook'ga `searchParams` bering (`react-router` talab qilmaydi):
|
|
305
335
|
|
|
306
336
|
```tsx
|
|
307
337
|
import { useListParamsBase } from "@querykitjs/web/react";
|
|
@@ -465,6 +495,59 @@ createRegistry({ adapter: "drizzle-pg", defaults: { maxPerPage: 50, maxLimit: 25
|
|
|
465
495
|
createQuery({ maxPerPage: Infinity });
|
|
466
496
|
```
|
|
467
497
|
|
|
498
|
+
## Limits
|
|
499
|
+
|
|
500
|
+
`perPage`/`limit` clamp qilinadi (yuqorida), lekin **sahifa chuqurligi** va **sort maydonlari soni** clamp qilinmaydi — builder `Error` tashlaydi, chunki jim kesish so'rov ma'nosini o'zgartirardi (boshqa sahifa yoki boshqa tartib). Qiymatlar `@querykitjs/core`dan — zod va class-validator bilan bir xil.
|
|
501
|
+
|
|
502
|
+
| Limit | Default | Nima sanaladi | Oshsa |
|
|
503
|
+
| --------------- | -------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
|
|
504
|
+
| `maxOffset` | `10 000` | `list`: `(page - 1) × perPage`; `infinite`: `offset` | `Error: querykit: page 502 × perPage 20 exceeds maxOffset (10000); …` |
|
|
505
|
+
| `maxSortFields` | `3` | normalizatsiyadan keyingi `sort` maydonlari | `Error: querykit: sort with 4 fields exceeds maxSortFields (3)` |
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
buildListParams({ page: 501, perPage: 20 }); // ✅ offset 10 000
|
|
509
|
+
buildListParams({ page: 502, perPage: 20 }); // ❌ Error — chuqurroq sahifalar uchun buildCursorParams
|
|
510
|
+
|
|
511
|
+
createQuery({ maxOffset: 50_000, maxSortFields: 4 });
|
|
512
|
+
createRegistry({ adapter: "drizzle-pg", defaults: { maxOffset: 50_000 } });
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
- Filtr limitlari (chuqurlik, shartlar, `in` elementlari, satr uzunligi) web'da **tekshirilmaydi** — ularni server (zod / class-validator) rad etadi va 400 qaytaradi.
|
|
516
|
+
- **URL o'quvchilar clamp qiladi.** URL foydalanuvchi qo'lida, shuning uchun `readListParams`, `searchParamsToPayload`, `resource.fromSearchParams` va hook'lar (`useListParams`, `useListParamsBase`) xato tashlamaydi: `page` ruxsat etilgan oxirgi sahifaga (`floor(maxOffset / perPage) + 1`), `sort` birinchi `maxSortFields` maydoniga kesiladi va natijada `clamped: true` bo'ladi. `?page=9999&size=20` → `page: 501`, `clamped: true`; `?sortType=a,b,c,d` → 3 maydon, `clamped: true`. Registry orqali — registry `defaults` limitlari bilan.
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
const read = readListParams(schema, new URLSearchParams("page=9999&size=20")); // { page: 501, perPage: 20, clamped: true, … }
|
|
520
|
+
const body = users.fromSearchParams(sp, { schema }); // body.clamped === true
|
|
521
|
+
JSON.stringify(body); // clamped yo'q — u payload'da non-enumerable, server'ga bormaydi
|
|
522
|
+
const { params, clamped } = useListParams(users, { schema }); // render paytida xato yo'q
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Dasturchi to'g'ridan-to'g'ri chaqirgan builder'lar (`buildListParams`, `resource.list`, …) avvalgidek `Error` tashlaydi.
|
|
526
|
+
|
|
527
|
+
## Migrating (limitlar)
|
|
528
|
+
|
|
529
|
+
- `buildListParams` / `buildInfiniteParams` / `createQuery` / registry `list`/`infinite` endi offset 10 000 dan chuqur yoki 3 tadan ko'p sort maydonli so'rovda `Error` tashlaydi (ilgari payload yasalardi va server uni qabul qilardi). Chuqur sahifalar — cursor; zarur bo'lsa `maxOffset` / `maxSortFields` bilan oshiring (serverda ham xuddi shu limitni oshiring).
|
|
530
|
+
|
|
531
|
+
- `Meta.totalCount` / `Meta.totalPages` endi `number | null` (`withTotal: false` javobida `null`). `meta.totalCount + 1` kabi kod TS'da `null` tekshiruvini talab qiladi; `withTotal` yubormaydigan ilovada qiymat avvalgidek raqam.
|
|
532
|
+
- URL'dan o'qilgan chuqur `page` endi xato emas, clamp (`clamped: true`) — error boundary yoki qo'lda tekshiruv kerak emas.
|
|
533
|
+
|
|
534
|
+
## Xatolar — `isQueryKitError`
|
|
535
|
+
|
|
536
|
+
Server so'rovni rad etsa (`forbidden-key`, `forbidden-operator`, `limit-exceeded`, `empty-filter`, …), backend `QueryKitError.info`ni 400 javobida qaytarishi mumkin. Javob konverti loyihaniki; web faqat `info` shaklini tiplaydi:
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
import { isQueryKitError, type QueryKitErrorBody } from "@querykitjs/web";
|
|
540
|
+
|
|
541
|
+
// backend: catch (e) { if (e instanceof QueryKitError) return res.status(400).json({ error: e.info }); }
|
|
542
|
+
const body = error.response?.data?.error;
|
|
543
|
+
if (isQueryKitError(body)) {
|
|
544
|
+
// body: { reason, site, key, source?, operation? }
|
|
545
|
+
if (body.reason === "forbidden-key" && body.site === "filter") showFieldError(body.key);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
`isQueryKitError` `reason` va `site` ni core ro'yxatidan tekshiradi, `key` — satr; boshqa har obyekt (masalan `{ statusCode: 400, message }`) → `false`.
|
|
550
|
+
|
|
468
551
|
## So'rovni o'zingiz yuborasiz
|
|
469
552
|
|
|
470
553
|
Kutubxona faqat **so'rovgacha** ishlaydi. Transport yo'q:
|
|
@@ -486,7 +569,7 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
486
569
|
| `registry.resource<T>(name)` | entity-tipli builder/filter/schema/keys/parser |
|
|
487
570
|
| `resource.list/infinite/cursor/params(input)` | payload qurish (offset/infinite/cursor/pagination'siz) |
|
|
488
571
|
| `resource.f` / `createFilters<T>()` / `f` | tipli filter builder / tipli / tipsiz |
|
|
489
|
-
| `resource.search(term, fields)`
|
|
572
|
+
| `resource.search(term, fields, { operation? })` | OR preset (`contains` / `startsWith` / `endsWith`) |
|
|
490
573
|
| `resource.schema(desc)` / `defineListSchema(desc)` | URL param → filter tavsifi |
|
|
491
574
|
| `resource.fromSearchParams(sp, {schema})` | searchParams → list payload (SSR) |
|
|
492
575
|
| `resource.keys` | TanStack/SWR query keys |
|
|
@@ -495,11 +578,12 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
495
578
|
| `buildParams / buildListParams / buildInfiniteParams / buildCursorParams` | standalone builder'lar |
|
|
496
579
|
| `createQuery(config)` | registry'siz builder (default'lar/pruneEmpty) |
|
|
497
580
|
| `schemaToFilter(schema, sp)` | searchParams → `FieldCondition[]` |
|
|
498
|
-
| `searchParamsToPayload(schema, sp)` | searchParams → to'liq payload
|
|
499
|
-
| `readListParams(schema, sp)`
|
|
581
|
+
| `searchParamsToPayload(schema, sp)` | searchParams → to'liq payload (clamp, `clamped`) |
|
|
582
|
+
| `readListParams(schema, sp, url?, limits?)` | searchParams → `ListParams` + `clamped` |
|
|
583
|
+
| `isQueryKitError(body)` / `QueryKitErrorBody` | 400 javobidagi querykit xatosini tanish |
|
|
500
584
|
| `setParam/setPage/setSize/setSort/resetParams` | URL yozish (immutable, page-reset) |
|
|
501
585
|
| `encodeSort/decodeSort` | `[{key,direction}]` ↔ `"-createdAt,id"` (multi-field) |
|
|
502
|
-
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router
|
|
586
|
+
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router) |
|
|
503
587
|
| `useListParamsBase(resource, {schema, searchParams, setSearchParams})` (`/react`) | router-agnostik hook |
|
|
504
588
|
|
|
505
589
|
## Ishlab chiqish
|