@querykitjs/web 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +115 -31
- package/README.md +115 -31
- package/dist/index.cjs +1 -360
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -3
- package/dist/index.d.ts +31 -3
- package/dist/index.js +1 -336
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +1 -147
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +11 -4
- package/dist/react/index.d.ts +11 -4
- package/dist/react/index.js +1 -144
- package/dist/react/index.js.map +1 -1
- package/dist/{registry-B1WohYab.d.cts → registry-DOVi_zCU.d.cts} +67 -11
- package/dist/{registry-B1WohYab.d.ts → registry-DOVi_zCU.d.ts} +67 -11
- package/package.json +39 -15
- package/skills/querykit-web/SKILL.md +181 -0
|
@@ -46,6 +46,8 @@ interface Params<T = Record<string, unknown>> {
|
|
|
46
46
|
interface ListParams<T = Record<string, unknown>> extends Params<T> {
|
|
47
47
|
page?: number;
|
|
48
48
|
perPage?: number;
|
|
49
|
+
/** `false` — the server runs no count; the meta totals come back `null` (sent only when given). */
|
|
50
|
+
withTotal?: boolean;
|
|
49
51
|
}
|
|
50
52
|
/** Infinite-scroll (limit + offset) paginatsiya kirishi. */
|
|
51
53
|
interface InfiniteParams<T = Record<string, unknown>> extends Params<T> {
|
|
@@ -72,6 +74,8 @@ interface QueryPayload {
|
|
|
72
74
|
interface ListPayload extends QueryPayload {
|
|
73
75
|
page: number;
|
|
74
76
|
perPage: number;
|
|
77
|
+
/** Only present when given in the input. */
|
|
78
|
+
withTotal?: boolean;
|
|
75
79
|
}
|
|
76
80
|
interface InfinitePayload extends QueryPayload {
|
|
77
81
|
limit: number;
|
|
@@ -99,8 +103,10 @@ type RawInfiniteMeta = Partial<Core.InfiniteMeta>;
|
|
|
99
103
|
type RawCursorMeta = Partial<Core.CursorMeta>;
|
|
100
104
|
/** Offset (list) — normalizatsiya qilingan meta (camelCase). */
|
|
101
105
|
interface Meta {
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
/** `null` when the request sent `withTotal: false` (no count ran). */
|
|
107
|
+
totalPages: number | null;
|
|
108
|
+
/** `null` when the request sent `withTotal: false` (no count ran). */
|
|
109
|
+
totalCount: number | null;
|
|
104
110
|
currentPage: number;
|
|
105
111
|
perPage: number;
|
|
106
112
|
hasNext: boolean;
|
|
@@ -262,10 +268,37 @@ declare function encodeSort(sort: SortInput): string;
|
|
|
262
268
|
* present (`{ and: [orGroup, …conditions] }`).
|
|
263
269
|
*/
|
|
264
270
|
declare function schemaToFilter(schema: ListSchema, searchParams: URLSearchParams): Filter;
|
|
265
|
-
/**
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
/**
|
|
272
|
+
* The bounds the URL readers clamp to — the builder's own (`createRegistry`
|
|
273
|
+
* `defaults`); an absent one is the core default.
|
|
274
|
+
*/
|
|
275
|
+
interface UrlLimits {
|
|
276
|
+
perPage?: number;
|
|
277
|
+
maxPerPage?: number;
|
|
278
|
+
maxOffset?: number;
|
|
279
|
+
maxSortFields?: number;
|
|
280
|
+
}
|
|
281
|
+
/** {@link ListParams} read from a URL. `clamped` — the URL's page or sort was cut to the limits. */
|
|
282
|
+
type UrlListParams = ListParams & {
|
|
283
|
+
clamped: boolean;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* A list payload built from a URL. `clamped` is non-enumerable: readable in code,
|
|
287
|
+
* never serialized, so the wire payload is unchanged.
|
|
288
|
+
*/
|
|
289
|
+
type UrlListPayload = ListPayload & {
|
|
290
|
+
readonly clamped?: boolean;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* searchParams → normalizatsiyalanmagan {@link ListParams} (filter/sort/page/perPage).
|
|
294
|
+
* The URL is user input: a page past `maxOffset` becomes the last allowed page and a
|
|
295
|
+
* sort past `maxSortFields` keeps its first fields, with `clamped: true` — so a
|
|
296
|
+
* hand-edited `?page=9999` never throws during render. The builders still throw
|
|
297
|
+
* for a direct call (task 008).
|
|
298
|
+
*/
|
|
299
|
+
declare function readListParams(schema: ListSchema, searchParams: URLSearchParams, url?: UrlConfig, limits?: UrlLimits): UrlListParams;
|
|
300
|
+
/** searchParams → to'liq, yuborishga tayyor {@link ListPayload} (core default limits; clamped like {@link readListParams}). */
|
|
301
|
+
declare function searchParamsToPayload(schema: ListSchema, searchParams: URLSearchParams, url?: UrlConfig): UrlListPayload;
|
|
269
302
|
/** Filter param'ini o'rnatadi/o'chiradi va `page`ni reset qiladi. */
|
|
270
303
|
declare function setParam(searchParams: URLSearchParams, name: string, value: string | null | undefined, url?: UrlConfig): URLSearchParams;
|
|
271
304
|
/** Sahifani o'rnatadi (`page` reset qilinmaydi). */
|
|
@@ -331,6 +364,11 @@ type ResourceCursorParams<T, A extends AdapterName> = Omit<CursorParams<T>, "wit
|
|
|
331
364
|
type ResourceParams<T, A extends AdapterName> = Omit<Params<T>, "with"> & {
|
|
332
365
|
with?: WithFor<A>;
|
|
333
366
|
};
|
|
367
|
+
/** `resource.search` options. */
|
|
368
|
+
interface SearchOptions {
|
|
369
|
+
/** The string operator per field (default `"contains"`). */
|
|
370
|
+
operation?: "contains" | "startsWith" | "endsWith";
|
|
371
|
+
}
|
|
334
372
|
interface RawResponse<T, M> {
|
|
335
373
|
data?: T[];
|
|
336
374
|
meta?: M | null;
|
|
@@ -365,16 +403,22 @@ interface Resource<T, A extends AdapterName> {
|
|
|
365
403
|
params(input?: ResourceParams<T, A>): QueryPayload;
|
|
366
404
|
/** Filter builder typed to `T`'s fields. */
|
|
367
405
|
f: ReturnType<typeof createFilters<T>>;
|
|
368
|
-
/**
|
|
369
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Search preset — OR of `operation` (default `contains`) across `fields` (one input → many columns).
|
|
408
|
+
* Each field must be `filterable` with that operator on the server.
|
|
409
|
+
*/
|
|
410
|
+
search(term: string, fields: FieldKey<T>[], options?: SearchOptions): Filter<T>;
|
|
370
411
|
/** Declare a URL↔filter schema (identity helper). */
|
|
371
412
|
schema<S extends ListSchema>(s: S): S;
|
|
372
|
-
/**
|
|
413
|
+
/**
|
|
414
|
+
* Build a list payload from URL search params (SSR — the non-hook `useListParams`).
|
|
415
|
+
* A page or sort past the registry limits is clamped (`clamped`, non-enumerable — never sent).
|
|
416
|
+
*/
|
|
373
417
|
fromSearchParams(searchParams: URLSearchParams, options: {
|
|
374
418
|
schema: ListSchema;
|
|
375
419
|
url?: UrlConfig;
|
|
376
420
|
with?: WithFor<A>;
|
|
377
|
-
}):
|
|
421
|
+
}): UrlListPayload;
|
|
378
422
|
/** Stable query keys for TanStack/SWR caches. */
|
|
379
423
|
keys: ResourceKeys;
|
|
380
424
|
/** Map a list response's meta to camelCase (`data` typed to `T`). */
|
|
@@ -395,6 +439,10 @@ interface RegistryDefaults {
|
|
|
395
439
|
maxPerPage?: number;
|
|
396
440
|
/** `limit` upper bound (core `DEFAULT_MAX_LIMIT` = 200). */
|
|
397
441
|
maxLimit?: number;
|
|
442
|
+
/** Page depth bound (core `DEFAULT_MAX_OFFSET` = 10 000) — a deeper `list`/`infinite` request throws. */
|
|
443
|
+
maxOffset?: number;
|
|
444
|
+
/** Sort field count bound (core `DEFAULT_MAX_SORT_FIELDS` = 3) — more fields throw. */
|
|
445
|
+
maxSortFields?: number;
|
|
398
446
|
/** `list`/`infinite` sort — `[{key,direction}]` or `["-createdAt", "id"]` shorthand. */
|
|
399
447
|
sort?: SortInput;
|
|
400
448
|
/** Cursor-only defaults. */
|
|
@@ -402,9 +450,16 @@ interface RegistryDefaults {
|
|
|
402
450
|
order?: SortDirection;
|
|
403
451
|
};
|
|
404
452
|
}
|
|
453
|
+
/** The backend registry's `timestamps` field names, so the web default sort names the same field. */
|
|
454
|
+
interface RegistryTimestamps {
|
|
455
|
+
/** The createdAt field (default `"createdAt"`) — the default `list`/`infinite` sort is `[{ key: createdAt, direction: "desc" }]`. */
|
|
456
|
+
createdAt?: string;
|
|
457
|
+
}
|
|
405
458
|
interface RegistryConfig<A extends AdapterName> {
|
|
406
459
|
adapter: A;
|
|
407
460
|
defaults?: RegistryDefaults;
|
|
461
|
+
/** Same as the backend registry's `timestamps`; `defaults.sort` wins over it. */
|
|
462
|
+
timestamps?: RegistryTimestamps;
|
|
408
463
|
/** Drop empty filter values built from form inputs (default `true`). */
|
|
409
464
|
pruneEmpty?: boolean;
|
|
410
465
|
}
|
|
@@ -431,4 +486,5 @@ interface Registry<A extends AdapterName> {
|
|
|
431
486
|
*/
|
|
432
487
|
declare function createRegistry<A extends AdapterName>(config: RegistryConfig<A>): Registry<A>;
|
|
433
488
|
|
|
434
|
-
export {
|
|
489
|
+
export { createFilters as $, createRegistry as a0, decodeSort as a1, defineListSchema as a2, encodeSort as a3, readListParams as a4, resetParams as a5, schemaToFilter as a6, searchParamsToPayload as a7, setPage as a8, setParam as a9, setSize as aa, setSort as ab };
|
|
490
|
+
export type { AdapterName as A, Resource as B, CursorParams as C, DrizzleWith as D, ResourceCursorParams as E, Filter as F, ResourceInfiniteParams as G, ResourceKeys as H, InfiniteParams as I, ResourceListParams as J, ResourceParams as K, ListParams as L, Meta as M, NotGroup as N, OrGroup as O, Params as P, QueryPayload as Q, RawCursorMeta as R, Sort as S, SearchOptions as T, SortDirection as U, SortItem as V, UrlConfig as W, UrlLimits as X, UrlListParams as Y, UrlListPayload as Z, WithFor as _, CursorPayload as a, InfinitePayload as b, ListPayload as c, FieldCondition as d, FilterNode as e, SortInput as f, AndGroup as g, CursorMeta as h, CursorResult as i, FieldDescriptor as j, FieldKey as k, FilterOperator as l, FilterScalar as m, FilterValue as n, InfiniteMeta as o, InfiniteResult as p, ListResult as q, ListSchema as r, MongoosePopulate as s, PrismaInclude as t, RawInfiniteMeta as u, RawMeta as v, Registry as w, RegistryConfig as x, RegistryDefaults as y, RegistryTimestamps as z };
|
|
@@ -46,6 +46,8 @@ interface Params<T = Record<string, unknown>> {
|
|
|
46
46
|
interface ListParams<T = Record<string, unknown>> extends Params<T> {
|
|
47
47
|
page?: number;
|
|
48
48
|
perPage?: number;
|
|
49
|
+
/** `false` — the server runs no count; the meta totals come back `null` (sent only when given). */
|
|
50
|
+
withTotal?: boolean;
|
|
49
51
|
}
|
|
50
52
|
/** Infinite-scroll (limit + offset) paginatsiya kirishi. */
|
|
51
53
|
interface InfiniteParams<T = Record<string, unknown>> extends Params<T> {
|
|
@@ -72,6 +74,8 @@ interface QueryPayload {
|
|
|
72
74
|
interface ListPayload extends QueryPayload {
|
|
73
75
|
page: number;
|
|
74
76
|
perPage: number;
|
|
77
|
+
/** Only present when given in the input. */
|
|
78
|
+
withTotal?: boolean;
|
|
75
79
|
}
|
|
76
80
|
interface InfinitePayload extends QueryPayload {
|
|
77
81
|
limit: number;
|
|
@@ -99,8 +103,10 @@ type RawInfiniteMeta = Partial<Core.InfiniteMeta>;
|
|
|
99
103
|
type RawCursorMeta = Partial<Core.CursorMeta>;
|
|
100
104
|
/** Offset (list) — normalizatsiya qilingan meta (camelCase). */
|
|
101
105
|
interface Meta {
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
/** `null` when the request sent `withTotal: false` (no count ran). */
|
|
107
|
+
totalPages: number | null;
|
|
108
|
+
/** `null` when the request sent `withTotal: false` (no count ran). */
|
|
109
|
+
totalCount: number | null;
|
|
104
110
|
currentPage: number;
|
|
105
111
|
perPage: number;
|
|
106
112
|
hasNext: boolean;
|
|
@@ -262,10 +268,37 @@ declare function encodeSort(sort: SortInput): string;
|
|
|
262
268
|
* present (`{ and: [orGroup, …conditions] }`).
|
|
263
269
|
*/
|
|
264
270
|
declare function schemaToFilter(schema: ListSchema, searchParams: URLSearchParams): Filter;
|
|
265
|
-
/**
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
/**
|
|
272
|
+
* The bounds the URL readers clamp to — the builder's own (`createRegistry`
|
|
273
|
+
* `defaults`); an absent one is the core default.
|
|
274
|
+
*/
|
|
275
|
+
interface UrlLimits {
|
|
276
|
+
perPage?: number;
|
|
277
|
+
maxPerPage?: number;
|
|
278
|
+
maxOffset?: number;
|
|
279
|
+
maxSortFields?: number;
|
|
280
|
+
}
|
|
281
|
+
/** {@link ListParams} read from a URL. `clamped` — the URL's page or sort was cut to the limits. */
|
|
282
|
+
type UrlListParams = ListParams & {
|
|
283
|
+
clamped: boolean;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* A list payload built from a URL. `clamped` is non-enumerable: readable in code,
|
|
287
|
+
* never serialized, so the wire payload is unchanged.
|
|
288
|
+
*/
|
|
289
|
+
type UrlListPayload = ListPayload & {
|
|
290
|
+
readonly clamped?: boolean;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* searchParams → normalizatsiyalanmagan {@link ListParams} (filter/sort/page/perPage).
|
|
294
|
+
* The URL is user input: a page past `maxOffset` becomes the last allowed page and a
|
|
295
|
+
* sort past `maxSortFields` keeps its first fields, with `clamped: true` — so a
|
|
296
|
+
* hand-edited `?page=9999` never throws during render. The builders still throw
|
|
297
|
+
* for a direct call (task 008).
|
|
298
|
+
*/
|
|
299
|
+
declare function readListParams(schema: ListSchema, searchParams: URLSearchParams, url?: UrlConfig, limits?: UrlLimits): UrlListParams;
|
|
300
|
+
/** searchParams → to'liq, yuborishga tayyor {@link ListPayload} (core default limits; clamped like {@link readListParams}). */
|
|
301
|
+
declare function searchParamsToPayload(schema: ListSchema, searchParams: URLSearchParams, url?: UrlConfig): UrlListPayload;
|
|
269
302
|
/** Filter param'ini o'rnatadi/o'chiradi va `page`ni reset qiladi. */
|
|
270
303
|
declare function setParam(searchParams: URLSearchParams, name: string, value: string | null | undefined, url?: UrlConfig): URLSearchParams;
|
|
271
304
|
/** Sahifani o'rnatadi (`page` reset qilinmaydi). */
|
|
@@ -331,6 +364,11 @@ type ResourceCursorParams<T, A extends AdapterName> = Omit<CursorParams<T>, "wit
|
|
|
331
364
|
type ResourceParams<T, A extends AdapterName> = Omit<Params<T>, "with"> & {
|
|
332
365
|
with?: WithFor<A>;
|
|
333
366
|
};
|
|
367
|
+
/** `resource.search` options. */
|
|
368
|
+
interface SearchOptions {
|
|
369
|
+
/** The string operator per field (default `"contains"`). */
|
|
370
|
+
operation?: "contains" | "startsWith" | "endsWith";
|
|
371
|
+
}
|
|
334
372
|
interface RawResponse<T, M> {
|
|
335
373
|
data?: T[];
|
|
336
374
|
meta?: M | null;
|
|
@@ -365,16 +403,22 @@ interface Resource<T, A extends AdapterName> {
|
|
|
365
403
|
params(input?: ResourceParams<T, A>): QueryPayload;
|
|
366
404
|
/** Filter builder typed to `T`'s fields. */
|
|
367
405
|
f: ReturnType<typeof createFilters<T>>;
|
|
368
|
-
/**
|
|
369
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Search preset — OR of `operation` (default `contains`) across `fields` (one input → many columns).
|
|
408
|
+
* Each field must be `filterable` with that operator on the server.
|
|
409
|
+
*/
|
|
410
|
+
search(term: string, fields: FieldKey<T>[], options?: SearchOptions): Filter<T>;
|
|
370
411
|
/** Declare a URL↔filter schema (identity helper). */
|
|
371
412
|
schema<S extends ListSchema>(s: S): S;
|
|
372
|
-
/**
|
|
413
|
+
/**
|
|
414
|
+
* Build a list payload from URL search params (SSR — the non-hook `useListParams`).
|
|
415
|
+
* A page or sort past the registry limits is clamped (`clamped`, non-enumerable — never sent).
|
|
416
|
+
*/
|
|
373
417
|
fromSearchParams(searchParams: URLSearchParams, options: {
|
|
374
418
|
schema: ListSchema;
|
|
375
419
|
url?: UrlConfig;
|
|
376
420
|
with?: WithFor<A>;
|
|
377
|
-
}):
|
|
421
|
+
}): UrlListPayload;
|
|
378
422
|
/** Stable query keys for TanStack/SWR caches. */
|
|
379
423
|
keys: ResourceKeys;
|
|
380
424
|
/** Map a list response's meta to camelCase (`data` typed to `T`). */
|
|
@@ -395,6 +439,10 @@ interface RegistryDefaults {
|
|
|
395
439
|
maxPerPage?: number;
|
|
396
440
|
/** `limit` upper bound (core `DEFAULT_MAX_LIMIT` = 200). */
|
|
397
441
|
maxLimit?: number;
|
|
442
|
+
/** Page depth bound (core `DEFAULT_MAX_OFFSET` = 10 000) — a deeper `list`/`infinite` request throws. */
|
|
443
|
+
maxOffset?: number;
|
|
444
|
+
/** Sort field count bound (core `DEFAULT_MAX_SORT_FIELDS` = 3) — more fields throw. */
|
|
445
|
+
maxSortFields?: number;
|
|
398
446
|
/** `list`/`infinite` sort — `[{key,direction}]` or `["-createdAt", "id"]` shorthand. */
|
|
399
447
|
sort?: SortInput;
|
|
400
448
|
/** Cursor-only defaults. */
|
|
@@ -402,9 +450,16 @@ interface RegistryDefaults {
|
|
|
402
450
|
order?: SortDirection;
|
|
403
451
|
};
|
|
404
452
|
}
|
|
453
|
+
/** The backend registry's `timestamps` field names, so the web default sort names the same field. */
|
|
454
|
+
interface RegistryTimestamps {
|
|
455
|
+
/** The createdAt field (default `"createdAt"`) — the default `list`/`infinite` sort is `[{ key: createdAt, direction: "desc" }]`. */
|
|
456
|
+
createdAt?: string;
|
|
457
|
+
}
|
|
405
458
|
interface RegistryConfig<A extends AdapterName> {
|
|
406
459
|
adapter: A;
|
|
407
460
|
defaults?: RegistryDefaults;
|
|
461
|
+
/** Same as the backend registry's `timestamps`; `defaults.sort` wins over it. */
|
|
462
|
+
timestamps?: RegistryTimestamps;
|
|
408
463
|
/** Drop empty filter values built from form inputs (default `true`). */
|
|
409
464
|
pruneEmpty?: boolean;
|
|
410
465
|
}
|
|
@@ -431,4 +486,5 @@ interface Registry<A extends AdapterName> {
|
|
|
431
486
|
*/
|
|
432
487
|
declare function createRegistry<A extends AdapterName>(config: RegistryConfig<A>): Registry<A>;
|
|
433
488
|
|
|
434
|
-
export {
|
|
489
|
+
export { createFilters as $, createRegistry as a0, decodeSort as a1, defineListSchema as a2, encodeSort as a3, readListParams as a4, resetParams as a5, schemaToFilter as a6, searchParamsToPayload as a7, setPage as a8, setParam as a9, setSize as aa, setSort as ab };
|
|
490
|
+
export type { AdapterName as A, Resource as B, CursorParams as C, DrizzleWith as D, ResourceCursorParams as E, Filter as F, ResourceInfiniteParams as G, ResourceKeys as H, InfiniteParams as I, ResourceListParams as J, ResourceParams as K, ListParams as L, Meta as M, NotGroup as N, OrGroup as O, Params as P, QueryPayload as Q, RawCursorMeta as R, Sort as S, SearchOptions as T, SortDirection as U, SortItem as V, UrlConfig as W, UrlLimits as X, UrlListParams as Y, UrlListPayload as Z, WithFor as _, CursorPayload as a, InfinitePayload as b, ListPayload as c, FieldCondition as d, FilterNode as e, SortInput as f, AndGroup as g, CursorMeta as h, CursorResult as i, FieldDescriptor as j, FieldKey as k, FilterOperator as l, FilterScalar as m, FilterValue as n, InfiniteMeta as o, InfiniteResult as p, ListResult as q, ListSchema as r, MongoosePopulate as s, PrismaInclude as t, RawInfiniteMeta as u, RawMeta as v, Registry as w, RegistryConfig as x, RegistryDefaults as y, RegistryTimestamps as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@querykitjs/web",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Frontend query-building for querykit — typed filters, pagination params & URL sync. Zero-dependency; you send the request.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -23,18 +23,36 @@
|
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
24
24
|
"exports": {
|
|
25
25
|
".": {
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
29
34
|
},
|
|
30
35
|
"./react": {
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/react/index.d.ts",
|
|
38
|
+
"default": "./dist/react/index.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/react/index.d.cts",
|
|
42
|
+
"default": "./dist/react/index.cjs"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"typesVersions": {
|
|
47
|
+
"*": {
|
|
48
|
+
"react": [
|
|
49
|
+
"./dist/react/index.d.ts"
|
|
50
|
+
]
|
|
34
51
|
}
|
|
35
52
|
},
|
|
36
53
|
"files": [
|
|
37
54
|
"dist",
|
|
55
|
+
"skills/querykit-web",
|
|
38
56
|
"README.md",
|
|
39
57
|
"README.en.md",
|
|
40
58
|
"LICENSE"
|
|
@@ -43,31 +61,37 @@
|
|
|
43
61
|
"publishConfig": {
|
|
44
62
|
"access": "public"
|
|
45
63
|
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.12"
|
|
66
|
+
},
|
|
46
67
|
"scripts": {
|
|
47
68
|
"build": "tsup",
|
|
48
69
|
"dev": "tsup --watch",
|
|
49
70
|
"typecheck": "tsc --noEmit",
|
|
50
|
-
"test:smoke": "bun run test/smoke.ts"
|
|
71
|
+
"test:smoke": "bun run test/smoke.ts",
|
|
72
|
+
"test:hook": "bun run test/hook.ts"
|
|
51
73
|
},
|
|
52
74
|
"dependencies": {
|
|
53
|
-
"@querykitjs/core": "^
|
|
75
|
+
"@querykitjs/core": "^2.0.0"
|
|
54
76
|
},
|
|
55
77
|
"peerDependencies": {
|
|
56
|
-
"react": "
|
|
57
|
-
"react-router
|
|
78
|
+
"react": "^18.3 || ^19.2.7",
|
|
79
|
+
"react-router": "^7.0 || ^8.0"
|
|
58
80
|
},
|
|
59
81
|
"peerDependenciesMeta": {
|
|
60
82
|
"react": {
|
|
61
83
|
"optional": true
|
|
62
84
|
},
|
|
63
|
-
"react-router
|
|
85
|
+
"react-router": {
|
|
64
86
|
"optional": true
|
|
65
87
|
}
|
|
66
88
|
},
|
|
67
89
|
"devDependencies": {
|
|
68
|
-
"@types/react": "
|
|
69
|
-
"react": "
|
|
70
|
-
"react
|
|
90
|
+
"@types/react": "19.3.0",
|
|
91
|
+
"@types/react-dom": "19.3.0",
|
|
92
|
+
"react": "19.3.1",
|
|
93
|
+
"react-dom": "19.3.1",
|
|
94
|
+
"react-router": "8.5.1"
|
|
71
95
|
},
|
|
72
96
|
"keywords": [
|
|
73
97
|
"querykit",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: querykit-web
|
|
3
|
+
description: Use when frontend code builds or reads a querykit list request (@querykitjs/web, @querykitjs/web/react) - createRegistry({ adapter }), qk.resource<T>(name), f filters, sort, list / infinite / cursor payloads, withTotal, parseList / parseInfinite / parseCursor, search, adapter-aware with, defineListSchema and URL helpers (readListParams, setPage, setSort), useListParams / useListParamsBase (clamped), fromSearchParams for SSR, query keys, or reading a 400 with isQueryKitError.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Safety rules
|
|
7
|
+
|
|
8
|
+
- DO treat the server as the authority: `filterable`, `sortable` and the filter limits live in the backend repository and its validator. The web package does not repeat them, so a refused filter comes back as a 400.
|
|
9
|
+
- DON'T write `page` / `perPage` / `limit` math by hand. Build every payload with `resource.list` / `infinite` / `cursor` (or `buildListParams` …): `perPage` and `limit` are clamped to `200`, and an offset past `maxOffset` (`10000`) or more than `maxSortFields` (`3`) sort fields throws an `Error`. Use a cursor for deep pages.
|
|
10
|
+
- DO read list state from the URL with `readListParams`, `fromSearchParams` or `useListParams`: they never throw, they clamp a too-deep `page` or too-long `sort` and report it as `clamped: true` (`clamped` is not sent to the server).
|
|
11
|
+
- DON'T send `columns`, `with` or `withDeleted` unless the backend opened them on purpose (validator `allow`, repository `relations`). In `with`, name only the relations and columns the backend lists in `relations`; anything else is `forbidden-key`.
|
|
12
|
+
- DO send dates as ISO strings: the wire is JSON.
|
|
13
|
+
- DO read a 400 body with `isQueryKitError(body)` and use its `reason`, `site` and `key` (for example, mark the field named by `key`).
|
|
14
|
+
- DO handle `withTotal: false`: `meta.totalCount` and `meta.totalPages` are then `null`, so show "next" / "previous" from `hasNext` / `hasPrev` instead of a page count.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- A browser or SSR frontend (React, Next.js, any fetch client) that talks to a querykit backend.
|
|
19
|
+
- Building list, infinite-scroll or cursor request bodies, keeping filters in the URL, and caching with TanStack Query or SWR.
|
|
20
|
+
|
|
21
|
+
The package builds the payload only. You send the request yourself.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createRegistry } from "@querykitjs/web";
|
|
27
|
+
|
|
28
|
+
interface User {
|
|
29
|
+
id: number;
|
|
30
|
+
name: string;
|
|
31
|
+
status: "active" | "blocked";
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const qk = createRegistry({
|
|
36
|
+
adapter: "drizzle-pg", // "mongoose" | "drizzle-pg" | "drizzle-sqlite" | "prisma-pg" — types `with` only
|
|
37
|
+
defaults: { perPage: 20, limit: 20 },
|
|
38
|
+
timestamps: { createdAt: "createdAt" }, // same name as the backend; the default sort is this field, desc
|
|
39
|
+
});
|
|
40
|
+
export const users = qk.resource<User>("users"); // the name is the cache-key namespace
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Common tasks
|
|
44
|
+
|
|
45
|
+
### Filter, sort and an offset page
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { createRegistry } from "@querykitjs/web";
|
|
49
|
+
|
|
50
|
+
const users = createRegistry({ adapter: "prisma-pg" }).resource<{ id: number; name: string; age: number; createdAt: string }>("users");
|
|
51
|
+
const { f } = users;
|
|
52
|
+
|
|
53
|
+
const body = users.list({
|
|
54
|
+
filter: f.and(f.gte("age", 18), f.or(f.contains("name", "ali"), f.isNull("age"))),
|
|
55
|
+
sort: [
|
|
56
|
+
{ key: "name", direction: "asc" },
|
|
57
|
+
{ key: "createdAt", direction: "desc" },
|
|
58
|
+
],
|
|
59
|
+
page: 2,
|
|
60
|
+
perPage: 20,
|
|
61
|
+
withTotal: false, // the server skips the count
|
|
62
|
+
});
|
|
63
|
+
const res = await fetch("/api/users/list", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
64
|
+
const { data, meta } = users.parseList(await res.json());
|
|
65
|
+
console.log(data.length, meta.totalCount ?? "unknown", meta.hasNext);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Infinite scroll and cursor payloads
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createRegistry } from "@querykitjs/web";
|
|
72
|
+
|
|
73
|
+
const posts = createRegistry({ adapter: "mongoose" }).resource<{ _id: string; title: string; createdAt: string }>("posts");
|
|
74
|
+
|
|
75
|
+
const feedBody = posts.infinite({ limit: 20, offset: 40 });
|
|
76
|
+
const firstPage = posts.cursor({ limit: 20, order: "desc", cursorKey: "createdAt" }); // no sort on a cursor
|
|
77
|
+
declare const raw: unknown;
|
|
78
|
+
const feed = posts.parseInfinite(raw as Parameters<typeof posts.parseInfinite>[0]);
|
|
79
|
+
const page = posts.parseCursor(raw as Parameters<typeof posts.parseCursor>[0]);
|
|
80
|
+
const nextBody = posts.cursor({ limit: 20, order: "desc", cursorKey: "createdAt", cursor: page.meta.nextCursor });
|
|
81
|
+
console.log(feedBody, firstPage, feed.meta.hasMore, feed.meta.nextOffset, nextBody);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Search box across several fields
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createRegistry } from "@querykitjs/web";
|
|
88
|
+
|
|
89
|
+
const users = createRegistry({ adapter: "drizzle-pg" }).resource<{ id: number; name: string; email: string }>("users");
|
|
90
|
+
|
|
91
|
+
const filter = users.search("ali", ["name", "email"], { operation: "startsWith" }); // OR of startsWith
|
|
92
|
+
const body = users.list({ filter, page: 1 });
|
|
93
|
+
console.log(body);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Relations with an adapter-aware `with`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { createRegistry } from "@querykitjs/web";
|
|
100
|
+
|
|
101
|
+
const posts = createRegistry({ adapter: "drizzle-pg" }).resource<{ id: number; title: string }>("posts");
|
|
102
|
+
|
|
103
|
+
// only relations and columns the backend lists in `relations`
|
|
104
|
+
const body = posts.list({ with: { author: { columns: { name: true } } }, page: 1 });
|
|
105
|
+
console.log(body.with);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Filters in the URL
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { defineListSchema, readListParams, setPage, setSort } from "@querykitjs/web";
|
|
112
|
+
|
|
113
|
+
const schema = defineListSchema({
|
|
114
|
+
status: { operation: "=", default: "active" },
|
|
115
|
+
q: { search: ["name", "email"] },
|
|
116
|
+
createdAt: { range: ["fromDate", "toDate"] },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const read = readListParams(schema, new URLSearchParams("page=9999&size=20&q=ali"));
|
|
120
|
+
console.log(read.page, read.clamped); // 501 true — clamped, never thrown
|
|
121
|
+
|
|
122
|
+
let sp = new URLSearchParams(location.search);
|
|
123
|
+
sp = setPage(sp, 2);
|
|
124
|
+
sp = setSort(sp, [{ key: "name", direction: "asc" }]); // resets the page
|
|
125
|
+
history.replaceState(null, "", `?${sp.toString()}`);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### React list page with `useListParams`
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { createRegistry } from "@querykitjs/web";
|
|
132
|
+
import { useListParams } from "@querykitjs/web/react";
|
|
133
|
+
|
|
134
|
+
const users = createRegistry({ adapter: "mongoose" }).resource<{ _id: string; name: string; status: string }>("users");
|
|
135
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true }, status: { operation: "=" } });
|
|
136
|
+
|
|
137
|
+
export function UsersListPage() {
|
|
138
|
+
const { params, clamped, setParam, setPage } = useListParams(users, { schema: usersSchema });
|
|
139
|
+
if (clamped) setPage(params.page ?? 1); // fix a hand-edited URL
|
|
140
|
+
return (
|
|
141
|
+
<div>
|
|
142
|
+
<input placeholder="Name" onChange={event => setParam("name", event.target.value)} />
|
|
143
|
+
<pre>{JSON.stringify(users.keys.list(params))}</pre>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### SSR payload and a refused request
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { createRegistry, isQueryKitError } from "@querykitjs/web";
|
|
153
|
+
|
|
154
|
+
const users = createRegistry({ adapter: "prisma-pg" }).resource<{ id: number; name: string }>("users");
|
|
155
|
+
const usersSchema = users.schema({ name: { operation: "%_%", trim: true } });
|
|
156
|
+
|
|
157
|
+
export async function loadUsers(searchParams: URLSearchParams) {
|
|
158
|
+
const body = users.fromSearchParams(searchParams, { schema: usersSchema });
|
|
159
|
+
const res = await fetch("https://api.example.com/users/list", { method: "POST", body: JSON.stringify(body) });
|
|
160
|
+
const json: unknown = await res.json();
|
|
161
|
+
if (res.status === 400) {
|
|
162
|
+
const error = (json as { error?: unknown }).error;
|
|
163
|
+
if (isQueryKitError(error)) return { fieldError: { key: error.key, reason: error.reason, site: error.site } };
|
|
164
|
+
}
|
|
165
|
+
return users.parseList(json as Parameters<typeof users.parseList>[0]);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Request contract
|
|
170
|
+
|
|
171
|
+
The same JSON shape as every querykit package (`@querykitjs/core`): conditions `{ key, operation?, value? }`, groups `{ and }` / `{ or }` / `{ not }` (a flat array = AND), `sort` always `[{ key, direction? }]`, offset `page` / `perPage` / `withTotal`, infinite `limit` / `offset`, cursor `limit` / `cursor` / `cursorKey` / `order` / `direction` (no `sort`). Meta is snake_case: `total_items`, `total_pages`, `has_next`, `has_more`, `next_offset`, `next_cursor`, `prev_cursor`. The body types are `WireOffsetParams`, `WireInfiniteParams` and `WireCursorParams`; validate with `@querykitjs/zod` or `@querykitjs/class-validator`, then pass the body as is. Refusals are `QueryKitError` with `{ source, site, key, operation?, reason }`.
|
|
172
|
+
|
|
173
|
+
`parseList` / `parseInfinite` / `parseCursor` map that meta to camelCase: `totalCount`, `totalPages`, `currentPage`, `perPage`, `hasNext`, `hasPrev`; `limit`, `offset`, `count`, `hasMore`, `nextOffset`; `nextCursor`, `prevCursor`.
|
|
174
|
+
|
|
175
|
+
## Mistakes to avoid
|
|
176
|
+
|
|
177
|
+
- Expecting `list` / `infinite` / `cursor` to fetch: they return a plain payload object, synchronously.
|
|
178
|
+
- Passing `sort` to `cursor`: cursor pages order by `cursorKey` with `order` and `direction`.
|
|
179
|
+
- Computing `meta.totalCount + 1` without a `null` check: it is `null` after `withTotal: false`.
|
|
180
|
+
- Validating filter size in the browser: depth, condition count and string length are checked by the server only.
|
|
181
|
+
- Using `useListParams` outside react-router: use `useListParamsBase` with your own `searchParams` and `setSearchParams`.
|