@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.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'да; funksiya/SQL formalarи ko'rsatilmaydi.
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-dom` ichida), `useListParamsBase` (router-agnostik). Core framework'siz — Vue ham shu core'ga bog'lanadi.
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-dom`) ixtiyoriy peer.
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-dom (peer).
61
- # Router-agnostik `useListParamsBase` react-router-dom talab qilmaydi.
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
- | `pruneEmpty` | Form input'lardan qurilgan bo'sh filterlarni avtomatik tashlash (default `true`). |
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)` | ko'p maydon bo'ylab OR-of-`contains` preset |
117
- | `users.schema(desc)` | `defineListSchema` bog'langan (URL ↔ filter) |
118
- | `users.fromSearchParams(sp, {schema})` | `URLSearchParams`'dan to'g'ridan-to'g'ri list payload (SSR / hook'siz) |
119
- | `users.keys` | TanStack/SWR uchun barqaror query keys |
120
- | `users.parseList / parseInfinite / parseCursor` | javob meta'sini snake→camel |
121
- | `ListResult<T> / InfiniteResult<T> / CursorResult<T>` (tiplar) | `parse*` qaytaradigan tiplar |
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
- | `drizzle-pg` / `drizzle-sqlite` | `boolean \| { columns?, with?, limit?, offset? }` | `where:(f,op)=>…`, `orderBy:(f,op)=>…`, raw `sql` |
233
- | `prisma-pg` | `boolean \| { select?, include?, where?, orderBy?, take?, skip? }` | funksiya formalar |
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 formalarини JSON'да yubora olmaganingiz uchun ular umuman taklif qilinmaydi.
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-dom ichida ishlatiladi
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
- **Router-agnostik** (Next.js, TanStack Router) — base hook'ga `searchParams` bering (`react-router-dom` talab qilmaydi):
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)` | OR-of-contains preset |
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)` | searchParams → `ListParams` |
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-dom) |
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