@querykitjs/web 3.1.0 → 4.1.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 +208 -38
- package/README.md +209 -39
- package/dist/angular/index.cjs +2 -0
- package/dist/angular/index.cjs.map +1 -0
- package/dist/angular/index.d.cts +70 -0
- package/dist/angular/index.d.ts +70 -0
- package/dist/angular/index.js +2 -0
- package/dist/angular/index.js.map +1 -0
- package/dist/index.cjs +1 -360
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -3
- package/dist/index.d.ts +75 -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-CEq4xFui.d.cts} +111 -53
- package/dist/{registry-B1WohYab.d.ts → registry-CEq4xFui.d.ts} +111 -53
- package/dist/vue/index.cjs +2 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +64 -0
- package/dist/vue/index.d.ts +64 -0
- package/dist/vue/index.js +2 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +100 -15
- package/skills/querykit-web/SKILL.md +241 -0
package/README.md
CHANGED
|
@@ -46,21 +46,40 @@ function UsersPage() {
|
|
|
46
46
|
- **Registry** — `adapter` + default'larni bir joyda sozlang, `resource<T>()` entity'ga tipli, adapter-aware builder/filter/schema/keys/parser beradi.
|
|
47
47
|
- **Ikkala filter uslubi** — `f.eq()...` builder (maydon tipiga qarab operator) VA object/massiv (`{key, operation, value}`). Legacy `IFilter[]` bilan to'liq mos.
|
|
48
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'
|
|
49
|
+
- **Adapter-aware `with`** — `mongoose`/`drizzle-pg`/`drizzle-sqlite`/`prisma-pg` uchun faqat **wire-safe** (JSON) relation opsiyalari intellisense'da; funksiya/SQL formalari ko'rsatilmaydi. `mongodb` da `with` yo'q — relation'larni ikkinchi so'rov bilan yuklang.
|
|
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, Vue composable va Angular signal** — `useListParams` (`react-router` / `vue-router` ichida) va `useListParamsBase` (router-agnostik) `/react` va `/vue` da; `injectListParams` va `injectListParamsBase` `/angular` da. `createListState` — shu mantiq, framework'siz.
|
|
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`) `vue` (+ `vue-router`) va `@angular/core` (+ `@angular/router`, `rxjs`) 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
|
+
# Vue composable uchun: vue (peer). `useListParams` uchun qo'shimcha: vue-router (peer).
|
|
63
|
+
# Angular uchun: @angular/core va rxjs (peer). `injectListParams` uchun qo'shimcha: @angular/router (peer).
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
### Talablar
|
|
67
|
+
|
|
68
|
+
| | Versiya | Tekshiriladi (`bun run test:matrix`) |
|
|
69
|
+
| ------------------------------------------------------------ | -------------------- | ------------------------------------ |
|
|
70
|
+
| Node.js | `>=22.12` | 24 |
|
|
71
|
+
| `react` (ixtiyoriy peer) | `^18.3 \|\| ^19.2.7` | 18.3.1, 19.3.1 |
|
|
72
|
+
| `react-router` (ixtiyoriy peer, faqat `useListParams`) | `^7.0 \|\| ^8.0` | 7.18.5, 8.5.1 |
|
|
73
|
+
| `vue` (ixtiyoriy peer) | `^3.5` | 3.5.0, 3.5.44 |
|
|
74
|
+
| `vue-router` (ixtiyoriy peer, faqat `useListParams`) | `^4.6 \|\| ^5` | 4.6.4, 5.4.0 |
|
|
75
|
+
| `@angular/core` (ixtiyoriy peer) | `^21 \|\| ^22` | 21.2.26, 22.2.4 |
|
|
76
|
+
| `@angular/router` (ixtiyoriy peer, faqat `injectListParams`) | `^21 \|\| ^22` | 21.2.26, 22.2.4 |
|
|
77
|
+
| `rxjs` (ixtiyoriy peer, Angular'ning o'ziniki) | `^7.8` | 7.8.2 |
|
|
78
|
+
|
|
79
|
+
Node 20 endi qo'llab-quvvatlanmaydi (`engines`: `>=22.12`). Peer diapazoni — oxirgi ikki major; ikkala cheti testda. Vue'ning bitta qo'llab-quvvatlanadigan major'i bor: diapazon 3.5 dan — vue-router 4.6 qabul qiladigan eng eski minor.
|
|
80
|
+
|
|
81
|
+
**`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`.
|
|
82
|
+
|
|
64
83
|
## Ikki xil ishlatish
|
|
65
84
|
|
|
66
85
|
```ts
|
|
@@ -77,25 +96,27 @@ import { buildListParams, f } from "@querykitjs/web";
|
|
|
77
96
|
|
|
78
97
|
```ts
|
|
79
98
|
export const qk = createRegistry({
|
|
80
|
-
adapter: "mongoose", // "mongoose" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg"
|
|
99
|
+
adapter: "mongoose", // "mongoose" | "mongodb" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg"
|
|
81
100
|
defaults: {
|
|
82
101
|
perPage: 20, // list (offset) sahifa hajmi
|
|
83
102
|
limit: 20, // infinite + cursor hajmi
|
|
84
103
|
sort: ["-createdAt"], // list + infinite tartibi (boshidagi "-" = desc)
|
|
85
104
|
cursor: { order: "asc" }, // faqat cursor: kalit bo'ylab yurish tartibi
|
|
86
105
|
},
|
|
106
|
+
timestamps: { createdAt: "created_at" }, // backend registry'dagi bilan bir xil; default sort shu maydon bo'yicha desc
|
|
87
107
|
pruneEmpty: true, // bo'sh filterlarni tashlash (""/null/undefined/[]); 0/false qoladi
|
|
88
108
|
});
|
|
89
109
|
```
|
|
90
110
|
|
|
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
|
-
| `
|
|
111
|
+
| Config | Vazifasi |
|
|
112
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `adapter` | Backend turi — `with` tipini moslaydi (faqat intellisense). |
|
|
114
|
+
| `defaults.perPage` | `list` uchun default `perPage`. |
|
|
115
|
+
| `defaults.limit` | `infinite` va `cursor` uchun default `limit`. |
|
|
116
|
+
| `defaults.sort` | `list`/`infinite` default sort — `[{key,direction}]` yoki `["-field"]` shorthand. |
|
|
117
|
+
| `defaults.cursor.order` | Default cursor tartibi (`"asc"`/`"desc"`). |
|
|
118
|
+
| `timestamps.createdAt` | Backend `timestamps.createdAt` bilan bir xil; `defaults.sort` yo'q bo'lsa default sort `[{ key: createdAt, direction: "desc" }]` (default `"createdAt"`). |
|
|
119
|
+
| `pruneEmpty` | Form input'lardan qurilgan bo'sh filterlarni avtomatik tashlash (default `true`). |
|
|
99
120
|
|
|
100
121
|
Har bir default har chaqiriqда override qilinadi.
|
|
101
122
|
|
|
@@ -106,19 +127,19 @@ import type { IUser } from "@my/api-types";
|
|
|
106
127
|
export const users = qk.resource<IUser>("users"); // "users" = cache-key namespace (majburiy, unikal)
|
|
107
128
|
```
|
|
108
129
|
|
|
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
|
|
130
|
+
| A'zo | Nima |
|
|
131
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
132
|
+
| `users.list(input)` | offset (page) payload quradi |
|
|
133
|
+
| `users.infinite(input)` | infinite (limit/offset) payload quradi |
|
|
134
|
+
| `users.cursor(input)` | cursor (keyset) payload quradi |
|
|
135
|
+
| `users.params(input)` | pagination'siz payload (filter/sort/columns/with) |
|
|
136
|
+
| `users.f` | `createFilters<IUser>()` — maydon-tipli builder |
|
|
137
|
+
| `users.search(term, fields, { operation? })` | ko'p maydon bo'ylab OR preset (`contains` default, `startsWith`, `endsWith`) |
|
|
138
|
+
| `users.schema(desc)` | `defineListSchema` bog'langan (URL ↔ filter) |
|
|
139
|
+
| `users.fromSearchParams(sp, {schema})` | `URLSearchParams`'dan to'g'ridan-to'g'ri list payload (SSR / hook'siz) |
|
|
140
|
+
| `users.keys` | TanStack/SWR uchun barqaror query keys |
|
|
141
|
+
| `users.parseList / parseInfinite / parseCursor` | javob meta'sini snake→camel |
|
|
142
|
+
| `ListResult<T> / InfiniteResult<T> / CursorResult<T>` (tiplar) | `parse*` qaytaradigan tiplar |
|
|
122
143
|
|
|
123
144
|
Barcha builder'lar registry'ning `defaults` + `pruneEmpty`ini meros oladi; tiplar `IUser` va `adapter`dan kelib chiqadi.
|
|
124
145
|
|
|
@@ -155,8 +176,13 @@ f.contains("age", "x"); // ✗ compile-error — `contains` string-only
|
|
|
155
176
|
```ts
|
|
156
177
|
users.search("ali", ["buyerName", "email"]);
|
|
157
178
|
// → or(contains("buyerName","ali"), contains("email","ali"))
|
|
179
|
+
users.search("ali", ["buyerName"], { operation: "startsWith" });
|
|
180
|
+
// → or(startsWith("buyerName","ali")) — indeks ishlatishi mumkin
|
|
158
181
|
```
|
|
159
182
|
|
|
183
|
+
- `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.
|
|
184
|
+
- Har maydon serverda `filterable` bo'lishi kerak, o'sha operator bilan (`{ buyerName: ["%_%"] }` yoki `["%_"]`); aks holda server `forbidden-key` / `forbidden-operator` bilan 400 qaytaradi.
|
|
185
|
+
|
|
160
186
|
Tipsiz `f` ham bor: `import { f } from "@querykitjs/web"` (maydon-tip tekshiruvisiz, tez ishlatish).
|
|
161
187
|
|
|
162
188
|
## Sort — hamisha `[{ key, direction }]` (ko'p-maydon)
|
|
@@ -219,6 +245,15 @@ users.parseInfinite(res.data); // { data, meta: { hasMore, nextOffset, … } }
|
|
|
219
245
|
users.parseCursor(res.data); // { data, meta: { nextCursor, hasNext, … } }
|
|
220
246
|
```
|
|
221
247
|
|
|
248
|
+
**`withTotal: false`** — umumiy son kerak bo'lmasa (infinite-ga o'xshash ro'yxat, "keyingi" tugmasi), server count so'rovini ishlatmaydi:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const body = users.list({ page, perPage: 20, withTotal: false }); // payload'da withTotal: false (berilmasa — kalit yo'q)
|
|
252
|
+
const { meta } = users.parseList(res.data); // meta.totalCount === null, meta.totalPages === null; hasNext ishlaydi
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`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.
|
|
256
|
+
|
|
222
257
|
`parse*` ikki darajaда bor (bitta umumiy implementatsiya — meta entity-agnostik):
|
|
223
258
|
`qk.parseList/...` (registry, `data: unknown[]`) — `resource` bo'lmasa; `users.parseList/...` (entity'ga tipli).
|
|
224
259
|
|
|
@@ -226,11 +261,12 @@ users.parseCursor(res.data); // { data, meta: { nextCursor, hasNext, … } }
|
|
|
226
261
|
|
|
227
262
|
Registry `with`ni adapter bo'yicha tiplaydi — **faqat serializable** opsiyalar:
|
|
228
263
|
|
|
229
|
-
| adapter | `with[rel]` intellisense | Ko'rsatilmaydi
|
|
230
|
-
| ------------------------------- | ------------------------------------------------------------------ |
|
|
231
|
-
| `mongoose` | `boolean \| { select?, populate?, match?, options? }` | —
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
264
|
+
| adapter | `with[rel]` intellisense | Ko'rsatilmaydi |
|
|
265
|
+
| ------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------- |
|
|
266
|
+
| `mongoose` | `boolean \| { select?, populate?, match?, options? }` | — |
|
|
267
|
+
| `mongodb` | `with` yo'q — relation'larni ikkinchi so'rov bilan yuklang | har qanday `with` (TS xato; server `forbidden-key`) |
|
|
268
|
+
| `drizzle-pg` / `drizzle-sqlite` | `boolean \| { columns?, with?, limit?, offset? }` | `where:(f,op)=>…`, `orderBy:(f,op)=>…`, raw `sql` |
|
|
269
|
+
| `prisma-pg` | `boolean \| { select?, include?, where?, orderBy?, take?, skip? }` | funksiya formalar |
|
|
234
270
|
|
|
235
271
|
```ts
|
|
236
272
|
// adapter: "mongoose"
|
|
@@ -239,7 +275,7 @@ users.list({ with: { author: { select: "name email" } } }); // ✓ `select`
|
|
|
239
275
|
users.list({ with: { author: { columns: { name: true } } } }); // ✓ `columns`; `where`-fn yashirin
|
|
240
276
|
```
|
|
241
277
|
|
|
242
|
-
Runtime o'zgarmaydi (pass-through) — bu sof DX. Funksiya/SQL
|
|
278
|
+
Runtime o'zgarmaydi (pass-through) — bu sof DX. Funksiya/SQL formalarini JSON'da yubora olmaganingiz uchun ular umuman taklif qilinmaydi.
|
|
243
279
|
|
|
244
280
|
## Declarative list schema — URL ↔ filter
|
|
245
281
|
|
|
@@ -276,7 +312,7 @@ Bo'sh/**yo'q** paramlar tashlanadi; `default` faqat param **yo'q** bo'lganда t
|
|
|
276
312
|
## React hook
|
|
277
313
|
|
|
278
314
|
```tsx
|
|
279
|
-
import { useListParams } from "@querykitjs/web/react"; // react-router
|
|
315
|
+
import { useListParams } from "@querykitjs/web/react"; // react-router ichida ishlatiladi
|
|
280
316
|
import { qk } from "@/lib/query";
|
|
281
317
|
|
|
282
318
|
const users = qk.resource<IUser>("users");
|
|
@@ -301,13 +337,88 @@ function UsersListPage() {
|
|
|
301
337
|
|
|
302
338
|
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
339
|
|
|
304
|
-
|
|
340
|
+
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)`.
|
|
341
|
+
|
|
342
|
+
**Router-agnostik** (Next.js, TanStack Router) — base hook'ga `searchParams` bering (`react-router` talab qilmaydi):
|
|
305
343
|
|
|
306
344
|
```tsx
|
|
307
345
|
import { useListParamsBase } from "@querykitjs/web/react";
|
|
308
346
|
const { params, setPage } = useListParamsBase(users, { schema: usersSchema, searchParams, setSearchParams });
|
|
309
347
|
```
|
|
310
348
|
|
|
349
|
+
## Vue composable
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { useListParams } from "@querykitjs/web/vue"; // ichida vue-router
|
|
353
|
+
import { useQuery } from "@tanstack/vue-query";
|
|
354
|
+
import { computed } from "vue";
|
|
355
|
+
|
|
356
|
+
const users = qk.resource<IUser>("users");
|
|
357
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
|
|
358
|
+
|
|
359
|
+
// <script setup> ichida
|
|
360
|
+
const { params, clamped, setParam, setPage, setSort, reset } = useListParams(users, { schema: usersSchema });
|
|
361
|
+
|
|
362
|
+
const q = useQuery({
|
|
363
|
+
queryKey: computed(() => users.keys.list(params.value)),
|
|
364
|
+
queryFn: async () => users.parseList((await axios.post("/users/list", params.value)).data),
|
|
365
|
+
});
|
|
366
|
+
// setParam("status", "active") → router.push({ query }) (tarixda yangi yozuv), page reset
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
React hook bilan bir xil o'qish, clamp va setterlar — `params`, `clamped` va `searchParams` computed ref.
|
|
370
|
+
Takroriy kalit (`?status=a&status=b`) saqlanadi; `?q` (`=` siz) bo'sh qiymat sifatida o'qiladi. SSR (Nuxt) da ishlaydi: `window` ga murojaat yo'q.
|
|
371
|
+
|
|
372
|
+
**Router-agnostik** — base composable'ga `searchParams` (ref, getter yoki qiymat) bering:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { useListParamsBase } from "@querykitjs/web/vue";
|
|
376
|
+
const { params, setPage } = useListParamsBase(users, { schema: usersSchema, searchParams: () => currentSearchParams.value, setSearchParams });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Angular
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { Component, computed } from "@angular/core";
|
|
383
|
+
import { injectListParams } from "@querykitjs/web/angular"; // ichida Angular Router
|
|
384
|
+
|
|
385
|
+
const users = qk.resource<IUser>("users");
|
|
386
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
|
|
387
|
+
|
|
388
|
+
@Component({
|
|
389
|
+
selector: "app-users",
|
|
390
|
+
template: `<button (click)="list.setPage(2)">{{ list.params().page }}</button>`,
|
|
391
|
+
})
|
|
392
|
+
export class UsersPage {
|
|
393
|
+
readonly list = injectListParams(users, { schema: usersSchema });
|
|
394
|
+
readonly queryKey = computed(() => users.keys.list(this.list.params()));
|
|
395
|
+
// list.setParam("status", "active") → router.navigate (tarixda yangi yozuv), page reset
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
React va Vue bilan bir xil o'qish, clamp va setterlar — `params`, `clamped` va `searchParams` signal. Injection context'da chaqiring (field initializer yoki constructor); boshqa joyda Angular `NG0203` xatosini beradi. Oddiy ESM entry: decorator ham, Angular package format ham yo'q, `@angular/build` bilan build bo'ladi.
|
|
400
|
+
|
|
401
|
+
**Router-agnostik** — base funksiyaga o'z `searchParams` signal'ingizni bering:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { injectListParamsBase } from "@querykitjs/web/angular";
|
|
405
|
+
const searchParams = signal(new URLSearchParams());
|
|
406
|
+
const list = injectListParamsBase(users, { schema: usersSchema, searchParams, setSearchParams: next => searchParams.set(next) });
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Istalgan framework — `createListState`
|
|
410
|
+
|
|
411
|
+
Ularning hammasi ortidagi mantiq, framework'siz (Svelte, Solid, vanilla). Toza: search params'ni o'qiydi va keyingisini qaytaradi; navigatsiya sizda.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
import { createListState } from "@querykitjs/web";
|
|
415
|
+
|
|
416
|
+
const list = createListState(users, { schema: usersSchema });
|
|
417
|
+
const { params, clamped } = list.read(new URLSearchParams(location.search));
|
|
418
|
+
const next = list.actions(new URLSearchParams(location.search)).setPage(2); // yangi URLSearchParams
|
|
419
|
+
history.pushState(null, "", `?${next}`);
|
|
420
|
+
```
|
|
421
|
+
|
|
311
422
|
## SSR — `fromSearchParams` (hook'siz)
|
|
312
423
|
|
|
313
424
|
Server component / `getServerSideProps` uchun — URL'дан to'g'ridan-to'g'ri payload:
|
|
@@ -357,6 +468,8 @@ const cur = users.parseCursor(curRes.data); // cur.meta.nextCursor, cur.meta.has
|
|
|
357
468
|
|
|
358
469
|
### TanStack `useQuery` (offset)
|
|
359
470
|
|
|
471
|
+
> Tayyor options: [`@querykitjs/tanstack-query`](../tanstack-query/README.md) shu obyektlarni o'zi quradi — `listOptions`, `infiniteOptions`, `cursorOptions` (kalit, fetch, meta parse, sahifa parametrlari).
|
|
472
|
+
|
|
360
473
|
```tsx
|
|
361
474
|
import { useQuery } from "@tanstack/react-query";
|
|
362
475
|
const users = qk.resource<IUser>("users");
|
|
@@ -465,6 +578,59 @@ createRegistry({ adapter: "drizzle-pg", defaults: { maxPerPage: 50, maxLimit: 25
|
|
|
465
578
|
createQuery({ maxPerPage: Infinity });
|
|
466
579
|
```
|
|
467
580
|
|
|
581
|
+
## Limits
|
|
582
|
+
|
|
583
|
+
`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.
|
|
584
|
+
|
|
585
|
+
| Limit | Default | Nima sanaladi | Oshsa |
|
|
586
|
+
| --------------- | -------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
|
|
587
|
+
| `maxOffset` | `10 000` | `list`: `(page - 1) × perPage`; `infinite`: `offset` | `Error: querykit: page 502 × perPage 20 exceeds maxOffset (10000); …` |
|
|
588
|
+
| `maxSortFields` | `3` | normalizatsiyadan keyingi `sort` maydonlari | `Error: querykit: sort with 4 fields exceeds maxSortFields (3)` |
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
buildListParams({ page: 501, perPage: 20 }); // ✅ offset 10 000
|
|
592
|
+
buildListParams({ page: 502, perPage: 20 }); // ❌ Error — chuqurroq sahifalar uchun buildCursorParams
|
|
593
|
+
|
|
594
|
+
createQuery({ maxOffset: 50_000, maxSortFields: 4 });
|
|
595
|
+
createRegistry({ adapter: "drizzle-pg", defaults: { maxOffset: 50_000 } });
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
- Filtr limitlari (chuqurlik, shartlar, `in` elementlari, satr uzunligi) web'da **tekshirilmaydi** — ularni server (zod / class-validator) rad etadi va 400 qaytaradi.
|
|
599
|
+
- **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.
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
const read = readListParams(schema, new URLSearchParams("page=9999&size=20")); // { page: 501, perPage: 20, clamped: true, … }
|
|
603
|
+
const body = users.fromSearchParams(sp, { schema }); // body.clamped === true
|
|
604
|
+
JSON.stringify(body); // clamped yo'q — u payload'da non-enumerable, server'ga bormaydi
|
|
605
|
+
const { params, clamped } = useListParams(users, { schema }); // render paytida xato yo'q
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Dasturchi to'g'ridan-to'g'ri chaqirgan builder'lar (`buildListParams`, `resource.list`, …) avvalgidek `Error` tashlaydi.
|
|
609
|
+
|
|
610
|
+
## Migrating (limitlar)
|
|
611
|
+
|
|
612
|
+
- `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).
|
|
613
|
+
|
|
614
|
+
- `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.
|
|
615
|
+
- URL'dan o'qilgan chuqur `page` endi xato emas, clamp (`clamped: true`) — error boundary yoki qo'lda tekshiruv kerak emas.
|
|
616
|
+
|
|
617
|
+
## Xatolar — `isQueryKitError`
|
|
618
|
+
|
|
619
|
+
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:
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
import { isQueryKitError, type QueryKitErrorBody } from "@querykitjs/web";
|
|
623
|
+
|
|
624
|
+
// backend: catch (e) { if (e instanceof QueryKitError) return res.status(400).json({ error: e.info }); }
|
|
625
|
+
const body = error.response?.data?.error;
|
|
626
|
+
if (isQueryKitError(body)) {
|
|
627
|
+
// body: { reason, site, key, source?, operation? }
|
|
628
|
+
if (body.reason === "forbidden-key" && body.site === "filter") showFieldError(body.key);
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`isQueryKitError` `reason` va `site` ni core ro'yxatidan tekshiradi, `key` — satr; boshqa har obyekt (masalan `{ statusCode: 400, message }`) → `false`.
|
|
633
|
+
|
|
468
634
|
## So'rovni o'zingiz yuborasiz
|
|
469
635
|
|
|
470
636
|
Kutubxona faqat **so'rovgacha** ishlaydi. Transport yo'q:
|
|
@@ -486,7 +652,7 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
486
652
|
| `registry.resource<T>(name)` | entity-tipli builder/filter/schema/keys/parser |
|
|
487
653
|
| `resource.list/infinite/cursor/params(input)` | payload qurish (offset/infinite/cursor/pagination'siz) |
|
|
488
654
|
| `resource.f` / `createFilters<T>()` / `f` | tipli filter builder / tipli / tipsiz |
|
|
489
|
-
| `resource.search(term, fields)`
|
|
655
|
+
| `resource.search(term, fields, { operation? })` | OR preset (`contains` / `startsWith` / `endsWith`) |
|
|
490
656
|
| `resource.schema(desc)` / `defineListSchema(desc)` | URL param → filter tavsifi |
|
|
491
657
|
| `resource.fromSearchParams(sp, {schema})` | searchParams → list payload (SSR) |
|
|
492
658
|
| `resource.keys` | TanStack/SWR query keys |
|
|
@@ -495,12 +661,16 @@ const { data, meta } = users.parseList(await res.json());
|
|
|
495
661
|
| `buildParams / buildListParams / buildInfiniteParams / buildCursorParams` | standalone builder'lar |
|
|
496
662
|
| `createQuery(config)` | registry'siz builder (default'lar/pruneEmpty) |
|
|
497
663
|
| `schemaToFilter(schema, sp)` | searchParams → `FieldCondition[]` |
|
|
498
|
-
| `searchParamsToPayload(schema, sp)` | searchParams → to'liq payload
|
|
499
|
-
| `readListParams(schema, sp)`
|
|
664
|
+
| `searchParamsToPayload(schema, sp)` | searchParams → to'liq payload (clamp, `clamped`) |
|
|
665
|
+
| `readListParams(schema, sp, url?, limits?)` | searchParams → `ListParams` + `clamped` |
|
|
666
|
+
| `isQueryKitError(body)` / `QueryKitErrorBody` | 400 javobidagi querykit xatosini tanish |
|
|
500
667
|
| `setParam/setPage/setSize/setSort/resetParams` | URL yozish (immutable, page-reset) |
|
|
501
668
|
| `encodeSort/decodeSort` | `[{key,direction}]` ↔ `"-createdAt,id"` (multi-field) |
|
|
502
|
-
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router
|
|
669
|
+
| `useListParams(resource, {schema})` (`/react`) | URL-sync hook (react-router) |
|
|
503
670
|
| `useListParamsBase(resource, {schema, searchParams, setSearchParams})` (`/react`) | router-agnostik hook |
|
|
671
|
+
| `useListParams` / `useListParamsBase` (`/vue`) | Vue composable sifatida xuddi shu (computed ref) |
|
|
672
|
+
| `injectListParams` / `injectListParamsBase` (`/angular`) | Angular signal sifatida xuddi shu |
|
|
673
|
+
| `createListState(resource, {schema, url?, with?})` | framework'siz `read(sp)` / `actions(sp)` |
|
|
504
674
|
|
|
505
675
|
## Ishlab chiqish
|
|
506
676
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
'use strict';var core=require('@angular/core'),rxjsInterop=require('@angular/core/rxjs-interop'),router=require('@angular/router');require('@querykitjs/core');function P(a){let e={};for(let s of new Set(a.keys())){let t=a.getAll(s);e[s]=t.length>1?t:t[0]??"";}return e}var p={pageParam:"page",sizeParam:"size",sortParam:"sortType"};function A(a){return a.map(e=>typeof e=="string"?e.trim():e&&e.key?(e.direction==="desc"?"-":"")+e.key:"").filter(Boolean).join(",")}function l(a){return new URLSearchParams(a)}function g(a,e,s,t={}){let r={...p,...t},n=l(a);return n.delete(r.pageParam),s==null||s===""?n.delete(e):n.set(e,s),n}function S(a,e,s={}){let t={...p,...s},r=l(a);return r.set(t.pageParam,String(e)),r}function L(a,e,s={}){let t={...p,...s},r=l(a);return r.delete(t.pageParam),r.set(t.sizeParam,String(e)),r}function h(a,e,s={}){let t={...p,...s},r=l(a);r.delete(t.pageParam);let n=A(e);return n?r.set(t.sortParam,n):r.delete(t.sortParam),r}function y(a,e=[]){let s=new URLSearchParams;for(let t of e){let r=a.get(t);r!==null&&s.set(t,r);}return s}function U(a,e){let{schema:s,url:t,with:r}=e;return {read(n){let o=a.fromSearchParams(n,{schema:s,url:t,with:r});return {params:o,clamped:o.clamped===true}},actions(n){return {setParam:(o,c)=>g(n,o,c,t),setPage:o=>S(n,o,t),setSize:o=>L(n,o,t),setSort:o=>h(n,o,t),reset:o=>y(n,o)}}}}function f(a,e){let{schema:s,searchParams:t,setSearchParams:r,url:n,with:o}=e,c=U(a,{schema:s,url:n,with:o}),d=core.computed(()=>c.read(t())),m=()=>c.actions(t());return {params:core.computed(()=>d().params),clamped:core.computed(()=>d().clamped),searchParams:t,setParam:(i,x)=>r(m().setParam(i,x)),setPage:i=>r(m().setPage(i)),setSize:i=>r(m().setSize(i)),setSort:i=>r(m().setSort(i)),reset:i=>r(m().reset(i))}}function T(a){return new URLSearchParams(a.keys.flatMap(e=>a.getAll(e).map(s=>[e,s])))}function w(a,e){let s=core.inject(router.ActivatedRoute),t=core.inject(router.Router),r=rxjsInterop.toSignal(s.queryParamMap,{requireSync:true});return f(a,{...e,searchParams:core.computed(()=>T(r())),setSearchParams:n=>{t.navigate([],{relativeTo:s,queryParams:P(n)});}})}exports.injectListParams=w;exports.injectListParamsBase=f;//# sourceMappingURL=index.cjs.map
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/route-query.ts","../../src/url.ts","../../src/list-state.ts","../../src/angular/inject-list-params-base.ts","../../src/angular/inject-list-params.ts"],"names":["searchParamsToQuery","searchParams","query","key","values","URL_DEFAULTS","encodeSort","sort","i","clone","setParam","name","value","url","cfg","next","setPage","page","setSize","size","setSort","encoded","resetParams","keep","createListState","resource","options","schema","withRel","params","injectListParamsBase","setSearchParams","state","read","computed","actions","paramMapToSearchParams","paramMap","injectListParams","route","inject","ActivatedRoute","router","Router","toSignal"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n^ * A route query as vue-router gives it: v4 has `string | null | (string | null)[]` per key,\n * v5 always an array. `null` is a key without `=` (`?q`).\n */\nexport type RouteQuery = Record<string, string | null | (string | null)[] | undefined>;\n\n/** Route query → search params: a repeated key keeps every value; `null` becomes an empty value. */\nexport function queryToSearchParams(query: RouteQuery): URLSearchParams {\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (item !== undefined) searchParams.append(key, item ?? \"\");\n }\n }\n return searchParams;\n}\n\n/** Search params → route query for `router.push({ query })`: one value → string, a repeated key → array. */\nexport function searchParamsToQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const query: Record<string, string | string[]> = {};\n for (const key of new Set(searchParams.keys())) {\n const values = searchParams.getAll(key);\n query[key] = values.length > 1 ? values : (values[0] ?? \"\");\n }\n return query;\n}\n","import { DEFAULT_MAX_OFFSET, DEFAULT_MAX_PER_PAGE, DEFAULT_MAX_SORT_FIELDS, DEFAULT_PER_PAGE } from \"@querykitjs/core\";\n\nimport { buildListParams, clampInt } from \"./query\";\nimport type { FieldDescriptor, ListSchema } from \"./schema\";\nimport type { FieldCondition, Filter, FilterNode, FilterValue, ListParams, ListPayload, Sort, SortDirection, SortInput } from \"./types\";\n\n/** URL param nomlari (sahifa/o'lcham/sort) — sozlanadigan. */\nexport interface UrlConfig {\n pageParam?: string;\n sizeParam?: string;\n sortParam?: string;\n}\n\nconst URL_DEFAULTS: Required<UrlConfig> = {\n pageParam: \"page\",\n sizeParam: \"size\",\n sortParam: \"sortType\",\n};\n\n/**\n * `\"-createdAt,id,-appNumber\"` → `[{ key, direction }, …]` — comma-separated,\n * order preserved, `-` prefix = `desc` (else `asc`). Empty → `undefined`.\n */\nexport function decodeSort(value: string | null | undefined): Sort | undefined {\n if (!value) return undefined;\n const items = value\n .split(\",\")\n .map(s => s.trim())\n .filter(Boolean)\n .map(tok => {\n const desc = tok.startsWith(\"-\");\n return { key: desc ? tok.slice(1) : tok, direction: (desc ? \"desc\" : \"asc\") as SortDirection };\n })\n .filter(i => i.key); // drop empty keys (e.g. a lone \"-\")\n return items.length ? items : undefined;\n}\n\n/** `[{ key, direction }, …]` (or `\"-field\"` shorthand strings) → `\"-createdAt,id,-appNumber\"` (empty → `\"\"`). */\nexport function encodeSort(sort: SortInput): string {\n return sort\n .map(i => (typeof i === \"string\" ? i.trim() : i && i.key ? (i.direction === \"desc\" ? \"-\" : \"\") + i.key : \"\"))\n .filter(Boolean)\n .join(\",\");\n}\n\n/**\n * Schema + searchParams → filter. Applies `range`/`between`/`search`/`split`/\n * `default`/`trim`; empty/absent params are dropped. Returns a flat\n * `FieldCondition[]` (implicit AND), or a tree when a `search` OR-group is\n * present (`{ and: [orGroup, …conditions] }`).\n */\nexport function schemaToFilter(schema: ListSchema, searchParams: URLSearchParams): Filter {\n const conditions: FieldCondition[] = [];\n const groups: FilterNode[] = [];\n\n for (const [param, d] of Object.entries(schema) as [string, FieldDescriptor][]) {\n const key = d.key ?? param;\n\n // range → two inclusive conditions (>= , <=)\n if (d.range) {\n const from = searchParams.get(d.range[0]);\n const to = searchParams.get(d.range[1]);\n if (from) conditions.push(cond(key, \">=\", from));\n if (to) conditions.push(cond(key, \"<=\", to));\n continue;\n }\n\n // between → one condition with a 2-tuple value\n if (d.between) {\n const from = searchParams.get(d.between[0]);\n const to = searchParams.get(d.between[1]);\n if (from && to) conditions.push(cond(key, \"between\", [from, to]));\n continue;\n }\n\n // read value, falling back to `default` only when the param is ABSENT\n // (an empty `?x=` means the user cleared the filter → prune, don't default).\n const raw = searchParams.get(param);\n let value: FilterValue | undefined = raw !== null ? raw : d.default;\n if (typeof value === \"string\" && d.trim) value = value.trim();\n if (value === undefined || value === null || value === \"\") continue;\n if (Array.isArray(value) && value.length === 0) continue;\n\n // search → OR of `contains` across the listed fields (needs a string)\n if (d.search && typeof value === \"string\") {\n groups.push({ or: d.search.map(field => cond(field, \"%_%\", value as string)) });\n continue;\n }\n\n // split → comma-string → array (for `in`/`notIn`); guarded against empties\n if (d.split && typeof value === \"string\") {\n const delimiter = d.split === true ? \",\" : d.split;\n const parts = value\n .split(delimiter)\n .map(s => s.trim())\n .filter(Boolean);\n if (parts.length === 0) continue;\n value = parts;\n }\n\n conditions.push(cond(key, d.operation ?? \"=\", value));\n }\n\n return groups.length === 0 ? conditions : { and: [...groups, ...conditions] };\n}\n\nfunction cond(key: string, operation: FieldCondition[\"operation\"], value: FilterValue): FieldCondition {\n return { key, operation, value };\n}\n\n/**\n * The bounds the URL readers clamp to — the builder's own (`createRegistry`\n * `defaults`); an absent one is the core default.\n */\nexport interface UrlLimits {\n perPage?: number;\n maxPerPage?: number;\n maxOffset?: number;\n maxSortFields?: number;\n}\n\n/** {@link ListParams} read from a URL. `clamped` — the URL's page or sort was cut to the limits. */\nexport type UrlListParams = ListParams & { clamped: boolean };\n\n/**\n * A list payload built from a URL. `clamped` is non-enumerable: readable in code,\n * never serialized, so the wire payload is unchanged.\n */\nexport type UrlListPayload = ListPayload & { readonly clamped?: boolean };\n\n/** Marks a payload with a non-enumerable `clamped` flag. */\nexport function withClamped(payload: ListPayload, clamped: boolean): UrlListPayload {\n return Object.defineProperty(payload, \"clamped\", { value: clamped, enumerable: false });\n}\n\n/** The last page the builder accepts: `(page - 1) × perPage ≤ maxOffset`. */\nfunction lastPage(perPage: number | undefined, limits: UrlLimits): number {\n const resolved = clampInt(perPage, limits.perPage ?? DEFAULT_PER_PAGE, limits.maxPerPage ?? DEFAULT_MAX_PER_PAGE);\n return Math.floor((limits.maxOffset ?? DEFAULT_MAX_OFFSET) / resolved) + 1;\n}\n\n/**\n * searchParams → normalizatsiyalanmagan {@link ListParams} (filter/sort/page/perPage).\n * The URL is user input: a page past `maxOffset` becomes the last allowed page and a\n * sort past `maxSortFields` keeps its first fields, with `clamped: true` — so a\n * hand-edited `?page=9999` never throws during render. The builders still throw\n * for a direct call.\n */\nexport function readListParams(schema: ListSchema, searchParams: URLSearchParams, url: UrlConfig = {}, limits: UrlLimits = {}): UrlListParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const size = searchParams.get(cfg.sizeParam);\n const perPage = size ? Number(size) : undefined;\n const page = Math.trunc(Number(searchParams.get(cfg.pageParam))) || 1;\n const sort = decodeSort(searchParams.get(cfg.sortParam));\n const maxPage = lastPage(perPage, limits);\n const maxSortFields = limits.maxSortFields ?? DEFAULT_MAX_SORT_FIELDS;\n const isPageClamped = page > maxPage;\n const isSortClamped = sort !== undefined && sort.length > maxSortFields;\n return {\n filter: schemaToFilter(schema, searchParams),\n sort: isSortClamped ? sort.slice(0, maxSortFields) : sort,\n page: isPageClamped ? maxPage : page,\n perPage,\n clamped: isPageClamped || isSortClamped,\n };\n}\n\n/** searchParams → to'liq, yuborishga tayyor {@link ListPayload} (core default limits; clamped like {@link readListParams}). */\nexport function searchParamsToPayload(schema: ListSchema, searchParams: URLSearchParams, url?: UrlConfig): UrlListPayload {\n const params = readListParams(schema, searchParams, url);\n return withClamped(buildListParams(params), params.clamped);\n}\n\n/* ------------------------- URL yozish (immutable) ------------------------- */\n/* Har biri YANGI URLSearchParams qaytaradi; filter/sort o'zgarsa `page` reset. */\n\nfunction clone(searchParams: URLSearchParams): URLSearchParams {\n return new URLSearchParams(searchParams);\n}\n\n/** Filter param'ini o'rnatadi/o'chiradi va `page`ni reset qiladi. */\nexport function setParam(searchParams: URLSearchParams, name: string, value: string | null | undefined, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.delete(cfg.pageParam);\n if (value === null || value === undefined || value === \"\") next.delete(name);\n else next.set(name, value);\n return next;\n}\n\n/** Sahifani o'rnatadi (`page` reset qilinmaydi). */\nexport function setPage(searchParams: URLSearchParams, page: number, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.set(cfg.pageParam, String(page));\n return next;\n}\n\n/** Sahifa o'lchamini o'rnatadi va `page`ni reset qiladi. */\nexport function setSize(searchParams: URLSearchParams, size: number, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.delete(cfg.pageParam);\n next.set(cfg.sizeParam, String(size));\n return next;\n}\n\n/** Sortni o'rnatadi (`[{ key, direction }, …]`) va `page`ni reset qiladi. */\nexport function setSort(searchParams: URLSearchParams, sort: SortInput, url: UrlConfig = {}): URLSearchParams {\n const cfg = { ...URL_DEFAULTS, ...url };\n const next = clone(searchParams);\n next.delete(cfg.pageParam);\n const encoded = encodeSort(sort);\n if (encoded) next.set(cfg.sortParam, encoded);\n else next.delete(cfg.sortParam);\n return next;\n}\n\n/** Barcha list param'larini tozalaydi (ixtiyoriy saqlanadigan kalitlar bilan). */\nexport function resetParams(searchParams: URLSearchParams, keep: string[] = []): URLSearchParams {\n const next = new URLSearchParams();\n for (const key of keep) {\n const value = searchParams.get(key);\n if (value !== null) next.set(key, value);\n }\n return next;\n}\n","import type { AdapterName, WithFor } from \"./adapter\";\nimport type { Resource } from \"./registry\";\nimport type { ListSchema } from \"./schema\";\nimport type { ListPayload, SortInput } from \"./types\";\nimport type { UrlConfig } from \"./url\";\nimport { resetParams, setPage, setParam, setSize, setSort } from \"./url\";\n\nexport interface ListStateOptions<A extends AdapterName> {\n /** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */\n schema: ListSchema;\n /** URL param names (page/size/sortType) — optional. */\n url?: UrlConfig;\n /** Fixed relations to load on every request (not URL-driven). */\n with?: WithFor<A>;\n}\n\nexport interface ListStateRead {\n /** Ready-to-send list payload (built from the URL via the resource's defaults). */\n params: ListPayload;\n /** The URL's page or sort was past the limits and was clamped (`?page=9999` → the last allowed page). */\n clamped: boolean;\n}\n\n/** URL setters: each returns the NEXT search params and never mutates the given ones. */\nexport interface ListStateActions {\n /** Set/clear a filter param (resets `page`). */\n setParam: (name: string, value: string | null | undefined) => URLSearchParams;\n setPage: (page: number) => URLSearchParams;\n /** Resets `page`. */\n setSize: (size: number) => URLSearchParams;\n /** Resets `page`; an empty sort clears the param. */\n setSort: (sort: SortInput) => URLSearchParams;\n /** Clears every param except `keep`. */\n reset: (keep?: string[]) => URLSearchParams;\n}\n\nexport interface ListState {\n read: (searchParams: URLSearchParams) => ListStateRead;\n actions: (searchParams: URLSearchParams) => ListStateActions;\n}\n\n/**\n * Framework-free list URL state — the logic behind `useListParams` in `@querykitjs/web/react`\n * and `@querykitjs/web/vue`, for any other framework (Svelte, Solid, vanilla). Pure: it reads\n * search params and returns the next ones; navigating is the caller's job.\n *\n * @example\n * ```ts\n * const list = createListState(users, { schema: usersSchema });\n * const { params, clamped } = list.read(new URLSearchParams(location.search));\n * history.pushState(null, \"\", `?${list.actions(new URLSearchParams(location.search)).setPage(2)}`);\n * ```\n */\nexport function createListState<T, A extends AdapterName>(resource: Resource<T, A>, options: ListStateOptions<A>): ListState {\n const { schema, url, with: withRel } = options;\n return {\n read(searchParams) {\n const params = resource.fromSearchParams(searchParams, { schema, url, with: withRel });\n return { params, clamped: params.clamped === true };\n },\n actions(searchParams) {\n return {\n setParam: (name, value) => setParam(searchParams, name, value, url),\n setPage: page => setPage(searchParams, page, url),\n setSize: size => setSize(searchParams, size, url),\n setSort: sort => setSort(searchParams, sort, url),\n reset: keep => resetParams(searchParams, keep),\n };\n },\n };\n}\n","import { computed, type Signal } from \"@angular/core\";\n\nimport type { AdapterName, WithFor } from \"../adapter\";\nimport { createListState } from \"../list-state\";\nimport type { Resource } from \"../registry\";\nimport type { ListSchema } from \"../schema\";\nimport type { ListPayload, SortInput } from \"../types\";\nimport type { UrlConfig } from \"../url\";\n\nexport interface InjectListParamsBaseOptions<A extends AdapterName> {\n /** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */\n schema: ListSchema;\n /** Current URL search params as a signal (from your router). */\n searchParams: Signal<URLSearchParams>;\n /** Apply new search params (`(next) => void`). */\n setSearchParams: (next: URLSearchParams) => void;\n /** URL param names (page/size/sortType) — optional. */\n url?: UrlConfig;\n /** Fixed relations to load on every request (not URL-driven). */\n with?: WithFor<A>;\n}\n\nexport interface InjectListParamsResult {\n /** Ready-to-send list payload (built from the URL via the resource's defaults). */\n params: Signal<ListPayload>;\n /**\n * The URL's page or sort was past the limits and was clamped (`?page=9999` → the last\n * allowed page). The app may rewrite the URL (`setPage(params().page)`).\n */\n clamped: Signal<boolean>;\n searchParams: Signal<URLSearchParams>;\n /** Set/clear a filter param (resets `page`). */\n setParam: (name: string, value: string | null | undefined) => void;\n setPage: (page: number) => void;\n /** Resets `page`. */\n setSize: (size: number) => void;\n /** Resets `page`; an empty sort clears the param. */\n setSort: (sort: SortInput) => void;\n /** Clears every param except `keep`. */\n reset: (keep?: string[]) => void;\n}\n\n/**\n * Router-agnostic list URL state as signals — the Angular twin of the React and Vue\n * `useListParamsBase`: the same reads, clamping and setters. Bring your own\n * `searchParams` signal and `setSearchParams`.\n */\nexport function injectListParamsBase<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsBaseOptions<A>): InjectListParamsResult {\n const { schema, searchParams, setSearchParams, url, with: withRel } = options;\n const state = createListState(resource, { schema, url, with: withRel });\n const read = computed(() => state.read(searchParams()));\n const actions = () => state.actions(searchParams());\n\n return {\n params: computed(() => read().params),\n clamped: computed(() => read().clamped),\n searchParams,\n setParam: (name, value) => setSearchParams(actions().setParam(name, value)),\n setPage: page => setSearchParams(actions().setPage(page)),\n setSize: size => setSearchParams(actions().setSize(size)),\n setSort: sort => setSearchParams(actions().setSort(sort)),\n reset: keep => setSearchParams(actions().reset(keep)),\n };\n}\n","import { computed, inject } from \"@angular/core\";\nimport { toSignal } from \"@angular/core/rxjs-interop\";\nimport { ActivatedRoute, type ParamMap, Router } from \"@angular/router\";\n\nimport type { AdapterName, WithFor } from \"../adapter\";\nimport type { Resource } from \"../registry\";\nimport { searchParamsToQuery } from \"../route-query\";\nimport type { ListSchema } from \"../schema\";\nimport type { UrlConfig } from \"../url\";\nimport { injectListParamsBase, type InjectListParamsResult } from \"./inject-list-params-base\";\n\nexport interface InjectListParamsOptions<A extends AdapterName> {\n /** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */\n schema: ListSchema;\n /** URL param names (page/size/sortType) — optional. */\n url?: UrlConfig;\n /** Fixed relations to load on every request (not URL-driven). */\n with?: WithFor<A>;\n}\n\nfunction paramMapToSearchParams(paramMap: ParamMap): URLSearchParams {\n return new URLSearchParams(paramMap.keys.flatMap(key => paramMap.getAll(key).map(value => [key, value])));\n}\n\n/**\n * List URL state as signals, with the Angular **Router** (`ActivatedRoute` + `Router`).\n * Reads filters/sort/page from the query params via the schema, builds a ready-to-send\n * payload through the `resource`, and returns URL setters that `router.navigate` (a new\n * history entry; filter/sort/size changes reset `page`). Does **not** fetch. Call it in an\n * injection context (a field initializer or a constructor); elsewhere Angular throws `NG0203`.\n *\n * @example\n * ```ts\n * @Component({ template: `<button (click)=\"list.setPage(2)\">2</button>` })\n * class UsersPage {\n * readonly list = injectListParams(users, { schema: usersSchema });\n * readonly payload = computed(() => this.list.params());\n * }\n * ```\n */\nexport function injectListParams<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsOptions<A>): InjectListParamsResult {\n const route = inject(ActivatedRoute);\n const router = inject(Router);\n // `queryParamMap` replays its current value on subscribe, so the signal is never empty.\n const paramMap = toSignal(route.queryParamMap, { requireSync: true });\n return injectListParamsBase(resource, {\n ...options,\n searchParams: computed(() => paramMapToSearchParams(paramMap())),\n setSearchParams: next => void router.navigate([], { relativeTo: route, queryParams: searchParamsToQuery(next) }),\n });\n}\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AdapterName, ListSchema, UrlConfig, WithFor, ListPayload, SortInput, Resource } from '../index.cjs';
|
|
2
|
+
import { Signal } from '@angular/core';
|
|
3
|
+
import '@querykitjs/core';
|
|
4
|
+
|
|
5
|
+
interface InjectListParamsBaseOptions<A extends AdapterName> {
|
|
6
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
7
|
+
schema: ListSchema;
|
|
8
|
+
/** Current URL search params as a signal (from your router). */
|
|
9
|
+
searchParams: Signal<URLSearchParams>;
|
|
10
|
+
/** Apply new search params (`(next) => void`). */
|
|
11
|
+
setSearchParams: (next: URLSearchParams) => void;
|
|
12
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
13
|
+
url?: UrlConfig;
|
|
14
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
15
|
+
with?: WithFor<A>;
|
|
16
|
+
}
|
|
17
|
+
interface InjectListParamsResult {
|
|
18
|
+
/** Ready-to-send list payload (built from the URL via the resource's defaults). */
|
|
19
|
+
params: Signal<ListPayload>;
|
|
20
|
+
/**
|
|
21
|
+
* The URL's page or sort was past the limits and was clamped (`?page=9999` → the last
|
|
22
|
+
* allowed page). The app may rewrite the URL (`setPage(params().page)`).
|
|
23
|
+
*/
|
|
24
|
+
clamped: Signal<boolean>;
|
|
25
|
+
searchParams: Signal<URLSearchParams>;
|
|
26
|
+
/** Set/clear a filter param (resets `page`). */
|
|
27
|
+
setParam: (name: string, value: string | null | undefined) => void;
|
|
28
|
+
setPage: (page: number) => void;
|
|
29
|
+
/** Resets `page`. */
|
|
30
|
+
setSize: (size: number) => void;
|
|
31
|
+
/** Resets `page`; an empty sort clears the param. */
|
|
32
|
+
setSort: (sort: SortInput) => void;
|
|
33
|
+
/** Clears every param except `keep`. */
|
|
34
|
+
reset: (keep?: string[]) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Router-agnostic list URL state as signals — the Angular twin of the React and Vue
|
|
38
|
+
* `useListParamsBase`: the same reads, clamping and setters. Bring your own
|
|
39
|
+
* `searchParams` signal and `setSearchParams`.
|
|
40
|
+
*/
|
|
41
|
+
declare function injectListParamsBase<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsBaseOptions<A>): InjectListParamsResult;
|
|
42
|
+
|
|
43
|
+
interface InjectListParamsOptions<A extends AdapterName> {
|
|
44
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
45
|
+
schema: ListSchema;
|
|
46
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
47
|
+
url?: UrlConfig;
|
|
48
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
49
|
+
with?: WithFor<A>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* List URL state as signals, with the Angular **Router** (`ActivatedRoute` + `Router`).
|
|
53
|
+
* Reads filters/sort/page from the query params via the schema, builds a ready-to-send
|
|
54
|
+
* payload through the `resource`, and returns URL setters that `router.navigate` (a new
|
|
55
|
+
* history entry; filter/sort/size changes reset `page`). Does **not** fetch. Call it in an
|
|
56
|
+
* injection context (a field initializer or a constructor); elsewhere Angular throws `NG0203`.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* @Component({ template: `<button (click)="list.setPage(2)">2</button>` })
|
|
61
|
+
* class UsersPage {
|
|
62
|
+
* readonly list = injectListParams(users, { schema: usersSchema });
|
|
63
|
+
* readonly payload = computed(() => this.list.params());
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function injectListParams<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsOptions<A>): InjectListParamsResult;
|
|
68
|
+
|
|
69
|
+
export { injectListParams, injectListParamsBase };
|
|
70
|
+
export type { InjectListParamsBaseOptions, InjectListParamsOptions, InjectListParamsResult };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AdapterName, ListSchema, UrlConfig, WithFor, ListPayload, SortInput, Resource } from '../index.js';
|
|
2
|
+
import { Signal } from '@angular/core';
|
|
3
|
+
import '@querykitjs/core';
|
|
4
|
+
|
|
5
|
+
interface InjectListParamsBaseOptions<A extends AdapterName> {
|
|
6
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
7
|
+
schema: ListSchema;
|
|
8
|
+
/** Current URL search params as a signal (from your router). */
|
|
9
|
+
searchParams: Signal<URLSearchParams>;
|
|
10
|
+
/** Apply new search params (`(next) => void`). */
|
|
11
|
+
setSearchParams: (next: URLSearchParams) => void;
|
|
12
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
13
|
+
url?: UrlConfig;
|
|
14
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
15
|
+
with?: WithFor<A>;
|
|
16
|
+
}
|
|
17
|
+
interface InjectListParamsResult {
|
|
18
|
+
/** Ready-to-send list payload (built from the URL via the resource's defaults). */
|
|
19
|
+
params: Signal<ListPayload>;
|
|
20
|
+
/**
|
|
21
|
+
* The URL's page or sort was past the limits and was clamped (`?page=9999` → the last
|
|
22
|
+
* allowed page). The app may rewrite the URL (`setPage(params().page)`).
|
|
23
|
+
*/
|
|
24
|
+
clamped: Signal<boolean>;
|
|
25
|
+
searchParams: Signal<URLSearchParams>;
|
|
26
|
+
/** Set/clear a filter param (resets `page`). */
|
|
27
|
+
setParam: (name: string, value: string | null | undefined) => void;
|
|
28
|
+
setPage: (page: number) => void;
|
|
29
|
+
/** Resets `page`. */
|
|
30
|
+
setSize: (size: number) => void;
|
|
31
|
+
/** Resets `page`; an empty sort clears the param. */
|
|
32
|
+
setSort: (sort: SortInput) => void;
|
|
33
|
+
/** Clears every param except `keep`. */
|
|
34
|
+
reset: (keep?: string[]) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Router-agnostic list URL state as signals — the Angular twin of the React and Vue
|
|
38
|
+
* `useListParamsBase`: the same reads, clamping and setters. Bring your own
|
|
39
|
+
* `searchParams` signal and `setSearchParams`.
|
|
40
|
+
*/
|
|
41
|
+
declare function injectListParamsBase<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsBaseOptions<A>): InjectListParamsResult;
|
|
42
|
+
|
|
43
|
+
interface InjectListParamsOptions<A extends AdapterName> {
|
|
44
|
+
/** URL↔filter schema ({@link defineListSchema} / `resource.schema`). */
|
|
45
|
+
schema: ListSchema;
|
|
46
|
+
/** URL param names (page/size/sortType) — optional. */
|
|
47
|
+
url?: UrlConfig;
|
|
48
|
+
/** Fixed relations to load on every request (not URL-driven). */
|
|
49
|
+
with?: WithFor<A>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* List URL state as signals, with the Angular **Router** (`ActivatedRoute` + `Router`).
|
|
53
|
+
* Reads filters/sort/page from the query params via the schema, builds a ready-to-send
|
|
54
|
+
* payload through the `resource`, and returns URL setters that `router.navigate` (a new
|
|
55
|
+
* history entry; filter/sort/size changes reset `page`). Does **not** fetch. Call it in an
|
|
56
|
+
* injection context (a field initializer or a constructor); elsewhere Angular throws `NG0203`.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* @Component({ template: `<button (click)="list.setPage(2)">2</button>` })
|
|
61
|
+
* class UsersPage {
|
|
62
|
+
* readonly list = injectListParams(users, { schema: usersSchema });
|
|
63
|
+
* readonly payload = computed(() => this.list.params());
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function injectListParams<T, A extends AdapterName>(resource: Resource<T, A>, options: InjectListParamsOptions<A>): InjectListParamsResult;
|
|
68
|
+
|
|
69
|
+
export { injectListParams, injectListParamsBase };
|
|
70
|
+
export type { InjectListParamsBaseOptions, InjectListParamsOptions, InjectListParamsResult };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {computed,inject}from'@angular/core';import {toSignal}from'@angular/core/rxjs-interop';import {ActivatedRoute,Router}from'@angular/router';import'@querykitjs/core';function P(a){let e={};for(let s of new Set(a.keys())){let t=a.getAll(s);e[s]=t.length>1?t:t[0]??"";}return e}var p={pageParam:"page",sizeParam:"size",sortParam:"sortType"};function A(a){return a.map(e=>typeof e=="string"?e.trim():e&&e.key?(e.direction==="desc"?"-":"")+e.key:"").filter(Boolean).join(",")}function l(a){return new URLSearchParams(a)}function g(a,e,s,t={}){let r={...p,...t},n=l(a);return n.delete(r.pageParam),s==null||s===""?n.delete(e):n.set(e,s),n}function S(a,e,s={}){let t={...p,...s},r=l(a);return r.set(t.pageParam,String(e)),r}function L(a,e,s={}){let t={...p,...s},r=l(a);return r.delete(t.pageParam),r.set(t.sizeParam,String(e)),r}function h(a,e,s={}){let t={...p,...s},r=l(a);r.delete(t.pageParam);let n=A(e);return n?r.set(t.sortParam,n):r.delete(t.sortParam),r}function y(a,e=[]){let s=new URLSearchParams;for(let t of e){let r=a.get(t);r!==null&&s.set(t,r);}return s}function U(a,e){let{schema:s,url:t,with:r}=e;return {read(n){let o=a.fromSearchParams(n,{schema:s,url:t,with:r});return {params:o,clamped:o.clamped===true}},actions(n){return {setParam:(o,c)=>g(n,o,c,t),setPage:o=>S(n,o,t),setSize:o=>L(n,o,t),setSort:o=>h(n,o,t),reset:o=>y(n,o)}}}}function f(a,e){let{schema:s,searchParams:t,setSearchParams:r,url:n,with:o}=e,c=U(a,{schema:s,url:n,with:o}),d=computed(()=>c.read(t())),m=()=>c.actions(t());return {params:computed(()=>d().params),clamped:computed(()=>d().clamped),searchParams:t,setParam:(i,x)=>r(m().setParam(i,x)),setPage:i=>r(m().setPage(i)),setSize:i=>r(m().setSize(i)),setSort:i=>r(m().setSort(i)),reset:i=>r(m().reset(i))}}function T(a){return new URLSearchParams(a.keys.flatMap(e=>a.getAll(e).map(s=>[e,s])))}function w(a,e){let s=inject(ActivatedRoute),t=inject(Router),r=toSignal(s.queryParamMap,{requireSync:true});return f(a,{...e,searchParams:computed(()=>T(r())),setSearchParams:n=>{t.navigate([],{relativeTo:s,queryParams:P(n)});}})}export{w as injectListParams,f as injectListParamsBase};//# sourceMappingURL=index.js.map
|
|
2
|
+
//# sourceMappingURL=index.js.map
|