@beignet/react-query 0.0.1

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 ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/react-query
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
package/README.md ADDED
@@ -0,0 +1,407 @@
1
+ # @beignet/react-query
2
+
3
+ > TanStack Query integration for Beignet
4
+
5
+ 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
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @beignet/react-query @beignet/core @tanstack/react-query react
11
+ ```
12
+
13
+
14
+ ## TypeScript requirements
15
+
16
+ This package requires TypeScript 5.0 or higher for proper type inference.
17
+
18
+ ## Setup
19
+
20
+ ### 1. Create your client
21
+
22
+ ```ts
23
+ // lib/api-client.ts
24
+ import { createClient } from "@beignet/core/client";
25
+
26
+ export const apiClient = createClient({
27
+ baseUrl: "https://api.example.com",
28
+ headers: async () => ({
29
+ Authorization: `Bearer ${getToken()}`,
30
+ }),
31
+ });
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
+
41
+ export const rq = createReactQuery(apiClient);
42
+ ```
43
+
44
+ ### 3. Set up QueryClientProvider
45
+
46
+ ```tsx
47
+ // app/providers.tsx
48
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
49
+
50
+ const queryClient = new QueryClient();
51
+
52
+ export function Providers({ children }) {
53
+ return (
54
+ <QueryClientProvider client={queryClient}>
55
+ {children}
56
+ </QueryClientProvider>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ### Options-first API (recommended)
64
+
65
+ The options-first API generates options objects that can be used with any TanStack Query primitive.
66
+
67
+ #### Query options
68
+
69
+ ```tsx
70
+ import { useQuery } from "@tanstack/react-query";
71
+ import { rq } from "@/client/rq";
72
+ import { getTodo } from "@/features/todos/contracts";
73
+
74
+ function TodoDetail({ id }: { id: string }) {
75
+ // Generate query options
76
+ const todoQuery = rq(getTodo).queryOptions({
77
+ path: { id },
78
+ staleTime: 30_000,
79
+ });
80
+
81
+ // Use with any TanStack Query primitive
82
+ const { data, isLoading, error } = useQuery(todoQuery);
83
+
84
+ if (isLoading) return <div>Loading...</div>;
85
+ if (error) return <div>Error: {error.message}</div>;
86
+
87
+ return (
88
+ <div>
89
+ <h1>{data.title}</h1>
90
+ <p>Completed: {data.completed ? "Yes" : "No"}</p>
91
+ </div>
92
+ );
93
+ }
94
+ ```
95
+
96
+ #### Prefetching
97
+
98
+ ```tsx
99
+ import { useQueryClient } from "@tanstack/react-query";
100
+ import { rq } from "@/client/rq";
101
+ import { getTodo } from "@/features/todos/contracts";
102
+
103
+ function TodoList() {
104
+ const queryClient = useQueryClient();
105
+
106
+ const handleHover = (id: string) => {
107
+ // Prefetch on hover using queryOptions
108
+ const todoQuery = rq(getTodo).queryOptions({
109
+ path: { id },
110
+ });
111
+
112
+ queryClient.prefetchQuery(todoQuery);
113
+ };
114
+
115
+ return <div>{/* ... */}</div>;
116
+ }
117
+ ```
118
+
119
+ #### Server-side data fetching
120
+
121
+ ```tsx
122
+ import { QueryClient, dehydrate, HydrationBoundary } from "@tanstack/react-query";
123
+ import { rq } from "@/client/rq";
124
+ import { getTodo } from "@/features/todos/contracts";
125
+
126
+ export async function TodoPage({ params }: { params: { id: string } }) {
127
+ const queryClient = new QueryClient();
128
+
129
+ // Fetch on the server
130
+ const todoQuery = rq(getTodo).queryOptions({
131
+ path: { id: params.id },
132
+ });
133
+
134
+ await queryClient.prefetchQuery(todoQuery);
135
+
136
+ return (
137
+ <HydrationBoundary state={dehydrate(queryClient)}>
138
+ <TodoDetail id={params.id} />
139
+ </HydrationBoundary>
140
+ );
141
+ }
142
+ ```
143
+
144
+ #### Mutation options
145
+
146
+ ```tsx
147
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
148
+ import { rq } from "@/client/rq";
149
+ import { createTodo, listTodos } from "@/features/todos/contracts";
150
+
151
+ function CreateTodoForm() {
152
+ const queryClient = useQueryClient();
153
+
154
+ // Generate mutation options
155
+ const createTodoMutation = rq(createTodo).mutationOptions({
156
+ onSuccess: (data, vars, onMutateResult, context) => {
157
+ // Invalidate queries
158
+ queryClient.invalidateQueries({ queryKey: rq(listTodos).contractKey() });
159
+ },
160
+ });
161
+
162
+ const mutation = useMutation(createTodoMutation);
163
+
164
+ const handleSubmit = (data: { title: string }) => {
165
+ mutation.mutate({ body: data });
166
+ };
167
+
168
+ return (
169
+ <form onSubmit={handleSubmit}>
170
+ {/* form fields */}
171
+ <button disabled={mutation.isPending}>
172
+ {mutation.isPending ? "Creating..." : "Create Todo"}
173
+ </button>
174
+ </form>
175
+ );
176
+ }
177
+ ```
178
+
179
+ #### Infinite query options
180
+
181
+ ```tsx
182
+ import { useInfiniteQuery } from "@tanstack/react-query";
183
+ import { rq } from "@/client/rq";
184
+ import { listTodos } from "@/features/todos/contracts";
185
+
186
+ function InfiniteTodoList() {
187
+ const todosInfiniteQuery = rq(listTodos).infiniteQueryOptions({
188
+ query: { status: "open" },
189
+ page: ({ pageParam }) => ({
190
+ query: { cursor: pageParam ?? null },
191
+ }),
192
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
193
+ initialPageParam: null,
194
+ });
195
+
196
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
197
+ useInfiniteQuery(todosInfiniteQuery);
198
+
199
+ return (
200
+ <div>
201
+ {data?.pages.map((page) =>
202
+ page.items.map((todo) => <div key={todo.id}>{todo.title}</div>)
203
+ )}
204
+
205
+ <button
206
+ onClick={() => fetchNextPage()}
207
+ disabled={!hasNextPage || isFetchingNextPage}
208
+ >
209
+ {isFetchingNextPage ? "Loading more..." : "Load More"}
210
+ </button>
211
+ </div>
212
+ );
213
+ }
214
+ ```
215
+
216
+ ## API reference
217
+
218
+ ### `createReactQuery(client)`
219
+
220
+ Creates a React Query adapter factory.
221
+
222
+ ```ts
223
+ const rq = createReactQuery(apiClient);
224
+ ```
225
+
226
+ ### `rq(contract)`
227
+
228
+ Creates a contract helper with query/mutation options methods.
229
+
230
+ ```ts
231
+ const helper = rq(getTodo);
232
+ ```
233
+
234
+ ### `helper.queryOptions(options)`
235
+
236
+ **[Recommended]** Generate query options that can be passed to any TanStack Query primitive.
237
+
238
+ ```ts
239
+ const queryOpts = helper.queryOptions({
240
+ path: { ... }, // Required when the contract has required path params
241
+ query?: { ... }, // Required when the contract has required query params
242
+ body?: { ... }, // Required when the contract has a required body
243
+ key?: readonly unknown[], // Custom query key
244
+ // ...any other TanStack Query options (staleTime, enabled, etc.)
245
+ });
246
+
247
+ // Use with any primitive
248
+ useQuery(queryOpts);
249
+ queryClient.prefetchQuery(queryOpts);
250
+ queryClient.fetchQuery(queryOpts);
251
+ queryClient.ensureQueryData(queryOpts);
252
+ ```
253
+
254
+ ### `helper.mutationOptions(options)`
255
+
256
+ **[Recommended]** Generate mutation options that can be passed to `useMutation`.
257
+
258
+ ```ts
259
+ const mutationOpts = helper.mutationOptions({
260
+ // ...any TanStack Query mutation options (onSuccess, onError, retry, etc.)
261
+ });
262
+
263
+ useMutation(mutationOpts);
264
+ ```
265
+
266
+ ### `helper.infiniteQueryOptions(options)`
267
+
268
+ **[Recommended]** Generate infinite query options for pagination.
269
+
270
+ ```ts
271
+ const infiniteOpts = helper.infiniteQueryOptions({
272
+ path?: { ... }, // Static path params included in the cache key
273
+ query?: { ... }, // Static query params included in the cache key
274
+ page?: (ctx: { pageParam }) => ({
275
+ path?: { ... }, // Page-specific path params
276
+ query?: { ... }, // Page-specific query params (cursor, offset, etc.)
277
+ }),
278
+ initialPageParam: ...,
279
+ getNextPageParam: (lastPage) => ...,
280
+ getPreviousPageParam: (firstPage) => ...,
281
+ key?: readonly unknown[], // Optional custom query key
282
+ // ...any other TanStack Query infinite query options
283
+ });
284
+
285
+ useInfiniteQuery(infiniteOpts);
286
+ ```
287
+
288
+ If you need fully dynamic params, pass a custom `key` and use `params(...)` instead:
289
+
290
+ ```ts
291
+ const infiniteOpts = helper.infiniteQueryOptions({
292
+ key: ["todos", { status: "open" }],
293
+ params: ({ pageParam }) => ({
294
+ query: { status: "open", cursor: pageParam ?? null },
295
+ }),
296
+ initialPageParam: null,
297
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
298
+ });
299
+ ```
300
+
301
+ ### `helper.namespaceKey()`
302
+
303
+ Generate a namespace-level key for broad cache operations. Contracts created
304
+ from `createContractGroup().namespace("todos")` use that namespace.
305
+ Non-namespaced contracts use `null` so they cannot collide with a real
306
+ namespace string.
307
+
308
+ ```ts
309
+ helper.namespaceKey(); // ["beignet", "todos"]
310
+ ```
311
+
312
+ ### `helper.contractKey()`
313
+
314
+ Generate a contract-level key without path, query, or body params.
315
+
316
+ ```ts
317
+ helper.contractKey(); // ["beignet", "todos", "getTodo"]
318
+ ```
319
+
320
+ ### `helper.key(params?)`
321
+
322
+ Generate a stable query key for cache operations.
323
+
324
+ ```ts
325
+ helper.key(); // ["beignet", "todos", "getTodo"]
326
+ helper.key({ path: { id: "1" } }); // ["beignet", "todos", "getTodo", { path: { id: "1" } }]
327
+ ```
328
+
329
+ `key(...)` normalizes params the same way the client serializes them: `null`
330
+ and `undefined` object entries are omitted, while meaningful values like `0`,
331
+ `false`, and empty strings are preserved. Request bodies are included in query
332
+ keys when passed to `queryOptions(...)`.
333
+
334
+ These keys are plain TanStack Query keys, so prefix invalidation works as
335
+ expected:
336
+
337
+ ```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
+ });
343
+ ```
344
+
345
+ ### `helper.endpoint`
346
+
347
+ Access the underlying endpoint for manual calls.
348
+
349
+ ```ts
350
+ const data = await helper.endpoint.call({ path: { id: "123" } });
351
+ ```
352
+
353
+ ## Type inference
354
+
355
+ All options are fully typed based on your contracts:
356
+
357
+ ```ts
358
+ const { data } = useQuery(rq(getTodo).queryOptions({ path: { id: "123" } }));
359
+ // data is typed as: { id: string; title: string; completed: boolean }
360
+
361
+ const mutation = useMutation(rq(createTodo).mutationOptions());
362
+ mutation.mutate({ body: { title: "New Todo" } });
363
+ // body is typed as: { title: string; completed?: boolean }
364
+ ```
365
+
366
+ ## Error handling
367
+
368
+ React Query errors are typed from the endpoint contract, including declared
369
+ catalog errors and non-2xx response bodies:
370
+
371
+ ```ts
372
+ const todo = rq(getTodo);
373
+ const { error } = useQuery(todo.queryOptions({ path: { id: "123" } }));
374
+
375
+ if (todo.endpoint.isError(error, { code: "TODO_NOT_FOUND" })) {
376
+ console.log(error.details); // typed from the catalog details schema
377
+ } else if (error) {
378
+ console.log(error.message);
379
+ }
380
+ ```
381
+
382
+ ## Migration from hook-first API
383
+
384
+ If you were using the hook-first API, you can migrate to the options-first API:
385
+
386
+ **Before (hook-first):**
387
+ ```tsx
388
+ const { data } = rq(getTodo).useQuery({ path: { id } });
389
+ ```
390
+
391
+ **After (options-first):**
392
+ ```tsx
393
+ const { data } = useQuery(rq(getTodo).queryOptions({ path: { id } }));
394
+ ```
395
+
396
+ Hook wrappers have been removed—use the options-first API for all queries and mutations.
397
+
398
+ ## Related packages
399
+
400
+ - [`@beignet/core/contracts`](https://beignet.dev/contracts) - Core contract definitions
401
+ - [`@beignet/core/client`](https://beignet.dev/client) - HTTP client
402
+ - [`@beignet/nuqs`](https://beignet.dev/nuqs) - URL query state integration
403
+ - [`@beignet/react-hook-form`](https://beignet.dev/react-hook-form) - Form integration
404
+
405
+ ## License
406
+
407
+ MIT
@@ -0,0 +1,128 @@
1
+ import type { Client, Endpoint, EndpointCallArgs, InferEndpointContractError, InferSuccessResponse } from "@beignet/core/client";
2
+ import { type ContractLike, type HttpContractConfig, type ResolveContract } from "@beignet/core/contracts";
3
+ import type { InfiniteQueryObserverOptions, MutationOptions, QueryKey, QueryOptions } from "@tanstack/react-query";
4
+ /**
5
+ * Options for useQuery hook
6
+ */
7
+ type HasRequiredKeys<T> = {
8
+ [K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
9
+ }[keyof T];
10
+ type EndpointQueryArgs<TContract extends HttpContractConfig, TProvidedHeaders extends string> = Omit<EndpointCallArgs<TContract, TProvidedHeaders>, "rawBody" | "signal">;
11
+ type ContractQueryOptionsBase<TContract extends HttpContractConfig> = Omit<QueryOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, InferSuccessResponse<TContract>, QueryKey>, "queryKey" | "queryFn">;
12
+ export type ContractUseQueryOptions<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = EndpointQueryArgs<TContract, TProvidedHeaders> & {
13
+ key?: readonly unknown[];
14
+ } & ContractQueryOptionsBase<TContract>;
15
+ type ContractQueryOptionsCallArgs<TContract extends HttpContractConfig, TProvidedHeaders extends string> = HasRequiredKeys<EndpointQueryArgs<TContract, TProvidedHeaders>> extends never ? [args?: ContractUseQueryOptions<TContract, TProvidedHeaders>] : [args: ContractUseQueryOptions<TContract, TProvidedHeaders>];
16
+ /**
17
+ * Options for useMutation hook
18
+ */
19
+ export type ContractUseMutationOptions<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = Omit<MutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>, "mutationFn">;
20
+ type InfiniteQueryResolvedParams<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> = {
21
+ path?: EndpointCallArgs<TContract>["path"];
22
+ query?: EndpointCallArgs<TContract>["query"];
23
+ headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
24
+ };
25
+ type SafeInfiniteQueryArgs<TContract extends HttpContractConfig, TPageParam, TProvidedHeaders extends string> = {
26
+ path?: EndpointCallArgs<TContract>["path"];
27
+ query?: EndpointCallArgs<TContract>["query"];
28
+ headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
29
+ page?: (ctx: {
30
+ pageParam: TPageParam;
31
+ }) => InfiniteQueryResolvedParams<TContract, TProvidedHeaders>;
32
+ key?: readonly unknown[];
33
+ params?: never;
34
+ };
35
+ type DynamicInfiniteQueryArgs<TContract extends HttpContractConfig, TPageParam, TProvidedHeaders extends string> = {
36
+ params: (ctx: {
37
+ pageParam: TPageParam;
38
+ }) => InfiniteQueryResolvedParams<TContract, TProvidedHeaders>;
39
+ key: readonly unknown[];
40
+ path?: never;
41
+ query?: never;
42
+ page?: never;
43
+ };
44
+ 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">;
45
+ declare const BEIGNET_QUERY_KEY_SCOPE = "beignet";
46
+ export type ContractQueryNamespaceKey = readonly [
47
+ typeof BEIGNET_QUERY_KEY_SCOPE,
48
+ string | null
49
+ ];
50
+ export type ContractQueryContractKey = readonly [
51
+ typeof BEIGNET_QUERY_KEY_SCOPE,
52
+ string | null,
53
+ string
54
+ ];
55
+ export type ContractQueryKey = readonly [
56
+ typeof BEIGNET_QUERY_KEY_SCOPE,
57
+ string | null,
58
+ string,
59
+ unknown?
60
+ ];
61
+ /**
62
+ * React Query contract helper
63
+ */
64
+ export declare class ReactQueryContractHelper<TContract extends HttpContractConfig, TProvidedHeaders extends string = never> {
65
+ private contract;
66
+ private _endpoint;
67
+ constructor(contract: TContract, _endpoint: Endpoint<TContract, TProvidedHeaders>);
68
+ /**
69
+ * Fully qualified contract name.
70
+ */
71
+ get name(): string;
72
+ /**
73
+ * Resource namespace used for query-key grouping.
74
+ */
75
+ get namespace(): string | null;
76
+ /**
77
+ * Contract name without the resource namespace.
78
+ */
79
+ get localName(): string;
80
+ /**
81
+ * Build a namespace-level query key for resource-wide cache operations.
82
+ */
83
+ namespaceKey(): ContractQueryNamespaceKey;
84
+ /**
85
+ * Build a contract-level query key without path/query params.
86
+ */
87
+ contractKey(): ContractQueryContractKey;
88
+ /**
89
+ * Build a stable query key for this contract
90
+ *
91
+ * Only includes path/query in the key when they have defined values.
92
+ * This ensures consistent serialization between server (dehydrate) and
93
+ * client (hydrate), since undefined object values may serialize
94
+ * differently across React's RSC boundary vs JSON.stringify.
95
+ */
96
+ key(params?: {
97
+ path?: unknown;
98
+ query?: unknown;
99
+ body?: unknown;
100
+ }): ContractQueryKey;
101
+ /**
102
+ * Create query options for TanStack Query
103
+ * This can be passed to useQuery, prefetchQuery, fetchQuery, etc.
104
+ */
105
+ queryOptions(...callArgs: ContractQueryOptionsCallArgs<TContract, TProvidedHeaders>): QueryOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, InferSuccessResponse<TContract>, QueryKey> & {
106
+ queryKey: QueryKey;
107
+ };
108
+ /**
109
+ * Create mutation options for TanStack Query
110
+ * This can be passed to useMutation
111
+ */
112
+ mutationOptions(args?: Omit<MutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>, "mutationFn">): MutationOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, EndpointCallArgs<TContract, TProvidedHeaders>, unknown>;
113
+ /**
114
+ * Create infinite query options for TanStack Query
115
+ * This can be passed to useInfiniteQuery
116
+ */
117
+ infiniteQueryOptions<TPageParam = unknown>(args: ContractInfiniteQueryOptions<TContract, TPageParam, TProvidedHeaders>): InfiniteQueryObserverOptions<InferSuccessResponse<TContract>, InferEndpointContractError<TContract>, InferSuccessResponse<TContract>, QueryKey, TPageParam>;
118
+ /**
119
+ * Access to the underlying endpoint
120
+ */
121
+ get endpoint(): Endpoint<TContract, TProvidedHeaders>;
122
+ }
123
+ /**
124
+ * Create React Query adapter for a client
125
+ */
126
+ export declare function createReactQuery<TProvidedHeaders extends string = never>(client: Client<TProvidedHeaders>): <TContractLike extends ContractLike>(contract: TContractLike) => ReactQueryContractHelper<ResolveContract<TContractLike>, TProvidedHeaders>;
127
+ export {};
128
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,QAAQ,EACR,gBAAgB,EAChB,0BAA0B,EAC1B,oBAAoB,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EACV,4BAA4B,EAC5B,eAAe,EACf,QAAQ,EACR,YAAY,EACb,MAAM,uBAAuB,CAAC;AAE/B;;GAEG;AACH,KAAK,eAAe,CAAC,CAAC,IAAI;KACvB,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC;CACvE,CAAC,MAAM,CAAC,CAAC,CAAC;AAEX,KAAK,iBAAiB,CACpB,SAAS,SAAS,kBAAkB,EACpC,gBAAgB,SAAS,MAAM,IAC7B,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC,CAAC;AAE9E,KAAK,wBAAwB,CAAC,SAAS,SAAS,kBAAkB,IAAI,IAAI,CACxE,YAAY,CACV,oBAAoB,CAAC,SAAS,CAAC,EAC/B,0BAA0B,CAAC,SAAS,CAAC,EACrC,oBAAoB,CAAC,SAAS,CAAC,EAC/B,QAAQ,CACT,EACD,UAAU,GAAG,SAAS,CACvB,CAAC;AAEF,MAAM,MAAM,uBAAuB,CACjC,SAAS,SAAS,kBAAkB,EACpC,gBAAgB,SAAS,MAAM,GAAG,KAAK,IACrC,iBAAiB,CAAC,SAAS,EAAE,gBAAgB,CAAC,GAAG;IACnD,GAAG,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC1B,GAAG,wBAAwB,CAAC,SAAS,CAAC,CAAC;AAExC,KAAK,4BAA4B,CAC/B,SAAS,SAAS,kBAAkB,EACpC,gBAAgB,SAAS,MAAM,IAE/B,eAAe,CAAC,iBAAiB,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAK,GACzE,CAAC,IAAI,CAAC,EAAE,uBAAuB,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,GAC7D,CAAC,IAAI,EAAE,uBAAuB,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,0BAA0B,CACpC,SAAS,SAAS,kBAAkB,EACpC,gBAAgB,SAAS,MAAM,GAAG,KAAK,IACrC,IAAI,CACN,eAAe,CACb,oBAAoB,CAAC,SAAS,CAAC,EAC/B,0BAA0B,CAAC,SAAS,CAAC,EACrC,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAC7C,OAAO,CACR,EACD,YAAY,CACb,CAAC;AAEF,KAAK,2BAA2B,CAC9B,SAAS,SAAS,kBAAkB,EACpC,gBAAgB,SAAS,MAAM,GAAG,KAAK,IACrC;IACF,IAAI,CAAC,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,SAAS,CAAC,CAAC;CACpE,CAAC;AAEF,KAAK,qBAAqB,CACxB,SAAS,SAAS,kBAAkB,EACpC,UAAU,EACV,gBAAgB,SAAS,MAAM,IAC7B;IACF,IAAI,CAAC,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,SAAS,CAAC,CAAC;IACnE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE;QACX,SAAS,EAAE,UAAU,CAAC;KACvB,KAAK,2BAA2B,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IAC/D,GAAG,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,KAAK,CAAC;CAChB,CAAC;AAEF,KAAK,wBAAwB,CAC3B,SAAS,SAAS,kBAAkB,EACpC,UAAU,EACV,gBAAgB,SAAS,MAAM,IAC7B;IACF,MAAM,EAAE,CAAC,GAAG,EAAE;QACZ,SAAS,EAAE,UAAU,CAAC;KACvB,KAAK,2BAA2B,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;IAC/D,GAAG,EAAE,SAAS,OAAO,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,IAAI,CAAC,EAAE,KAAK,CAAC;CACd,CAAC;AAEF,KAAK,4BAA4B,CAC/B,SAAS,SAAS,kBAAkB,EACpC,UAAU,EACV,gBAAgB,SAAS,MAAM,IAC7B,CACA,qBAAqB,CAAC,SAAS,EAAE,UAAU,EAAE,gBAAgB,CAAC,GAC9D,wBAAwB,CAAC,SAAS,EAAE,UAAU,EAAE,gBAAgB,CAAC,CACpE,GACC,IAAI,CACF,4BAA4B,CAC1B,oBAAoB,CAAC,SAAS,CAAC,EAC/B,0BAA0B,CAAC,SAAS,CAAC,EACrC,oBAAoB,CAAC,SAAS,CAAC,EAC/B,QAAQ,EACR,UAAU,CACX,EACD,UAAU,GAAG,SAAS,CACvB,CAAC;AA6BJ,QAAA,MAAM,uBAAuB,YAAY,CAAC;AAG1C,MAAM,MAAM,yBAAyB,GAAG,SAAS;IAC/C,OAAO,uBAAuB;IAC9B,MAAM,GAAG,IAAI;CACd,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,SAAS;IAC9C,OAAO,uBAAuB;IAC9B,MAAM,GAAG,IAAI;IACb,MAAM;CACP,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,SAAS;IACtC,OAAO,uBAAuB;IAC9B,MAAM,GAAG,IAAI;IACb,MAAM;IACN,OAAO,CAAC;CACT,CAAC;AAEF;;GAEG;AACH,qBAAa,wBAAwB,CACnC,SAAS,SAAS,kBAAkB,EACpC,gBAAgB,SAAS,MAAM,GAAG,KAAK;IAGrC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;gBADT,QAAQ,EAAE,SAAS,EACnB,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;IAG1D;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,GAAG,IAAI,CAE7B;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;OAEG;IACH,YAAY,IAAI,yBAAyB;IAIzC;;OAEG;IACH,WAAW,IAAI,wBAAwB;IAIvC;;;;;;;OAOG;IACH,GAAG,CAAC,MAAM,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,GAAG,gBAAgB;IAgBpB;;;OAGG;IACH,YAAY,CACV,GAAG,QAAQ,EAAE,4BAA4B,CAAC,SAAS,EAAE,gBAAgB,CAAC,GACrE,YAAY,CACb,oBAAoB,CAAC,SAAS,CAAC,EAC/B,0BAA0B,CAAC,SAAS,CAAC,EACrC,oBAAoB,CAAC,SAAS,CAAC,EAC/B,QAAQ,CACT,GAAG;QAAE,QAAQ,EAAE,QAAQ,CAAA;KAAE;IA+B1B;;;OAGG;IACH,eAAe,CACb,IAAI,GAAE,IAAI,CACR,eAAe,CACb,oBAAoB,CAAC,SAAS,CAAC,EAC/B,0BAA0B,CAAC,SAAS,CAAC,EACrC,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAC7C,OAAO,CACR,EACD,YAAY,CACR,GACL,eAAe,CAChB,oBAAoB,CAAC,SAAS,CAAC,EAC/B,0BAA0B,CAAC,SAAS,CAAC,EACrC,gBAAgB,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAC7C,OAAO,CACR;IAkBD;;;OAGG;IACH,oBAAoB,CAAC,UAAU,GAAG,OAAO,EACvC,IAAI,EAAE,4BAA4B,CAAC,SAAS,EAAE,UAAU,EAAE,gBAAgB,CAAC,GAC1E,4BAA4B,CAC7B,oBAAoB,CAAC,SAAS,CAAC,EAC/B,0BAA0B,CAAC,SAAS,CAAC,EACrC,oBAAoB,CAAC,SAAS,CAAC,EAC/B,QAAQ,EACR,UAAU,CACX;IA4DD;;OAEG;IACH,IAAI,QAAQ,IAAI,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAEpD;CACF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,gBAAgB,SAAS,MAAM,GAAG,KAAK,EACtE,MAAM,EAAE,MAAM,CAAC,gBAAgB,CAAC,IAEb,aAAa,SAAS,YAAY,EACnD,UAAU,aAAa,KACtB,wBAAwB,CACzB,eAAe,CAAC,aAAa,CAAC,EAC9B,gBAAgB,CACjB,CAQF"}
package/dist/index.js ADDED
@@ -0,0 +1,190 @@
1
+ import { resolveContract, } from "@beignet/core/contracts";
2
+ function mergeParamObjects(base, patch) {
3
+ if (base == null)
4
+ return patch;
5
+ if (patch == null)
6
+ return base;
7
+ return {
8
+ ...base,
9
+ ...patch,
10
+ };
11
+ }
12
+ function normalizeKeyValue(value) {
13
+ if (value === undefined || value === null) {
14
+ return undefined;
15
+ }
16
+ if (typeof value !== "object" || Array.isArray(value)) {
17
+ return value;
18
+ }
19
+ const entries = Object.entries(value)
20
+ .filter(([, entryValue]) => entryValue !== undefined && entryValue !== null)
21
+ .sort(([a], [b]) => a.localeCompare(b));
22
+ if (entries.length === 0)
23
+ return undefined;
24
+ return Object.fromEntries(entries);
25
+ }
26
+ const BEIGNET_QUERY_KEY_SCOPE = "beignet";
27
+ const UNNAMESPACED_QUERY_KEY_NAMESPACE = null;
28
+ /**
29
+ * React Query contract helper
30
+ */
31
+ export class ReactQueryContractHelper {
32
+ contract;
33
+ _endpoint;
34
+ constructor(contract, _endpoint) {
35
+ this.contract = contract;
36
+ this._endpoint = _endpoint;
37
+ }
38
+ /**
39
+ * Fully qualified contract name.
40
+ */
41
+ get name() {
42
+ return this.contract.name;
43
+ }
44
+ /**
45
+ * Resource namespace used for query-key grouping.
46
+ */
47
+ get namespace() {
48
+ return this.contract.namespace ?? UNNAMESPACED_QUERY_KEY_NAMESPACE;
49
+ }
50
+ /**
51
+ * Contract name without the resource namespace.
52
+ */
53
+ get localName() {
54
+ return this.contract.localName ?? this.contract.name;
55
+ }
56
+ /**
57
+ * Build a namespace-level query key for resource-wide cache operations.
58
+ */
59
+ namespaceKey() {
60
+ return [BEIGNET_QUERY_KEY_SCOPE, this.namespace];
61
+ }
62
+ /**
63
+ * Build a contract-level query key without path/query params.
64
+ */
65
+ contractKey() {
66
+ return [BEIGNET_QUERY_KEY_SCOPE, this.namespace, this.localName];
67
+ }
68
+ /**
69
+ * Build a stable query key for this contract
70
+ *
71
+ * Only includes path/query in the key when they have defined values.
72
+ * This ensures consistent serialization between server (dehydrate) and
73
+ * client (hydrate), since undefined object values may serialize
74
+ * differently across React's RSC boundary vs JSON.stringify.
75
+ */
76
+ key(params) {
77
+ const path = normalizeKeyValue(params?.path);
78
+ const query = normalizeKeyValue(params?.query);
79
+ const body = normalizeKeyValue(params?.body);
80
+ if (path === undefined && query === undefined && body === undefined) {
81
+ return this.contractKey();
82
+ }
83
+ const cleanParams = {};
84
+ if (path !== undefined)
85
+ cleanParams.path = path;
86
+ if (query !== undefined)
87
+ cleanParams.query = query;
88
+ if (body !== undefined)
89
+ cleanParams.body = body;
90
+ return [...this.contractKey(), cleanParams];
91
+ }
92
+ /**
93
+ * Create query options for TanStack Query
94
+ * This can be passed to useQuery, prefetchQuery, fetchQuery, etc.
95
+ */
96
+ queryOptions(...callArgs) {
97
+ const args = (callArgs[0] ?? {});
98
+ const { path, query, body, headers, key: customKey, ...rest } = args;
99
+ const queryKey = (customKey || this.key({ path, query, body }));
100
+ const queryFn = async (context) => {
101
+ return await this._endpoint.call({
102
+ path,
103
+ query,
104
+ body,
105
+ headers,
106
+ signal: context.signal,
107
+ });
108
+ };
109
+ return {
110
+ ...rest,
111
+ queryKey,
112
+ queryFn,
113
+ };
114
+ }
115
+ /**
116
+ * Create mutation options for TanStack Query
117
+ * This can be passed to useMutation
118
+ */
119
+ mutationOptions(args = {}) {
120
+ const mutationFn = async (vars) => {
121
+ return await this._endpoint.call(vars);
122
+ };
123
+ return {
124
+ ...args,
125
+ mutationFn,
126
+ };
127
+ }
128
+ /**
129
+ * Create infinite query options for TanStack Query
130
+ * This can be passed to useInfiniteQuery
131
+ */
132
+ infiniteQueryOptions(args) {
133
+ let queryKey;
134
+ let rest;
135
+ let resolveParams;
136
+ if ("params" in args && typeof args.params === "function") {
137
+ const { params, key, ...other } = args;
138
+ if (!key) {
139
+ throw new Error("infiniteQueryOptions({ params }) requires a custom key. Use static path/query for the cache key, or pass key explicitly.");
140
+ }
141
+ queryKey = key;
142
+ rest = other;
143
+ resolveParams = params;
144
+ }
145
+ else {
146
+ const { path, query, headers, page, key, ...other } = args;
147
+ queryKey = (key || this.key({ path, query }));
148
+ rest = other;
149
+ resolveParams = (context) => {
150
+ const pageParams = page?.(context);
151
+ return {
152
+ path: mergeParamObjects(path, pageParams?.path),
153
+ query: mergeParamObjects(query, pageParams?.query),
154
+ headers: mergeParamObjects(headers, pageParams?.headers),
155
+ };
156
+ };
157
+ }
158
+ const queryFn = async (context) => {
159
+ const resolvedParams = resolveParams(context);
160
+ return await this._endpoint.call({
161
+ path: resolvedParams.path,
162
+ query: resolvedParams.query,
163
+ headers: resolvedParams.headers,
164
+ signal: context.signal,
165
+ });
166
+ };
167
+ return {
168
+ ...rest,
169
+ queryKey,
170
+ queryFn,
171
+ };
172
+ }
173
+ /**
174
+ * Access to the underlying endpoint
175
+ */
176
+ get endpoint() {
177
+ return this._endpoint;
178
+ }
179
+ }
180
+ /**
181
+ * Create React Query adapter for a client
182
+ */
183
+ export function createReactQuery(client) {
184
+ return function rq(contract) {
185
+ const resolved = resolveContract(contract);
186
+ const endpoint = client.endpoint(contract);
187
+ return new ReactQueryContractHelper(resolved, endpoint);
188
+ };
189
+ }
190
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@beignet/react-query",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "TanStack Query integration for Beignet",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/*.test-d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "clean": "rm -rf dist coverage .turbo",
27
+ "test": "bun test",
28
+ "test:coverage": "bun test --coverage",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "contract",
33
+ "api",
34
+ "typescript",
35
+ "react-query",
36
+ "tanstack"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/taylorbryant/beignet.git",
42
+ "directory": "packages/react-query"
43
+ },
44
+ "author": "Taylor Bryant",
45
+ "homepage": "https://github.com/taylorbryant/beignet#readme",
46
+ "bugs": "https://github.com/taylorbryant/beignet/issues",
47
+ "sideEffects": false,
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@tanstack/react-query": "^5.0.0",
56
+ "react": "^18.0.0 || ^19.0.0"
57
+ },
58
+ "dependencies": {
59
+ "@beignet/core": "*"
60
+ },
61
+ "devDependencies": {
62
+ "@tanstack/react-query": "^5.0.0",
63
+ "@testing-library/dom": "^9.3.4",
64
+ "@testing-library/react": "^14.3.1",
65
+ "@types/bun": "^1.3.13",
66
+ "@types/node": "^20.10.0",
67
+ "@types/react": "^18.2.0",
68
+ "happy-dom": "^20.0.11",
69
+ "react": "^18.2.0",
70
+ "typescript": "^5.3.0",
71
+ "zod": "^4.0.0"
72
+ }
73
+ }
package/src/index.ts ADDED
@@ -0,0 +1,433 @@
1
+ import type {
2
+ Client,
3
+ Endpoint,
4
+ EndpointCallArgs,
5
+ InferEndpointContractError,
6
+ InferSuccessResponse,
7
+ } from "@beignet/core/client";
8
+ import {
9
+ type ContractLike,
10
+ type HttpContractConfig,
11
+ type ResolveContract,
12
+ resolveContract,
13
+ } from "@beignet/core/contracts";
14
+
15
+ import type {
16
+ InfiniteQueryObserverOptions,
17
+ MutationOptions,
18
+ QueryKey,
19
+ QueryOptions,
20
+ } from "@tanstack/react-query";
21
+
22
+ /**
23
+ * Options for useQuery hook
24
+ */
25
+ type HasRequiredKeys<T> = {
26
+ [K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
27
+ }[keyof T];
28
+
29
+ type EndpointQueryArgs<
30
+ TContract extends HttpContractConfig,
31
+ TProvidedHeaders extends string,
32
+ > = Omit<EndpointCallArgs<TContract, TProvidedHeaders>, "rawBody" | "signal">;
33
+
34
+ type ContractQueryOptionsBase<TContract extends HttpContractConfig> = Omit<
35
+ QueryOptions<
36
+ InferSuccessResponse<TContract>,
37
+ InferEndpointContractError<TContract>,
38
+ InferSuccessResponse<TContract>,
39
+ QueryKey
40
+ >,
41
+ "queryKey" | "queryFn"
42
+ >;
43
+
44
+ export type ContractUseQueryOptions<
45
+ TContract extends HttpContractConfig,
46
+ TProvidedHeaders extends string = never,
47
+ > = EndpointQueryArgs<TContract, TProvidedHeaders> & {
48
+ key?: readonly unknown[];
49
+ } & ContractQueryOptionsBase<TContract>;
50
+
51
+ type ContractQueryOptionsCallArgs<
52
+ TContract extends HttpContractConfig,
53
+ TProvidedHeaders extends string,
54
+ > =
55
+ HasRequiredKeys<EndpointQueryArgs<TContract, TProvidedHeaders>> extends never
56
+ ? [args?: ContractUseQueryOptions<TContract, TProvidedHeaders>]
57
+ : [args: ContractUseQueryOptions<TContract, TProvidedHeaders>];
58
+
59
+ /**
60
+ * Options for useMutation hook
61
+ */
62
+ export type ContractUseMutationOptions<
63
+ TContract extends HttpContractConfig,
64
+ TProvidedHeaders extends string = never,
65
+ > = Omit<
66
+ MutationOptions<
67
+ InferSuccessResponse<TContract>,
68
+ InferEndpointContractError<TContract>,
69
+ EndpointCallArgs<TContract, TProvidedHeaders>,
70
+ unknown
71
+ >,
72
+ "mutationFn"
73
+ >;
74
+
75
+ type InfiniteQueryResolvedParams<
76
+ TContract extends HttpContractConfig,
77
+ TProvidedHeaders extends string = never,
78
+ > = {
79
+ path?: EndpointCallArgs<TContract>["path"];
80
+ query?: EndpointCallArgs<TContract>["query"];
81
+ headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
82
+ };
83
+
84
+ type SafeInfiniteQueryArgs<
85
+ TContract extends HttpContractConfig,
86
+ TPageParam,
87
+ TProvidedHeaders extends string,
88
+ > = {
89
+ path?: EndpointCallArgs<TContract>["path"];
90
+ query?: EndpointCallArgs<TContract>["query"];
91
+ headers?: EndpointCallArgs<TContract, TProvidedHeaders>["headers"];
92
+ page?: (ctx: {
93
+ pageParam: TPageParam;
94
+ }) => InfiniteQueryResolvedParams<TContract, TProvidedHeaders>;
95
+ key?: readonly unknown[];
96
+ params?: never;
97
+ };
98
+
99
+ type DynamicInfiniteQueryArgs<
100
+ TContract extends HttpContractConfig,
101
+ TPageParam,
102
+ TProvidedHeaders extends string,
103
+ > = {
104
+ params: (ctx: {
105
+ pageParam: TPageParam;
106
+ }) => InfiniteQueryResolvedParams<TContract, TProvidedHeaders>;
107
+ key: readonly unknown[];
108
+ path?: never;
109
+ query?: never;
110
+ page?: never;
111
+ };
112
+
113
+ type ContractInfiniteQueryOptions<
114
+ TContract extends HttpContractConfig,
115
+ TPageParam,
116
+ TProvidedHeaders extends string,
117
+ > = (
118
+ | SafeInfiniteQueryArgs<TContract, TPageParam, TProvidedHeaders>
119
+ | DynamicInfiniteQueryArgs<TContract, TPageParam, TProvidedHeaders>
120
+ ) &
121
+ Omit<
122
+ InfiniteQueryObserverOptions<
123
+ InferSuccessResponse<TContract>,
124
+ InferEndpointContractError<TContract>,
125
+ InferSuccessResponse<TContract>,
126
+ QueryKey,
127
+ TPageParam
128
+ >,
129
+ "queryKey" | "queryFn"
130
+ >;
131
+
132
+ function mergeParamObjects<T>(base?: T, patch?: T): T | undefined {
133
+ if (base == null) return patch;
134
+ if (patch == null) return base;
135
+
136
+ return {
137
+ ...(base as Record<string, unknown>),
138
+ ...(patch as Record<string, unknown>),
139
+ } as T;
140
+ }
141
+
142
+ function normalizeKeyValue(value: unknown): unknown | undefined {
143
+ if (value === undefined || value === null) {
144
+ return undefined;
145
+ }
146
+
147
+ if (typeof value !== "object" || Array.isArray(value)) {
148
+ return value;
149
+ }
150
+
151
+ const entries = Object.entries(value as Record<string, unknown>)
152
+ .filter(([, entryValue]) => entryValue !== undefined && entryValue !== null)
153
+ .sort(([a], [b]) => a.localeCompare(b));
154
+
155
+ if (entries.length === 0) return undefined;
156
+ return Object.fromEntries(entries);
157
+ }
158
+
159
+ const BEIGNET_QUERY_KEY_SCOPE = "beignet";
160
+ const UNNAMESPACED_QUERY_KEY_NAMESPACE = null;
161
+
162
+ export type ContractQueryNamespaceKey = readonly [
163
+ typeof BEIGNET_QUERY_KEY_SCOPE,
164
+ string | null,
165
+ ];
166
+
167
+ export type ContractQueryContractKey = readonly [
168
+ typeof BEIGNET_QUERY_KEY_SCOPE,
169
+ string | null,
170
+ string,
171
+ ];
172
+
173
+ export type ContractQueryKey = readonly [
174
+ typeof BEIGNET_QUERY_KEY_SCOPE,
175
+ string | null,
176
+ string,
177
+ unknown?,
178
+ ];
179
+
180
+ /**
181
+ * React Query contract helper
182
+ */
183
+ export class ReactQueryContractHelper<
184
+ TContract extends HttpContractConfig,
185
+ TProvidedHeaders extends string = never,
186
+ > {
187
+ constructor(
188
+ private contract: TContract,
189
+ private _endpoint: Endpoint<TContract, TProvidedHeaders>,
190
+ ) {}
191
+
192
+ /**
193
+ * Fully qualified contract name.
194
+ */
195
+ get name(): string {
196
+ return this.contract.name;
197
+ }
198
+
199
+ /**
200
+ * Resource namespace used for query-key grouping.
201
+ */
202
+ get namespace(): string | null {
203
+ return this.contract.namespace ?? UNNAMESPACED_QUERY_KEY_NAMESPACE;
204
+ }
205
+
206
+ /**
207
+ * Contract name without the resource namespace.
208
+ */
209
+ get localName(): string {
210
+ return this.contract.localName ?? this.contract.name;
211
+ }
212
+
213
+ /**
214
+ * Build a namespace-level query key for resource-wide cache operations.
215
+ */
216
+ namespaceKey(): ContractQueryNamespaceKey {
217
+ return [BEIGNET_QUERY_KEY_SCOPE, this.namespace] as const;
218
+ }
219
+
220
+ /**
221
+ * Build a contract-level query key without path/query params.
222
+ */
223
+ contractKey(): ContractQueryContractKey {
224
+ return [BEIGNET_QUERY_KEY_SCOPE, this.namespace, this.localName] as const;
225
+ }
226
+
227
+ /**
228
+ * Build a stable query key for this contract
229
+ *
230
+ * Only includes path/query in the key when they have defined values.
231
+ * This ensures consistent serialization between server (dehydrate) and
232
+ * client (hydrate), since undefined object values may serialize
233
+ * differently across React's RSC boundary vs JSON.stringify.
234
+ */
235
+ key(params?: {
236
+ path?: unknown;
237
+ query?: unknown;
238
+ body?: unknown;
239
+ }): ContractQueryKey {
240
+ const path = normalizeKeyValue(params?.path);
241
+ const query = normalizeKeyValue(params?.query);
242
+ const body = normalizeKeyValue(params?.body);
243
+
244
+ if (path === undefined && query === undefined && body === undefined) {
245
+ return this.contractKey();
246
+ }
247
+
248
+ const cleanParams: Record<string, unknown> = {};
249
+ if (path !== undefined) cleanParams.path = path;
250
+ if (query !== undefined) cleanParams.query = query;
251
+ if (body !== undefined) cleanParams.body = body;
252
+ return [...this.contractKey(), cleanParams] as const;
253
+ }
254
+
255
+ /**
256
+ * Create query options for TanStack Query
257
+ * This can be passed to useQuery, prefetchQuery, fetchQuery, etc.
258
+ */
259
+ queryOptions(
260
+ ...callArgs: ContractQueryOptionsCallArgs<TContract, TProvidedHeaders>
261
+ ): QueryOptions<
262
+ InferSuccessResponse<TContract>,
263
+ InferEndpointContractError<TContract>,
264
+ InferSuccessResponse<TContract>,
265
+ QueryKey
266
+ > & { queryKey: QueryKey } {
267
+ const args = (callArgs[0] ?? {}) as ContractUseQueryOptions<
268
+ TContract,
269
+ TProvidedHeaders
270
+ >;
271
+ const { path, query, body, headers, key: customKey, ...rest } = args;
272
+
273
+ const queryKey = (customKey || this.key({ path, query, body })) as QueryKey;
274
+
275
+ const queryFn = async (context: { signal?: AbortSignal }) => {
276
+ return await this._endpoint.call({
277
+ path,
278
+ query,
279
+ body,
280
+ headers,
281
+ signal: context.signal,
282
+ } as unknown as EndpointCallArgs<TContract, TProvidedHeaders>);
283
+ };
284
+
285
+ return {
286
+ ...rest,
287
+ queryKey,
288
+ queryFn,
289
+ } as QueryOptions<
290
+ InferSuccessResponse<TContract>,
291
+ InferEndpointContractError<TContract>,
292
+ InferSuccessResponse<TContract>,
293
+ QueryKey
294
+ > & { queryKey: QueryKey };
295
+ }
296
+
297
+ /**
298
+ * Create mutation options for TanStack Query
299
+ * This can be passed to useMutation
300
+ */
301
+ mutationOptions(
302
+ args: Omit<
303
+ MutationOptions<
304
+ InferSuccessResponse<TContract>,
305
+ InferEndpointContractError<TContract>,
306
+ EndpointCallArgs<TContract, TProvidedHeaders>,
307
+ unknown
308
+ >,
309
+ "mutationFn"
310
+ > = {},
311
+ ): MutationOptions<
312
+ InferSuccessResponse<TContract>,
313
+ InferEndpointContractError<TContract>,
314
+ EndpointCallArgs<TContract, TProvidedHeaders>,
315
+ unknown
316
+ > {
317
+ const mutationFn = async (
318
+ vars: EndpointCallArgs<TContract, TProvidedHeaders>,
319
+ ) => {
320
+ return await this._endpoint.call(vars);
321
+ };
322
+
323
+ return {
324
+ ...args,
325
+ mutationFn,
326
+ } as MutationOptions<
327
+ InferSuccessResponse<TContract>,
328
+ InferEndpointContractError<TContract>,
329
+ EndpointCallArgs<TContract, TProvidedHeaders>,
330
+ unknown
331
+ >;
332
+ }
333
+
334
+ /**
335
+ * Create infinite query options for TanStack Query
336
+ * This can be passed to useInfiniteQuery
337
+ */
338
+ infiniteQueryOptions<TPageParam = unknown>(
339
+ args: ContractInfiniteQueryOptions<TContract, TPageParam, TProvidedHeaders>,
340
+ ): InfiniteQueryObserverOptions<
341
+ InferSuccessResponse<TContract>,
342
+ InferEndpointContractError<TContract>,
343
+ InferSuccessResponse<TContract>,
344
+ QueryKey,
345
+ TPageParam
346
+ > {
347
+ let queryKey: QueryKey;
348
+ let rest: Omit<
349
+ ContractInfiniteQueryOptions<TContract, TPageParam, TProvidedHeaders>,
350
+ "path" | "query" | "headers" | "page" | "params" | "key"
351
+ >;
352
+ let resolveParams: (context: {
353
+ pageParam: TPageParam;
354
+ }) => InfiniteQueryResolvedParams<TContract, TProvidedHeaders>;
355
+
356
+ if ("params" in args && typeof args.params === "function") {
357
+ const { params, key, ...other } = args;
358
+ if (!key) {
359
+ throw new Error(
360
+ "infiniteQueryOptions({ params }) requires a custom key. Use static path/query for the cache key, or pass key explicitly.",
361
+ );
362
+ }
363
+ queryKey = key as QueryKey;
364
+ rest = other;
365
+ resolveParams = params;
366
+ } else {
367
+ const { path, query, headers, page, key, ...other } = args;
368
+ queryKey = (key || this.key({ path, query })) as QueryKey;
369
+ rest = other;
370
+ resolveParams = (context) => {
371
+ const pageParams = page?.(context);
372
+ return {
373
+ path: mergeParamObjects(path, pageParams?.path),
374
+ query: mergeParamObjects(query, pageParams?.query),
375
+ headers: mergeParamObjects(headers, pageParams?.headers),
376
+ };
377
+ };
378
+ }
379
+
380
+ const queryFn = async (context: {
381
+ pageParam: TPageParam;
382
+ signal?: AbortSignal;
383
+ }) => {
384
+ const resolvedParams = resolveParams(context);
385
+ return await this._endpoint.call({
386
+ path: resolvedParams.path,
387
+ query: resolvedParams.query,
388
+ headers: resolvedParams.headers,
389
+ signal: context.signal,
390
+ } as unknown as EndpointCallArgs<TContract, TProvidedHeaders>);
391
+ };
392
+
393
+ return {
394
+ ...rest,
395
+ queryKey,
396
+ queryFn,
397
+ } as unknown as InfiniteQueryObserverOptions<
398
+ InferSuccessResponse<TContract>,
399
+ InferEndpointContractError<TContract>,
400
+ InferSuccessResponse<TContract>,
401
+ QueryKey,
402
+ TPageParam
403
+ >;
404
+ }
405
+
406
+ /**
407
+ * Access to the underlying endpoint
408
+ */
409
+ get endpoint(): Endpoint<TContract, TProvidedHeaders> {
410
+ return this._endpoint;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Create React Query adapter for a client
416
+ */
417
+ export function createReactQuery<TProvidedHeaders extends string = never>(
418
+ client: Client<TProvidedHeaders>,
419
+ ) {
420
+ return function rq<TContractLike extends ContractLike>(
421
+ contract: TContractLike,
422
+ ): ReactQueryContractHelper<
423
+ ResolveContract<TContractLike>,
424
+ TProvidedHeaders
425
+ > {
426
+ const resolved = resolveContract(contract);
427
+ const endpoint = client.endpoint(contract) as Endpoint<
428
+ ResolveContract<TContractLike>,
429
+ TProvidedHeaders
430
+ >;
431
+ return new ReactQueryContractHelper(resolved, endpoint);
432
+ };
433
+ }