@beignet/react-query 0.0.2 → 0.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @beignet/react-query
2
2
 
3
+ ## 0.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 8bcb31f: Mark package READMEs with Beignet's experimental alpha status and 0.0.x stability expectations.
8
+ - df4673f: Add TanStack Query filter helpers for Beignet contract cache invalidation and update generated UI examples to use them.
9
+ - 8fd9cb3: Document React Query key scopes, invalidation recipes, pagination, filters, and optimistic update patterns.
10
+ - d137044: Declare `@beignet/core` as a peer dependency with a lockstep version range in
11
+ every integration and provider package instead of a regular `"*"` dependency.
12
+ Installs now always resolve a single shared copy of core, so `instanceof`
13
+ checks such as `isContractError` and upload error identity keep working, and
14
+ mixed Beignet versions fail loudly at install time instead of at runtime.
15
+
16
+ If your package manager does not install peer dependencies automatically, add
17
+ `@beignet/core` to your app alongside these packages. `@beignet/nuqs` now also
18
+ declares `@beignet/react-query` as a peer dependency, and
19
+ `@beignet/provider-storage-s3` now expects you to install
20
+ `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner` yourself, matching
21
+ how other providers treat their SDKs.
22
+
23
+ - 1a79090: Emit Node-compatible ESM: all relative imports in published packages now carry explicit .js extensions, fixing ERR_MODULE_NOT_FOUND when running the CLI or importing package dist files under plain Node.
24
+ - 493d23b: Accept observer-level TanStack Query options (`enabled`, `staleTime`, `select`, `refetchInterval`, `placeholderData`, `throwOnError`) in `queryOptions(...)` and `mutationOptions(...)`. The options were already passed through at runtime but were rejected at the type level. `@beignet/nuqs` `toQueryOptions(...)` inherits the same fix.
25
+ - d6ad8bb: Standardize generated client helpers around `client/index.ts`, add a typed
26
+ React Query invalidation helper, and align package docs with the canonical app
27
+ client entrypoint.
28
+ - 8063d38: Rename the contract front door to `defineContract`/`defineContractGroup`, rename operational commands to tasks (`@beignet/core/tasks`, `defineTasks`, `runTask`, `beignet task run`, `beignet make task`, `server/tasks.ts`, `features/<feature>/tasks/`, `paths.tasks`), and standardize context binding: context-free declarations stay top-level (`defineEvent`), while context-bound definitions come from per-capability factories (`createListeners`, `createJobs`, `createSchedules`, `createNotifications`, `createTasks`) called once in `lib/`. Top-level context-generic `defineListener`, `defineJob`, `defineSchedule`, and `defineNotification` are removed.
29
+ - 192c6ad: Typed clients now attach idempotency keys automatically from contract metadata (override with `idempotencyKey`), React Query mutations keep the key stable across retry attempts, and the shared client error helpers `contractErrorMessage` and `rootFormError` are now exported by @beignet/core/client and @beignet/react-hook-form.
30
+ - 89390fe: Add a contract route segment to generated query keys, an adapter-level `keyHeaders` opt-in, and body pagination for infinite queries.
31
+
32
+ - Query keys now include the contract route after the local name: `["beignet", namespace | null, localName, "GET /v1/todos", params?]`. Two un-namespaced contract groups with different prefixes but the same derived local name (for example `GET /v1/todos` and `GET /v2/todos`) no longer share a cache key. This changes every generated query key, so persisted caches and any hand-written keys that mirrored the old `["beignet", namespace, localName, params?]` shape are invalidated after upgrading.
33
+ - `createReactQuery(client, { keyHeaders })` opts specific header names into generated query keys as a normalized lowercased `headers` key component. Headers stay excluded from keys by default so persisted caches never store credentials such as `Authorization` tokens.
34
+ - `infiniteQueryOptions(...)` accepts a static `body` merged with `page(...).body` per page, includes the static body in the derived key, and sends the merged body on each page request, so body-paginated POST search/list contracts work without a custom `key`/`params` escape hatch.
35
+
36
+ ## 0.0.3
37
+
38
+ ### Patch Changes
39
+
40
+ - Updated dependencies [3160184]
41
+ - Updated dependencies [254ef6d]
42
+ - Updated dependencies [4cb1784]
43
+ - Updated dependencies [8bd9085]
44
+ - @beignet/core@0.0.3
45
+
3
46
  ## 0.0.2
4
47
 
5
48
  ### Patch Changes
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  > TanStack Query integration for Beignet
4
4
 
5
+ > [!CAUTION]
6
+ > Beignet is experimental alpha software. The `0.0.x` package line is for early
7
+ > evaluation, and APIs may change between releases while the framework settles.
8
+
5
9
  This package provides **options-first** TanStack Query integration that's automatically typed based on your contracts. Generate `queryOptions`, `mutationOptions`, and `infiniteQueryOptions` that work with all TanStack Query primitives (`useQuery`, `useMutation`, `useInfiniteQuery`, `prefetchQuery`, `fetchQuery`, etc.) with full type inference.
6
10
 
7
11
  ## Installation
@@ -17,11 +21,12 @@ This package requires TypeScript 5.0 or higher for proper type inference.
17
21
 
18
22
  ## Setup
19
23
 
20
- ### 1. Create your client
24
+ ### 1. Create your app client helpers
21
25
 
22
26
  ```ts
23
- // lib/api-client.ts
24
27
  import { createClient } from "@beignet/core/client";
28
+ import { createReactQuery } from "@beignet/react-query";
29
+ import { QueryClient } from "@tanstack/react-query";
25
30
 
26
31
  export const apiClient = createClient({
27
32
  baseUrl: "https://api.example.com",
@@ -29,27 +34,25 @@ export const apiClient = createClient({
29
34
  Authorization: `Bearer ${getToken()}`,
30
35
  }),
31
36
  });
32
- ```
33
-
34
- ### 2. Create the React Query adapter
35
-
36
- ```ts
37
- // lib/rq.ts
38
- import { createReactQuery } from "@beignet/react-query";
39
- import { apiClient } from "./api-client";
40
37
 
41
38
  export const rq = createReactQuery(apiClient);
39
+
40
+ export function makeQueryClient() {
41
+ return new QueryClient();
42
+ }
42
43
  ```
43
44
 
44
- ### 3. Set up QueryClientProvider
45
+ ### 2. Set up QueryClientProvider
45
46
 
46
47
  ```tsx
47
48
  // app/providers.tsx
48
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
49
+ import { QueryClientProvider } from "@tanstack/react-query";
50
+ import { type ReactNode, useState } from "react";
51
+ import { makeQueryClient } from "@/client";
49
52
 
50
- const queryClient = new QueryClient();
53
+ export function Providers({ children }: { children: ReactNode }) {
54
+ const [queryClient] = useState(() => makeQueryClient());
51
55
 
52
- export function Providers({ children }) {
53
56
  return (
54
57
  <QueryClientProvider client={queryClient}>
55
58
  {children}
@@ -68,7 +71,7 @@ The options-first API generates options objects that can be used with any TanSta
68
71
 
69
72
  ```tsx
70
73
  import { useQuery } from "@tanstack/react-query";
71
- import { rq } from "@/client/rq";
74
+ import { rq } from "@/client";
72
75
  import { getTodo } from "@/features/todos/contracts";
73
76
 
74
77
  function TodoDetail({ id }: { id: string }) {
@@ -97,7 +100,7 @@ function TodoDetail({ id }: { id: string }) {
97
100
 
98
101
  ```tsx
99
102
  import { useQueryClient } from "@tanstack/react-query";
100
- import { rq } from "@/client/rq";
103
+ import { rq } from "@/client";
101
104
  import { getTodo } from "@/features/todos/contracts";
102
105
 
103
106
  function TodoList() {
@@ -119,12 +122,12 @@ function TodoList() {
119
122
  #### Server-side data fetching
120
123
 
121
124
  ```tsx
122
- import { QueryClient, dehydrate, HydrationBoundary } from "@tanstack/react-query";
123
- import { rq } from "@/client/rq";
125
+ import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
126
+ import { makeQueryClient, rq } from "@/client";
124
127
  import { getTodo } from "@/features/todos/contracts";
125
128
 
126
129
  export async function TodoPage({ params }: { params: { id: string } }) {
127
- const queryClient = new QueryClient();
130
+ const queryClient = makeQueryClient();
128
131
 
129
132
  // Fetch on the server
130
133
  const todoQuery = rq(getTodo).queryOptions({
@@ -145,7 +148,7 @@ export async function TodoPage({ params }: { params: { id: string } }) {
145
148
 
146
149
  ```tsx
147
150
  import { useMutation, useQueryClient } from "@tanstack/react-query";
148
- import { rq } from "@/client/rq";
151
+ import { rq } from "@/client";
149
152
  import { createTodo, listTodos } from "@/features/todos/contracts";
150
153
 
151
154
  function CreateTodoForm() {
@@ -154,8 +157,8 @@ function CreateTodoForm() {
154
157
  // Generate mutation options
155
158
  const createTodoMutation = rq(createTodo).mutationOptions({
156
159
  onSuccess: (data, vars, onMutateResult, context) => {
157
- // Invalidate queries
158
- queryClient.invalidateQueries({ queryKey: rq(listTodos).contractKey() });
160
+ // Invalidate every cached listTodos call.
161
+ rq(listTodos).invalidate(queryClient);
159
162
  },
160
163
  });
161
164
 
@@ -180,7 +183,7 @@ function CreateTodoForm() {
180
183
 
181
184
  ```tsx
182
185
  import { useInfiniteQuery } from "@tanstack/react-query";
183
- import { rq } from "@/client/rq";
186
+ import { rq } from "@/client";
184
187
  import { listTodos } from "@/features/todos/contracts";
185
188
 
186
189
  function InfiniteTodoList() {
@@ -215,7 +218,7 @@ function InfiniteTodoList() {
215
218
 
216
219
  ## API reference
217
220
 
218
- ### `createReactQuery(client)`
221
+ ### `createReactQuery(client, options?)`
219
222
 
220
223
  Creates a React Query adapter factory.
221
224
 
@@ -223,6 +226,28 @@ Creates a React Query adapter factory.
223
226
  const rq = createReactQuery(apiClient);
224
227
  ```
225
228
 
229
+ Headers are **excluded from generated query keys by default** because persisted
230
+ caches would otherwise store credentials such as `Authorization` tokens. When a
231
+ header changes response data — a tenant or workspace header, for example — opt
232
+ that specific header into keys with `keyHeaders`:
233
+
234
+ ```ts
235
+ const rq = createReactQuery(apiClient, {
236
+ keyHeaders: ["X-Tenant-Id"],
237
+ });
238
+
239
+ rq(listTodos).queryOptions({
240
+ headers: { "X-Tenant-Id": "tenant-1", Authorization: "Bearer ..." },
241
+ });
242
+ // queryKey: ["beignet", "todos", "listTodos", "GET /todos",
243
+ // { headers: { "x-tenant-id": "tenant-1" } }]
244
+ ```
245
+
246
+ Only whitelisted header names are included. Names are matched
247
+ case-insensitively and stored lowercased in the key. Never whitelist
248
+ credential headers; pass a per-call `key` when you need a fully custom key
249
+ instead.
250
+
226
251
  ### `rq(contract)`
227
252
 
228
253
  Creates a contract helper with query/mutation options methods.
@@ -240,6 +265,7 @@ const queryOpts = helper.queryOptions({
240
265
  path: { ... }, // Required when the contract has required path params
241
266
  query?: { ... }, // Required when the contract has required query params
242
267
  body?: { ... }, // Required when the contract has a required body
268
+ headers?: { ... }, // Sent with the request; keyed only via keyHeaders
243
269
  key?: readonly unknown[], // Custom query key
244
270
  // ...any other TanStack Query options (staleTime, enabled, etc.)
245
271
  });
@@ -263,6 +289,21 @@ const mutationOpts = helper.mutationOptions({
263
289
  useMutation(mutationOpts);
264
290
  ```
265
291
 
292
+ For contracts with idempotency metadata, the generated `mutationFn` derives
293
+ one idempotency key per `mutate(...)` invocation and keeps it stable across
294
+ TanStack retry attempts, so retried requests replay instead of re-executing.
295
+ Separate `mutate(...)` calls get distinct keys — retry stability does not
296
+ deduplicate double-clicks. Pass `idempotencyKey` in the mutation variables for
297
+ retry-with-same-key flows:
298
+
299
+ ```ts
300
+ mutation.mutate({ body: { title: "New todo" }, idempotencyKey: key });
301
+ ```
302
+
303
+ Caveat: calling `mutate()` with no variables skips per-invocation key
304
+ derivation and the client generates a fresh key per attempt. Pass a variables
305
+ object when an idempotent mutation should keep its key across retries.
306
+
266
307
  ### `helper.infiniteQueryOptions(options)`
267
308
 
268
309
  **[Recommended]** Generate infinite query options for pagination.
@@ -271,9 +312,11 @@ useMutation(mutationOpts);
271
312
  const infiniteOpts = helper.infiniteQueryOptions({
272
313
  path?: { ... }, // Static path params included in the cache key
273
314
  query?: { ... }, // Static query params included in the cache key
315
+ body?: { ... }, // Static body fields included in the cache key
274
316
  page?: (ctx: { pageParam }) => ({
275
317
  path?: { ... }, // Page-specific path params
276
318
  query?: { ... }, // Page-specific query params (cursor, offset, etc.)
319
+ body?: { ... }, // Page-specific body fields (cursor, offset, etc.)
277
320
  }),
278
321
  initialPageParam: ...,
279
322
  getNextPageParam: (lastPage) => ...,
@@ -285,6 +328,21 @@ const infiniteOpts = helper.infiniteQueryOptions({
285
328
  useInfiniteQuery(infiniteOpts);
286
329
  ```
287
330
 
331
+ For body-paginated contracts, such as a POST search endpoint, keep the stable
332
+ filters in the static `body` and put the cursor in `page(...).body`. The static
333
+ body is part of the cache key, and each page request sends the merged body:
334
+
335
+ ```ts
336
+ const searchOpts = rq(searchTodos).infiniteQueryOptions({
337
+ body: { term: "beignet" },
338
+ page: ({ pageParam }) => ({
339
+ body: { cursor: pageParam ?? null },
340
+ }),
341
+ initialPageParam: null as string | null,
342
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
343
+ });
344
+ ```
345
+
288
346
  If you need fully dynamic params, pass a custom `key` and use `params(...)` instead:
289
347
 
290
348
  ```ts
@@ -301,7 +359,7 @@ const infiniteOpts = helper.infiniteQueryOptions({
301
359
  ### `helper.namespaceKey()`
302
360
 
303
361
  Generate a namespace-level key for broad cache operations. Contracts created
304
- from `createContractGroup().namespace("todos")` use that namespace.
362
+ from `defineContractGroup().namespace("todos")` use that namespace.
305
363
  Non-namespaced contracts use `null` so they cannot collide with a real
306
364
  namespace string.
307
365
 
@@ -311,35 +369,115 @@ helper.namespaceKey(); // ["beignet", "todos"]
311
369
 
312
370
  ### `helper.contractKey()`
313
371
 
314
- Generate a contract-level key without path, query, or body params.
372
+ Generate a contract-level key without path, query, or body params. The key
373
+ includes the contract route (`"GET /todos/:id"`), so two contracts with the
374
+ same derived local name but different routes — for example `/v1/todos` and
375
+ `/v2/todos` list contracts — never share a cache key.
315
376
 
316
377
  ```ts
317
- helper.contractKey(); // ["beignet", "todos", "getTodo"]
378
+ helper.contractKey(); // ["beignet", "todos", "getTodo", "GET /todos/:id"]
318
379
  ```
319
380
 
381
+ The route segment is also exposed as `helper.route` (`"GET /todos/:id"`).
382
+
320
383
  ### `helper.key(params?)`
321
384
 
322
385
  Generate a stable query key for cache operations.
323
386
 
324
387
  ```ts
325
- helper.key(); // ["beignet", "todos", "getTodo"]
326
- helper.key({ path: { id: "1" } }); // ["beignet", "todos", "getTodo", { path: { id: "1" } }]
388
+ helper.key();
389
+ // ["beignet", "todos", "getTodo", "GET /todos/:id"]
390
+ helper.key({ path: { id: "1" } });
391
+ // ["beignet", "todos", "getTodo", "GET /todos/:id", { path: { id: "1" } }]
327
392
  ```
328
393
 
329
394
  `key(...)` normalizes params the same way the client serializes them: `null`
330
395
  and `undefined` object entries are omitted, while meaningful values like `0`,
331
396
  `false`, and empty strings are preserved. Request bodies are included in query
332
- keys when passed to `queryOptions(...)`.
397
+ keys when passed to `queryOptions(...)`. Headers are excluded unless the
398
+ adapter opted in with `createReactQuery(client, { keyHeaders })`.
399
+
400
+ ### `helper.namespaceFilter(options?)`
333
401
 
334
- These keys are plain TanStack Query keys, so prefix invalidation works as
335
- expected:
402
+ Generate TanStack Query filters for every contract in the helper's Beignet
403
+ namespace.
336
404
 
337
405
  ```ts
338
- queryClient.invalidateQueries({ queryKey: rq(getTodo).namespaceKey() });
339
- queryClient.invalidateQueries({ queryKey: rq(getTodo).contractKey() });
340
- queryClient.invalidateQueries({
341
- queryKey: rq(getTodo).key({ path: { id: "1" } }),
342
- });
406
+ queryClient.invalidateQueries(helper.namespaceFilter());
407
+ queryClient.invalidateQueries(helper.namespaceFilter({ stale: true }));
408
+ ```
409
+
410
+ ### `helper.contractFilter(options?)`
411
+
412
+ Generate TanStack Query filters for every cached call to this contract.
413
+
414
+ ```ts
415
+ queryClient.invalidateQueries(helper.contractFilter());
416
+ ```
417
+
418
+ ### `helper.filter(params?, options?)`
419
+
420
+ Generate TanStack Query filters for one contract call or parameter prefix.
421
+
422
+ ```ts
423
+ queryClient.invalidateQueries(
424
+ helper.filter({ path: { id: "1" } }, { exact: true }),
425
+ );
426
+ ```
427
+
428
+ These filters wrap plain TanStack Query keys, so prefix invalidation works as
429
+ expected while TanStack Query still owns cache behavior:
430
+
431
+ ```ts
432
+ queryClient.invalidateQueries(rq(getTodo).namespaceFilter());
433
+ rq(getTodo).invalidate(queryClient);
434
+ rq(getTodo).invalidate(queryClient, { path: { id: "1" } });
435
+ ```
436
+
437
+ Use the smallest filter that matches the data you want to refresh:
438
+
439
+ | Helper | Scope |
440
+ | --- | --- |
441
+ | `namespaceFilter()` | Every contract in one Beignet namespace. |
442
+ | `contractFilter()` | Every cached call for one contract. |
443
+ | `filter({ path, query, body })` | One parameter-scoped contract key. |
444
+
445
+ ### `helper.invalidate(queryClient, params?, options?)`
446
+
447
+ Invalidate cached data for one contract using the helper's generated filters.
448
+ With no params it invalidates every cached call to the contract. Pass params to
449
+ target a detail key or parameter prefix.
450
+
451
+ ```ts
452
+ await rq(listTodos).invalidate(queryClient);
453
+ await rq(getTodo).invalidate(queryClient, { path: { id: "1" } });
454
+ ```
455
+
456
+ For list writes, invalidate the contract key so every filtered or paginated list
457
+ result refreshes:
458
+
459
+ ```ts
460
+ const createTodoMutation = useMutation(
461
+ rq(createTodo).mutationOptions({
462
+ onSuccess: () => {
463
+ rq(listTodos).invalidate(queryClient);
464
+ },
465
+ }),
466
+ );
467
+ ```
468
+
469
+ For detail writes, update or invalidate the exact detail key and then invalidate
470
+ affected list contracts:
471
+
472
+ ```ts
473
+ const updateTodoMutation = useMutation(
474
+ rq(updateTodo).mutationOptions({
475
+ onSuccess: (_todo, vars) => {
476
+ rq(getTodo).invalidate(queryClient, { path: vars.path });
477
+ rq(listTodos).invalidate(queryClient);
478
+ },
479
+ }),
480
+ );
343
481
  ```
344
482
 
345
483
  ### `helper.endpoint`
package/dist/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
- import type { Client, Endpoint, EndpointCallArgs, InferEndpointContractError, InferSuccessResponse } from "@beignet/core/client";
1
+ import type { Client, Endpoint, EndpointCallArgs, InferBody, InferEndpointContractError, InferPathParams, InferQuery, InferSuccessResponse } from "@beignet/core/client";
2
2
  import { type ContractLike, type HttpContractConfig, type ResolveContract } from "@beignet/core/contracts";
3
- import type { InfiniteQueryObserverOptions, MutationOptions, QueryKey, QueryOptions } from "@tanstack/react-query";
4
- type HasRequiredKeys<T> = {
5
- [K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
6
- }[keyof T];
7
- type EndpointQueryArgs<TContract extends HttpContractConfig, TProvidedHeaders extends string> = Omit<EndpointCallArgs<TContract, TProvidedHeaders>, "rawBody" | "signal">;
8
- type ContractQueryOptionsBase<TContract extends HttpContractConfig> = Omit<QueryOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, InferSuccessResponse<TContract>, QueryKey>, "queryKey" | "queryFn">;
3
+ import type { InfiniteQueryObserverOptions, InvalidateQueryFilters, QueryClient, QueryFunction, QueryKey, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
4
+ type EndpointQueryArgs<TContract extends HttpContractConfig, TProvidedHeaders extends string> = Omit<EndpointCallArgs<TContract, TProvidedHeaders>, "rawBody" | "signal" | "idempotencyKey">;
5
+ type EndpointArgValue<TArgs, TKey extends "path" | "query" | "body" | "headers"> = TKey extends keyof TArgs ? TArgs[TKey] : undefined;
6
+ type IsRequiredValue<T> = undefined extends T ? false : true;
7
+ type RequiresQueryOptionsArgs<TContract extends HttpContractConfig, TProvidedHeaders extends string> = IsRequiredValue<InferPathParams<TContract>> extends true ? true : IsRequiredValue<InferQuery<TContract>> extends true ? true : IsRequiredValue<InferBody<TContract>> extends true ? true : IsRequiredValue<EndpointArgValue<EndpointCallArgs<TContract, TProvidedHeaders>, "headers">> extends true ? true : false;
8
+ type ContractQueryOptionsBase<TContract extends HttpContractConfig> = Omit<UseQueryOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, InferSuccessResponse<TContract>, QueryKey>, "queryKey" | "queryFn">;
9
+ type ContractQueryOptionsResult<TContract extends HttpContractConfig> = ContractQueryOptionsBase<TContract> & {
10
+ queryKey: QueryKey;
11
+ queryFn: QueryFunction<InferSuccessResponse<TContract>, QueryKey>;
12
+ };
9
13
  /**
10
14
  * Options accepted by `ReactQueryContractHelper.queryOptions(...)`.
11
15
  *
@@ -16,22 +20,39 @@ type ContractQueryOptionsBase<TContract extends HttpContractConfig> = Omit<Query
16
20
  export type ContractUseQueryOptions<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = EndpointQueryArgs<TContract, TProvidedHeaders> & {
17
21
  key?: readonly unknown[];
18
22
  } & ContractQueryOptionsBase<TContract>;
19
- type ContractQueryOptionsCallArgs<TContract extends HttpContractConfig, TProvidedHeaders extends string> = HasRequiredKeys<EndpointQueryArgs<TContract, TProvidedHeaders>> extends never ? [args?: ContractUseQueryOptions<TContract, TProvidedHeaders>] : [args: ContractUseQueryOptions<TContract, TProvidedHeaders>];
23
+ type ContractQueryOptionsCallArgs<TContract extends HttpContractConfig, TProvidedHeaders extends string> = RequiresQueryOptionsArgs<TContract, TProvidedHeaders> extends true ? [args: ContractUseQueryOptions<TContract, TProvidedHeaders>] : [args?: ContractUseQueryOptions<TContract, TProvidedHeaders>];
24
+ export type ContractQueryKeyParams<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = {
25
+ path?: EndpointArgValue<EndpointQueryArgs<TContract, TProvidedHeaders>, "path">;
26
+ query?: EndpointArgValue<EndpointQueryArgs<TContract, TProvidedHeaders>, "query">;
27
+ body?: EndpointArgValue<EndpointQueryArgs<TContract, TProvidedHeaders>, "body">;
28
+ headers?: EndpointArgValue<EndpointQueryArgs<TContract, TProvidedHeaders>, "headers">;
29
+ };
20
30
  /**
21
31
  * Options accepted by `ReactQueryContractHelper.mutationOptions(...)`.
22
32
  *
23
33
  * TanStack `mutationFn` is generated from the contract endpoint, so callers pass
24
34
  * the normal TanStack mutation lifecycle options.
25
35
  */
26
- export type ContractUseMutationOptions<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = Omit<MutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>, "mutationFn">;
36
+ export type ContractUseMutationOptions<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = Omit<UseMutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>, "mutationFn">;
37
+ /**
38
+ * Body input for infinite queries.
39
+ *
40
+ * Object bodies are shallow-partial because the full request body is the
41
+ * merge of the static `body` and each `page(...).body`, so neither side has
42
+ * to satisfy required body fields alone. The merged body is still validated
43
+ * by the endpoint call.
44
+ */
45
+ type InfiniteQueryBody<TContract extends HttpContractConfig> = InferBody<TContract> extends Record<string, unknown> ? Partial<InferBody<TContract>> : EndpointCallArgs<TContract>["body"];
27
46
  type InfiniteQueryResolvedParams<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = {
28
47
  path?: EndpointCallArgs<TContract>["path"];
29
48
  query?: EndpointCallArgs<TContract>["query"];
49
+ body?: InfiniteQueryBody<TContract>;
30
50
  headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
31
51
  };
32
52
  type SafeInfiniteQueryArgs<TContract extends HttpContractConfig, TPageParam, TProvidedHeaders extends string> = {
33
53
  path?: EndpointCallArgs<TContract>["path"];
34
54
  query?: EndpointCallArgs<TContract>["query"];
55
+ body?: InfiniteQueryBody<TContract>;
35
56
  headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
36
57
  page?: (ctx: {
37
58
  pageParam: TPageParam;
@@ -46,6 +67,7 @@ type DynamicInfiniteQueryArgs<TContract extends HttpContractConfig, TPageParam,
46
67
  key: readonly unknown[];
47
68
  path?: never;
48
69
  query?: never;
70
+ body?: never;
49
71
  page?: never;
50
72
  };
51
73
  type ContractInfiniteQueryOptions<TContract extends HttpContractConfig, TPageParam, TProvidedHeaders extends string> = (SafeInfiniteQueryArgs<TContract, TPageParam, TProvidedHeaders> | DynamicInfiniteQueryArgs<TContract, TPageParam, TProvidedHeaders>) & Omit<InfiniteQueryObserverOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, InferSuccessResponse<TContract>, QueryKey, TPageParam>, "queryKey" | "queryFn">;
@@ -59,10 +81,14 @@ export type ContractQueryNamespaceKey = readonly [
59
81
  ];
60
82
  /**
61
83
  * Query key for one contract without path/query/body params.
84
+ *
85
+ * The fourth element is the contract route (`"GET /todos"`), so contracts
86
+ * with the same derived local name but different routes never share a key.
62
87
  */
63
88
  export type ContractQueryContractKey = readonly [
64
89
  typeof BEIGNET_QUERY_KEY_SCOPE,
65
90
  string | null,
91
+ string,
66
92
  string
67
93
  ];
68
94
  /**
@@ -72,8 +98,28 @@ export type ContractQueryKey = readonly [
72
98
  typeof BEIGNET_QUERY_KEY_SCOPE,
73
99
  string | null,
74
100
  string,
101
+ string,
75
102
  unknown?
76
103
  ];
104
+ export type ContractQueryFilterOptions = Omit<InvalidateQueryFilters<QueryKey>, "queryKey">;
105
+ export type ContractQueryFilter<TKey extends QueryKey> = ContractQueryFilterOptions & {
106
+ queryKey: TKey;
107
+ };
108
+ /**
109
+ * Options accepted by `createReactQuery(client, options?)`.
110
+ */
111
+ export type CreateReactQueryOptions = {
112
+ /**
113
+ * Header names to include in generated query keys.
114
+ *
115
+ * Headers are excluded from query keys by default because persisted caches
116
+ * would otherwise store credentials such as `Authorization` tokens. Opt in
117
+ * per adapter with the specific identity headers that change response data,
118
+ * such as a tenant header. Names are matched case-insensitively and stored
119
+ * lowercased in the key.
120
+ */
121
+ keyHeaders?: readonly string[];
122
+ };
77
123
  /**
78
124
  * TanStack Query helper bound to one Beignet contract.
79
125
  *
@@ -84,11 +130,17 @@ export type ContractQueryKey = readonly [
84
130
  export declare class ReactQueryContractHelper<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> {
85
131
  private contract;
86
132
  private _endpoint;
87
- constructor(contract: TContract, _endpoint: Endpoint<TContract, TProvidedHeaders>);
133
+ private options;
134
+ constructor(contract: TContract, _endpoint: Endpoint<TContract, TProvidedHeaders>, options?: CreateReactQueryOptions);
88
135
  /**
89
136
  * Fully qualified contract name.
90
137
  */
91
138
  get name(): string;
139
+ /**
140
+ * Contract route used as the key segment that disambiguates contracts with
141
+ * the same local name, such as `/v1/todos` and `/v2/todos` list contracts.
142
+ */
143
+ get route(): string;
92
144
  /**
93
145
  * Resource namespace used for query-key grouping.
94
146
  */
@@ -105,6 +157,22 @@ export declare class ReactQueryContractHelper<TContract extends HttpContractConf
105
157
  * Build a contract-level query key without path/query params.
106
158
  */
107
159
  contractKey(): ContractQueryContractKey;
160
+ /**
161
+ * Build TanStack Query filters for all contracts in this namespace.
162
+ */
163
+ namespaceFilter(options?: ContractQueryFilterOptions): ContractQueryFilter<ContractQueryNamespaceKey>;
164
+ /**
165
+ * Build TanStack Query filters for every cached call to this contract.
166
+ */
167
+ contractFilter(options?: ContractQueryFilterOptions): ContractQueryFilter<ContractQueryContractKey>;
168
+ /**
169
+ * Pick the adapter-whitelisted `keyHeaders` out of call headers.
170
+ *
171
+ * Returns `undefined` unless the adapter opted in with `keyHeaders` and the
172
+ * call provides at least one whitelisted header. Header names are matched
173
+ * case-insensitively and lowercased in the key component.
174
+ */
175
+ private pickKeyHeaders;
108
176
  /**
109
177
  * Build a stable query key for this contract
110
178
  *
@@ -112,28 +180,49 @@ export declare class ReactQueryContractHelper<TContract extends HttpContractConf
112
180
  * This ensures consistent serialization between server (dehydrate) and
113
181
  * client (hydrate), since undefined object values may serialize
114
182
  * differently across React's RSC boundary vs JSON.stringify.
183
+ *
184
+ * Headers are never included unless the adapter opted in with
185
+ * `createReactQuery(client, { keyHeaders })`, and then only the whitelisted
186
+ * header names are included.
115
187
  */
116
188
  key(params?: {
117
189
  path?: unknown;
118
190
  query?: unknown;
119
191
  body?: unknown;
192
+ headers?: unknown;
120
193
  }): ContractQueryKey;
194
+ /**
195
+ * Build TanStack Query filters for one contract call or parameter prefix.
196
+ */
197
+ filter(params?: ContractQueryKeyParams<TContract, TProvidedHeaders>, options?: ContractQueryFilterOptions): ContractQueryFilter<ContractQueryKey>;
198
+ /**
199
+ * Invalidate cached data for this contract.
200
+ *
201
+ * With no params this invalidates every cached call to the contract. Pass
202
+ * path/query/body params to target a single call or parameter prefix.
203
+ */
204
+ invalidate(queryClient: QueryClient, params?: ContractQueryKeyParams<TContract, TProvidedHeaders>, options?: ContractQueryFilterOptions): Promise<void>;
121
205
  /**
122
206
  * Create query options for TanStack Query.
123
207
  *
124
208
  * Pass the result to `useQuery`, `prefetchQuery`, `fetchQuery`, or any API
125
209
  * that accepts query options.
126
210
  */
127
- queryOptions(...callArgs: ContractQueryOptionsCallArgs<TContract, TProvidedHeaders>): QueryOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, InferSuccessResponse<TContract>, QueryKey> & {
128
- queryKey: QueryKey;
129
- };
211
+ queryOptions(...callArgs: ContractQueryOptionsCallArgs<TContract, TProvidedHeaders>): ContractQueryOptionsResult<TContract>;
130
212
  /**
131
213
  * Create mutation options for TanStack Query.
132
214
  *
133
215
  * The generated `mutationFn` accepts the same variables shape as the
134
216
  * underlying contract endpoint call.
217
+ *
218
+ * For contracts with idempotency metadata, the generated `mutationFn`
219
+ * derives one idempotency key per `mutate(...)` invocation and keeps it
220
+ * stable across TanStack retry attempts, which re-invoke `mutationFn` with
221
+ * the same variables object. Pass `idempotencyKey` in the variables to
222
+ * control the key explicitly. Calling `mutate()` with no variables falls
223
+ * back to per-attempt key generation in the client.
135
224
  */
136
- mutationOptions(args?: Omit<MutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>, "mutationFn">): MutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>;
225
+ mutationOptions(args?: ContractUseMutationOptions<TContract, TProvidedHeaders>): UseMutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>;
137
226
  /**
138
227
  * Create infinite query options for TanStack Query.
139
228
  *
@@ -152,7 +241,11 @@ export declare class ReactQueryContractHelper<TContract extends HttpContractConf
152
241
  *
153
242
  * Create this once near client setup, then bind contracts with the returned
154
243
  * `rq(contract)` function.
244
+ *
245
+ * Pass `keyHeaders` to include specific identity headers, such as a tenant
246
+ * header, in generated query keys. Headers are excluded by default so
247
+ * persisted caches never store credentials.
155
248
  */
156
- export declare function createReactQuery<TProvidedHeaders extends string = never>(client: Client<TProvidedHeaders>): <TContractLike extends ContractLike>(contract: TContractLike) => ReactQueryContractHelper<ResolveContract<TContractLike>, TProvidedHeaders>;
249
+ export declare function createReactQuery<TProvidedHeaders extends string = never>(client: Client<TProvidedHeaders>, options?: CreateReactQueryOptions): <TContractLike extends ContractLike>(contract: TContractLike) => ReactQueryContractHelper<ResolveContract<TContractLike>, TProvidedHeaders>;
157
250
  export {};
158
251
  //# sourceMappingURL=index.d.ts.map