@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 +5 -0
- package/README.md +407 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +190 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/index.ts +433 -0
package/CHANGELOG.md
ADDED
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
|
package/dist/index.d.ts
ADDED
|
@@ -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":"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"}
|
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
|
+
}
|