@shipworthy/openapi-react-query 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +458 -2
- package/dist/client.d.ts +2 -0
- package/dist/client.js +7 -0
- package/dist/hooks/use-infinite-query.d.ts +13 -0
- package/dist/hooks/use-infinite-query.js +36 -0
- package/dist/hooks/use-mutation.d.ts +8 -0
- package/dist/hooks/use-mutation.js +23 -0
- package/dist/hooks/use-query.d.ts +20 -0
- package/dist/hooks/use-query.js +31 -0
- package/dist/hooks/use-suspense-query.d.ts +10 -0
- package/dist/hooks/use-suspense-query.js +16 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/provider.d.ts +8 -0
- package/dist/provider.js +28 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +26 -0
- package/package.json +43 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shipworthy AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,459 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shipworthy/openapi-react-query
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed [TanStack Query](https://tanstack.com/query) hooks for React, driven by the `paths` type
|
|
4
|
+
generated from an OpenAPI document. You pass your generated `paths` interface once, then call
|
|
5
|
+
hooks with a URL path such as `"/pets/{pet_id}"`. TypeScript infers the request parameters,
|
|
6
|
+
request body, response data, and error body for that operation. No per-operation code is
|
|
7
|
+
generated. Requests go through
|
|
8
|
+
[`@shipworthy/openapi-fetch`](../openapi-fetch), which this package installs for you. You
|
|
9
|
+
generate the `paths` type with `openapi generate typescript` from `@shipworthy/openapi-cli` and
|
|
10
|
+
`@shipworthy/openapi-typescript`.
|
|
11
|
+
|
|
12
|
+
Use this package when you want path-based hooks with a ready-made provider. Use the sibling
|
|
13
|
+
`@shipworthy/openapi-tanstack-query` instead when you want generated, operation-ID-based query
|
|
14
|
+
utilities that work with TanStack's own `useQuery`.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bun add @shipworthy/openapi-react-query @tanstack/react-query react
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
To generate types, also install the CLI, the TypeScript generator, and the generator's peers as dev dependencies:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript effect@^4
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Requirement | Value |
|
|
29
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| Peer `@tanstack/react-query` | `^5.104.1` |
|
|
31
|
+
| Peer `react` | `^18.0.0 \|\| ^19.0.0` |
|
|
32
|
+
| Dependency | `@shipworthy/openapi-fetch` (installed automatically) |
|
|
33
|
+
| Module format | ESM only (`"type": "module"`). There is no CommonJS build. |
|
|
34
|
+
| Shipped files | Compiled JavaScript and `.d.ts` declarations in `dist/` (`exports["."]` and the `react-native` field point to `./dist/index.js`). |
|
|
35
|
+
| CLI runtime | The `openapi` binary runs on Node.js `>=22.19.0` or Bun (`bunx --bun @shipworthy/openapi-cli`) |
|
|
36
|
+
|
|
37
|
+
No TypeScript or TSX transpilation of `node_modules` is needed. The provider's JSX is compiled
|
|
38
|
+
with the automatic runtime (`react/jsx-runtime`), so there is no `import React`. The package
|
|
39
|
+
declares no Node.js `engines` range. It uses the Fetch API globals supplied by your browser,
|
|
40
|
+
React Native environment, or server runtime. The Node.js requirement above applies to code generation.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
### 1. Generate the `paths` type
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
bunx --bun @shipworthy/openapi-cli generate typescript ./openapi.yaml -o ./src/generated/api.ts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The input can be a local JSON or YAML file or a URL. If you omit the input, the CLI reads the
|
|
51
|
+
schema from stdin. If you omit `-o`, it writes to stdout. The output exports `interface paths`,
|
|
52
|
+
along with `components`, `operations`, and related types.
|
|
53
|
+
|
|
54
|
+
### 2. Wrap your app in `ApiClientProvider`
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { ApiClientProvider } from "@shipworthy/openapi-react-query";
|
|
58
|
+
|
|
59
|
+
export function Root() {
|
|
60
|
+
return (
|
|
61
|
+
<ApiClientProvider baseUrl="https://api.example.com">
|
|
62
|
+
<PetList />
|
|
63
|
+
</ApiClientProvider>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`ApiClientProvider` also renders a TanStack `QueryClientProvider`, so you don't need a second
|
|
69
|
+
provider.
|
|
70
|
+
|
|
71
|
+
### 3. Bind your `paths` type and call the hooks
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useApiClientQuery } from "@shipworthy/openapi-react-query";
|
|
75
|
+
import type { paths } from "./generated/api";
|
|
76
|
+
|
|
77
|
+
function PetList() {
|
|
78
|
+
const useQuery = useApiClientQuery<paths>(); // bind the schema once
|
|
79
|
+
const { data, error, isPending } = useQuery("/pets", {
|
|
80
|
+
params: { query: { limit: 20 } },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (isPending) return <p>Loading…</p>;
|
|
84
|
+
if (error) return <p>{error.title}</p>; // typed from the operation's error response
|
|
85
|
+
return (
|
|
86
|
+
<ul>
|
|
87
|
+
{data.data.map((pet) => (
|
|
88
|
+
<li key={pet.id}>{pet.name}</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The examples below use this `paths` shape, abbreviated from `openapi-typescript` output:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
interface Pet {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
}
|
|
102
|
+
interface Problem {
|
|
103
|
+
title: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface paths {
|
|
107
|
+
"/pets": {
|
|
108
|
+
get: {
|
|
109
|
+
parameters: { query?: { limit?: number; cursor?: number } };
|
|
110
|
+
responses: {
|
|
111
|
+
200: { content: { "application/json": { data: Pet[]; next_cursor?: number } } };
|
|
112
|
+
400: { content: { "application/json": Problem } };
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
post: {
|
|
116
|
+
requestBody: { content: { "application/json": { name: string } } };
|
|
117
|
+
responses: {
|
|
118
|
+
201: { content: { "application/json": Pet } };
|
|
119
|
+
422: { content: { "application/json": Problem } };
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
"/pets/{pet_id}": {
|
|
124
|
+
get: {
|
|
125
|
+
parameters: { path: { pet_id: string } };
|
|
126
|
+
responses: {
|
|
127
|
+
200: { content: { "application/json": Pet } };
|
|
128
|
+
404: { content: { "application/json": Problem } };
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
delete: {
|
|
132
|
+
parameters: { path: { pet_id: string } };
|
|
133
|
+
responses: { 204: { content?: never } };
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## API
|
|
140
|
+
|
|
141
|
+
The package has a single entry point: `@shipworthy/openapi-react-query`.
|
|
142
|
+
|
|
143
|
+
| Export | Kind |
|
|
144
|
+
| --------------------------- | -------------------------- |
|
|
145
|
+
| `ApiClientProvider` | React component |
|
|
146
|
+
| `useApiClient` | Hook |
|
|
147
|
+
| `useApiClientQuery` | Hook factory |
|
|
148
|
+
| `useApiClientSuspenseQuery` | Hook factory |
|
|
149
|
+
| `useApiClientQueryOptions` | Hook factory |
|
|
150
|
+
| `useApiClientInfiniteQuery` | Hook factory |
|
|
151
|
+
| `useApiClientMutation` | Hook factory |
|
|
152
|
+
| `createApiClient` | Function (no React needed) |
|
|
153
|
+
| `ApiClient` | Type |
|
|
154
|
+
| `QueryKey` | Type |
|
|
155
|
+
|
|
156
|
+
### Calling the hook factories
|
|
157
|
+
|
|
158
|
+
Each `useApiClient*` export takes your `paths` type as a type argument and returns a hook
|
|
159
|
+
function:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const useQuery = useApiClientQuery<paths>();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Calling the factory runs no hooks. It returns the same function every time. The returned
|
|
166
|
+
function is a React hook: it reads the provider context and calls TanStack, so it must follow
|
|
167
|
+
the Rules of Hooks. If you don't pass `paths`, it defaults to `{}`, and no path type-checks.
|
|
168
|
+
|
|
169
|
+
The returned hooks take arguments in this order:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
hook(path, [method], init, options, queryClient)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Argument | Description |
|
|
176
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
177
|
+
| `path` | A key of `paths`, such as `"/pets/{pet_id}"`. Only paths that have the selected method are accepted. |
|
|
178
|
+
| `method` | Optional lowercase HTTP method: `"get" \| "put" \| "post" \| "delete" \| "options" \| "head" \| "patch" \| "trace"`. Query hooks default to `"get"`; the mutation hook defaults to `"post"`. The hook treats a string second argument as the method. |
|
|
179
|
+
| `init` | `openapi-fetch` request options: `params.path`, `params.query`, `params.header`, `params.cookie`, `body`, and similar. It is required when the operation has required parameters or a required body; otherwise it is optional. You can pass extra keys, which go to `openapi-fetch` unchanged. The mutation hook takes no `init` here; pass it to `mutate()` instead. |
|
|
180
|
+
| `options` | TanStack options for the underlying hook, minus `queryKey`/`queryFn` (or `mutationKey`/`mutationFn`). `select` is typed, and the result's `data` type follows its return type. |
|
|
181
|
+
| `queryClient` | Optional `QueryClient`. It is passed through to TanStack, and overrides the one from context. |
|
|
182
|
+
|
|
183
|
+
How results behave:
|
|
184
|
+
|
|
185
|
+
- **`data`** is the operation's success response body (the 2XX JSON).
|
|
186
|
+
- **`error`** is the parsed error response body (the non-2XX JSON) that `openapi-fetch` returns.
|
|
187
|
+
The hooks throw this body itself, not an `Error` instance. Network failures from `fetch`
|
|
188
|
+
propagate as they are.
|
|
189
|
+
- **Empty responses:** in the query and suspense hooks, a `204` response or a response with
|
|
190
|
+
`Content-Length: 0` resolves to `null` instead of `undefined`.
|
|
191
|
+
- **Cancellation:** query hooks pass TanStack's `AbortSignal` to the request as `signal`.
|
|
192
|
+
|
|
193
|
+
### `ApiClientProvider`
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
function ApiClientProvider(props: {
|
|
197
|
+
baseUrl: string;
|
|
198
|
+
queryClient?: QueryClient;
|
|
199
|
+
children?: React.ReactNode;
|
|
200
|
+
}): JSX.Element;
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The provider creates an `openapi-fetch` client for `baseUrl` and puts it in context. It also
|
|
204
|
+
renders `QueryClientProvider`.
|
|
205
|
+
|
|
206
|
+
If you don't pass `queryClient`, the provider uses a default `QueryClient` with
|
|
207
|
+
`defaultOptions.queries.staleTime = 60_000`. In the browser, one module-level instance is
|
|
208
|
+
created and reused. On the server (TanStack's `isServer`), it creates a new instance on every
|
|
209
|
+
render.
|
|
210
|
+
|
|
211
|
+
The provider takes no headers or auth token. See [Limitations](#limitations-and-caveats).
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
215
|
+
import { ApiClientProvider } from "@shipworthy/openapi-react-query";
|
|
216
|
+
|
|
217
|
+
const queryClient = new QueryClient();
|
|
218
|
+
|
|
219
|
+
<ApiClientProvider baseUrl={import.meta.env.VITE_API_URL} queryClient={queryClient}>
|
|
220
|
+
<App />
|
|
221
|
+
</ApiClientProvider>;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `useApiClient<Paths>()`
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
function useApiClient<Paths extends {} = {}>(): Client<Paths>; // Client from @shipworthy/openapi-fetch
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Returns the provider's `openapi-fetch` client, with methods `GET`, `POST`, `PUT`, `PATCH`,
|
|
231
|
+
`DELETE`, and so on. Use it for one-off imperative calls inside components.
|
|
232
|
+
|
|
233
|
+
If no `ApiClientProvider` is above the component, it throws
|
|
234
|
+
`Error("useApiClient must be used within ApiClientProvider")`. Every hook in this package
|
|
235
|
+
calls `useApiClient`, so they all throw this error outside the provider.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const client = useApiClient<paths>();
|
|
239
|
+
const { data, error, response } = await client.GET("/pets/{pet_id}", {
|
|
240
|
+
params: { path: { pet_id: "p1" } },
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `useApiClientQuery<Paths>()`
|
|
245
|
+
|
|
246
|
+
Returns a hook that wraps TanStack `useQuery` and returns `UseQueryResult<TData, TError>`.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const useQuery = useApiClientQuery<paths>();
|
|
250
|
+
|
|
251
|
+
// Path params are required, so the call needs init.
|
|
252
|
+
const pet = useQuery(
|
|
253
|
+
"/pets/{pet_id}",
|
|
254
|
+
{ params: { path: { pet_id: "p1" } } },
|
|
255
|
+
{ staleTime: 5_000 },
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// init is optional here, so pass undefined to reach options. select narrows the data type to string[].
|
|
259
|
+
const names = useQuery("/pets", undefined, { select: (res) => res.data.map((p) => p.name) });
|
|
260
|
+
|
|
261
|
+
// Explicit method
|
|
262
|
+
const same = useQuery("/pets/{pet_id}", "get", { params: { path: { pet_id: "p1" } } });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The query key is `[method, path]`, or `[method, path, init]` when you pass `init`. For example:
|
|
266
|
+
`["get", "/pets", { params: { query: { limit: 20 } } }]`.
|
|
267
|
+
|
|
268
|
+
### `useApiClientSuspenseQuery<Paths>()`
|
|
269
|
+
|
|
270
|
+
Same arguments as `useApiClientQuery`. It wraps TanStack `useSuspenseQuery`, so `data` is always
|
|
271
|
+
defined. Render the component inside a `<Suspense>` boundary, and catch errors with an error
|
|
272
|
+
boundary.
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
function PetDetail({ id }: { id: string }) {
|
|
276
|
+
const useSuspenseQuery = useApiClientSuspenseQuery<paths>();
|
|
277
|
+
const { data } = useSuspenseQuery("/pets/{pet_id}", { params: { path: { pet_id: id } } });
|
|
278
|
+
return <h1>{data.name}</h1>;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### `useApiClientQueryOptions<Paths>()`
|
|
283
|
+
|
|
284
|
+
Returns a hook that builds a TanStack options object (`{ queryKey, queryFn, ...options }`)
|
|
285
|
+
instead of running a query. It takes the same `path`, `[method]`, `init`, and `options`
|
|
286
|
+
arguments, but no `queryClient`. The returned `queryFn` is never `skipToken`.
|
|
287
|
+
|
|
288
|
+
Use the result with TanStack APIs: `useQuery`, `useQueries`, `queryClient.prefetchQuery`,
|
|
289
|
+
`fetchQuery`, `ensureQueryData`, and so on. It reads the client from context, so you must call
|
|
290
|
+
it inside a component under `ApiClientProvider`.
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
294
|
+
|
|
295
|
+
const queryOptions = useApiClientQueryOptions<paths>();
|
|
296
|
+
const options = queryOptions("/pets", { params: { query: { limit: 20 } } });
|
|
297
|
+
|
|
298
|
+
const result = useQuery(options); // result.data is typed
|
|
299
|
+
const queryClient = useQueryClient();
|
|
300
|
+
void queryClient.prefetchQuery(options);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### `useApiClientInfiniteQuery<Paths>()`
|
|
304
|
+
|
|
305
|
+
Wraps TanStack `useInfiniteQuery` for cursor- or offset-paginated endpoints. Its arguments
|
|
306
|
+
differ from the other query hooks: `init` and `options` are required.
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
hook(path, [method], init, options, queryClient?)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- `options` takes TanStack's infinite-query options, so `initialPageParam` and
|
|
313
|
+
`getNextPageParam` are required. It also takes `pageParamName?: string` (default
|
|
314
|
+
`"cursor"`).
|
|
315
|
+
- On each page request, the hook sets `params.query[pageParamName]` to the current page
|
|
316
|
+
param, merged with any query params in `init`. If the page param is `undefined`, the hook
|
|
317
|
+
sends `0`.
|
|
318
|
+
- `data` is TanStack's `InfiniteData<TData>`, an object with `pages` and `pageParams`.
|
|
319
|
+
- The query key is the same as for `useApiClientQuery`. It does not include the page param.
|
|
320
|
+
- Unlike the other query hooks, it applies no `204` → `null` handling.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const useInfiniteQuery = useApiClientInfiniteQuery<paths>();
|
|
324
|
+
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
|
|
325
|
+
"/pets",
|
|
326
|
+
{ params: { query: { limit: 20 } } },
|
|
327
|
+
{
|
|
328
|
+
initialPageParam: 0,
|
|
329
|
+
getNextPageParam: (lastPage) => lastPage.next_cursor, // undefined ends pagination
|
|
330
|
+
pageParamName: "cursor", // sent as ?cursor=<pageParam>
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
const pets = data?.pages.flatMap((page) => page.data);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### `useApiClientMutation<Paths>()`
|
|
337
|
+
|
|
338
|
+
Wraps TanStack `useMutation` and returns `UseMutationResult<TData, TError, TInit>`.
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
hook(path, [method], options?, queryClient?)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- The method defaults to `"post"`.
|
|
345
|
+
- You pass the request `init` (body, params) to `mutate()` or `mutateAsync()`, not to the hook.
|
|
346
|
+
- The mutation key is `[method, path]`.
|
|
347
|
+
- Options are TanStack mutation options minus `mutationKey` and `mutationFn`.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
351
|
+
|
|
352
|
+
const useMutation = useApiClientMutation<paths>();
|
|
353
|
+
const queryClient = useQueryClient();
|
|
354
|
+
|
|
355
|
+
const createPet = useMutation("/pets", {
|
|
356
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["get", "/pets"] }),
|
|
357
|
+
});
|
|
358
|
+
createPet.mutate({ body: { name: "Rex" } });
|
|
359
|
+
|
|
360
|
+
const deletePet = useMutation("/pets/{pet_id}", "delete");
|
|
361
|
+
deletePet.mutate({ params: { path: { pet_id: "p1" } } });
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### `createApiClient<Paths>(baseUrl, authToken?)`
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
function createApiClient<Paths extends {}>(baseUrl: string, authToken?: string): Client<Paths>;
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Creates a standalone `openapi-fetch` client. Use it outside React, for example in scripts,
|
|
371
|
+
server code, or loaders.
|
|
372
|
+
|
|
373
|
+
- If you pass `authToken`, every request sends `Authorization: Bearer <authToken>`.
|
|
374
|
+
- Methods return `{ data, error, response }` and do not throw on HTTP errors.
|
|
375
|
+
|
|
376
|
+
This client is separate from the one `ApiClientProvider` creates, and the hooks don't use it.
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { createApiClient } from "@shipworthy/openapi-react-query";
|
|
380
|
+
import type { paths } from "./generated/api";
|
|
381
|
+
|
|
382
|
+
const api = createApiClient<paths>("https://api.example.com", process.env.API_TOKEN);
|
|
383
|
+
const { data, error, response } = await api.GET("/pets", { params: { query: { limit: 10 } } });
|
|
384
|
+
if (error) console.error(response.status, error.title);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Types
|
|
388
|
+
|
|
389
|
+
- **`ApiClient`**: `ReturnType<typeof createApiClient>`, the client type with its `Paths`
|
|
390
|
+
parameter erased. For a client bound to your schema, annotate with
|
|
391
|
+
`ReturnType<typeof createApiClient<paths>>`.
|
|
392
|
+
- **`QueryKey<Paths, Method, Path, Init?>`**: the key that the query hooks build.
|
|
393
|
+
- It is `readonly [Method, Path]` when `Init` is `undefined`.
|
|
394
|
+
- Otherwise it is `readonly [Method, Path, Init]`.
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import type { QueryKey } from "@shipworthy/openapi-react-query";
|
|
398
|
+
type PetsKey = QueryKey<paths, "get", "/pets">;
|
|
399
|
+
const key: PetsKey = ["get", "/pets", { params: { query: { limit: 20 } } }];
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Cache keys and invalidation
|
|
403
|
+
|
|
404
|
+
| Hook | Key |
|
|
405
|
+
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
|
406
|
+
| Queries (`useApiClientQuery`, `useApiClientSuspenseQuery`, `useApiClientQueryOptions`, `useApiClientInfiniteQuery`) | `[method, path]` or `[method, path, init]` |
|
|
407
|
+
| Mutations | `[method, path]` |
|
|
408
|
+
|
|
409
|
+
The method is lowercase, and the path is the OpenAPI template (`"/pets/{pet_id}"`), not the
|
|
410
|
+
expanded URL. TanStack matches keys by prefix, so
|
|
411
|
+
`queryClient.invalidateQueries({ queryKey: ["get", "/pets"] })` invalidates every `GET /pets`
|
|
412
|
+
query, whatever its `init`.
|
|
413
|
+
|
|
414
|
+
## Limitations and caveats
|
|
415
|
+
|
|
416
|
+
- **No auth or headers from the provider.** `ApiClientProvider` accepts only `baseUrl` and
|
|
417
|
+
`queryClient`. To send headers, pass them per request in `init` (`params.header`, or extra
|
|
418
|
+
`openapi-fetch` options). `createApiClient`'s `authToken` affects only that standalone client.
|
|
419
|
+
- **The provider creates a new client on every render** of `ApiClientProvider`. Place it high
|
|
420
|
+
in the tree, where it rarely re-renders.
|
|
421
|
+
- **Thrown errors are response bodies,** not `Error` objects, so `error instanceof Error` is
|
|
422
|
+
`false` for HTTP errors.
|
|
423
|
+
- **OpenAPI support** depends on `@shipworthy/openapi-typescript`, which generates the `paths`
|
|
424
|
+
type. That package describes itself as converting OpenAPI 3.0 and 3.1 schemas.
|
|
425
|
+
|
|
426
|
+
## Troubleshooting
|
|
427
|
+
|
|
428
|
+
| Symptom | Cause / fix |
|
|
429
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
430
|
+
| `useApiClient must be used within ApiClientProvider` | A hook was called outside `ApiClientProvider`. Wrap the tree. |
|
|
431
|
+
| Every path is a type error | You called the factory without `paths` (`useApiClientQuery()`). Use `useApiClientQuery<paths>()`. |
|
|
432
|
+
| A path is not accepted | That path has no operation for the method. Pass the method explicitly, e.g. `useQuery("/x", "post", …)`. |
|
|
433
|
+
| Argument-count error when you omit `init` | The operation has required path, query, or header params, or a required body, so `init` is required. |
|
|
434
|
+
| Infinite query never sends a page | The API expects a query param other than `cursor`. Set `pageParamName`. |
|
|
435
|
+
|
|
436
|
+
## Key facts (for AI agents)
|
|
437
|
+
|
|
438
|
+
- Import everything from `@shipworthy/openapi-react-query`. There are no subpath exports and
|
|
439
|
+
no CLI in this package.
|
|
440
|
+
- Generate the `paths` type with
|
|
441
|
+
`openapi generate typescript <schema> -o <file>.ts` (`@shipworthy/openapi-cli` +
|
|
442
|
+
`@shipworthy/openapi-typescript`). Import it as `import type { paths } from "<file>"`.
|
|
443
|
+
- Mount `<ApiClientProvider baseUrl="…">` once. It already includes `QueryClientProvider`.
|
|
444
|
+
- The hook factories are curried. Call `useApiClientQuery<paths>()` first, and then call the
|
|
445
|
+
returned hook as `hook(path, method?, init, options?, queryClient?)`. Never call
|
|
446
|
+
`useApiClientQuery<paths>("/path", …)`.
|
|
447
|
+
- Methods are lowercase strings. Queries default to `"get"`; mutations default to `"post"`.
|
|
448
|
+
- Mutation `init` goes to `mutate(init)`, not to the hook.
|
|
449
|
+
- Infinite queries need `initialPageParam` and `getNextPageParam`. The page param is sent as
|
|
450
|
+
the `pageParamName` query param (default `cursor`).
|
|
451
|
+
- Query keys are `[method, path, init?]`; mutation keys are `[method, path]`.
|
|
452
|
+
- Hooks throw the typed error body. Query and suspense hooks resolve empty `204` responses as
|
|
453
|
+
`null`.
|
|
454
|
+
- `createApiClient(baseUrl, authToken?)` is for non-React use. The hooks don't use it.
|
|
455
|
+
- Peers: `react` `^18 || ^19`, `@tanstack/react-query` `^5.104.1`. ESM-only compiled JavaScript with `.d.ts` declarations.
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT. See [LICENSE](./LICENSE).
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type InfiniteData, type QueryClient, type UseInfiniteQueryOptions, type UseInfiniteQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import type { FetchResponse, HttpMethod, MaybeOptionalInit, MediaType, PathsWithMethod } from "@shipworthy/openapi-fetch";
|
|
3
|
+
import { type InferSelectReturnType, type InitWithUnknowns, type QueryKey } from "../utils.ts";
|
|
4
|
+
interface ApiClientInfiniteQuery<Paths extends Record<string, any>> {
|
|
5
|
+
<Path extends PathsWithMethod<Paths, "get">, Init extends MaybeOptionalInit<Paths[Path], "get">, Response extends Required<FetchResponse<Paths[Path]["get"], Init, MediaType>>, Options extends Omit<UseInfiniteQueryOptions<Response["data"], Response["error"], InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>, QueryKey<Paths, "get", Path>, unknown>, "queryKey" | "queryFn"> & {
|
|
6
|
+
pageParamName?: string;
|
|
7
|
+
}>(path: Path, init: InitWithUnknowns<Init>, options: Options, queryClient?: QueryClient): UseInfiniteQueryResult<InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>, Response["error"]>;
|
|
8
|
+
<Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method] & Record<string | number, any>, Init, MediaType>>, Options extends Omit<UseInfiniteQueryOptions<Response["data"], Response["error"], InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>, QueryKey<Paths, Method, Path>, unknown>, "queryKey" | "queryFn"> & {
|
|
9
|
+
pageParamName?: string;
|
|
10
|
+
}>(path: Path, method: Method, init: InitWithUnknowns<Init>, options: Options, queryClient?: QueryClient): UseInfiniteQueryResult<InferSelectReturnType<InfiniteData<Response["data"]>, Options["select"]>, Response["error"]>;
|
|
11
|
+
}
|
|
12
|
+
export declare function useApiClientInfiniteQuery<Paths extends Record<string, any> = {}>(): ApiClientInfiniteQuery<Paths>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useInfiniteQuery as useTanstackInfiniteQuery, } from "@tanstack/react-query";
|
|
2
|
+
import { useApiClient } from "../provider.js";
|
|
3
|
+
import { buildQueryKey, parseArgs, } from "../utils.js";
|
|
4
|
+
export function useApiClientInfiniteQuery() {
|
|
5
|
+
return useInfiniteQuery;
|
|
6
|
+
}
|
|
7
|
+
function useInfiniteQuery(path, ...args) {
|
|
8
|
+
const client = useApiClient();
|
|
9
|
+
const { method, rest } = parseArgs(args, "get");
|
|
10
|
+
const [init, options, queryClient] = rest;
|
|
11
|
+
const { pageParamName = "cursor", ...restOptions } = options;
|
|
12
|
+
const queryKey = buildQueryKey(method, path, init);
|
|
13
|
+
return useTanstackInfiniteQuery({
|
|
14
|
+
queryKey,
|
|
15
|
+
queryFn: async ({ pageParam = 0, signal }) => {
|
|
16
|
+
const mth = method.toUpperCase();
|
|
17
|
+
const fn = client[mth];
|
|
18
|
+
const mergedInit = {
|
|
19
|
+
...init,
|
|
20
|
+
signal,
|
|
21
|
+
params: {
|
|
22
|
+
...(init?.params ?? undefined),
|
|
23
|
+
query: {
|
|
24
|
+
...(init?.params?.query ?? undefined),
|
|
25
|
+
[pageParamName]: pageParam,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const { data, error } = await fn(path, mergedInit);
|
|
30
|
+
if (error)
|
|
31
|
+
throw error;
|
|
32
|
+
return data;
|
|
33
|
+
},
|
|
34
|
+
...restOptions,
|
|
35
|
+
}, queryClient);
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type QueryClient, type UseMutationOptions, type UseMutationResult } from "@tanstack/react-query";
|
|
2
|
+
import type { FetchResponse, HttpMethod, MaybeOptionalInit, MediaType, PathsWithMethod } from "@shipworthy/openapi-fetch";
|
|
3
|
+
interface ApiClientMutation<Paths extends Record<string, any>> {
|
|
4
|
+
<Path extends PathsWithMethod<Paths, "post">, Init extends MaybeOptionalInit<Paths[Path], "post">, Response extends Required<FetchResponse<Paths[Path]["post"], Init, MediaType>>, Options extends Omit<UseMutationOptions<Response["data"], Response["error"], Init>, "mutationKey" | "mutationFn">>(path: Path, options?: Options, queryClient?: QueryClient): UseMutationResult<Response["data"], Response["error"], Init>;
|
|
5
|
+
<Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method] & Record<string | number, any>, Init, MediaType>>, Options extends Omit<UseMutationOptions<Response["data"], Response["error"], Init>, "mutationKey" | "mutationFn">>(path: Path, method: Method, options?: Options, queryClient?: QueryClient): UseMutationResult<Response["data"], Response["error"], Init>;
|
|
6
|
+
}
|
|
7
|
+
export declare function useApiClientMutation<Paths extends Record<string, any> = {}>(): ApiClientMutation<Paths>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useMutation as useTanstackMutation, } from "@tanstack/react-query";
|
|
2
|
+
import { useApiClient } from "../provider.js";
|
|
3
|
+
import { parseArgs } from "../utils.js";
|
|
4
|
+
export function useApiClientMutation() {
|
|
5
|
+
return useMutation;
|
|
6
|
+
}
|
|
7
|
+
function useMutation(path, ...args) {
|
|
8
|
+
const client = useApiClient();
|
|
9
|
+
const { method, rest } = parseArgs(args, "post");
|
|
10
|
+
const [options, queryClient] = rest;
|
|
11
|
+
return useTanstackMutation({
|
|
12
|
+
mutationKey: [method, path],
|
|
13
|
+
mutationFn: async (init) => {
|
|
14
|
+
const mth = method.toUpperCase();
|
|
15
|
+
const fn = client[mth];
|
|
16
|
+
const { data, error } = await fn(path, init);
|
|
17
|
+
if (error)
|
|
18
|
+
throw error;
|
|
19
|
+
return data;
|
|
20
|
+
},
|
|
21
|
+
...(options ?? undefined),
|
|
22
|
+
}, queryClient);
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type QueryClient, type SkipToken, type UseQueryOptions, type UseQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import type { FetchResponse, HttpMethod, MaybeOptionalInit, MediaType, PathsWithMethod, RequiredKeysOf } from "@shipworthy/openapi-fetch";
|
|
3
|
+
import { type InferSelectReturnType, type InitWithUnknowns, type QueryKey } from "../utils.ts";
|
|
4
|
+
interface ApiClientQueryOptions<Paths extends Record<string, any>> {
|
|
5
|
+
<Path extends PathsWithMethod<Paths, "get">, Init extends MaybeOptionalInit<Paths[Path], "get">, Response extends Required<FetchResponse<Paths[Path]["get"], Init, MediaType>>, Options extends Omit<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, "get", Path>>, "queryKey" | "queryFn">>(path: Path, ...[init, options]: RequiredKeysOf<Init> extends never ? [InitWithUnknowns<Init>?, Options?] : [InitWithUnknowns<Init>, Options?]): NoInfer<Omit<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, "get", Path>>, "queryFn"> & {
|
|
6
|
+
queryFn: Exclude<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, "get", Path>>["queryFn"], SkipToken | undefined>;
|
|
7
|
+
}>;
|
|
8
|
+
<Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method] & Record<string | number, any>, Init, MediaType>>, Options extends Omit<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(path: Path, method: Method, ...[init, options]: RequiredKeysOf<Init> extends never ? [InitWithUnknowns<Init>?, Options?] : [InitWithUnknowns<Init>, Options?]): NoInfer<Omit<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, Method, Path>>, "queryFn"> & {
|
|
9
|
+
queryFn: Exclude<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, Method, Path>>["queryFn"], SkipToken | undefined>;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
interface ApiClientQuery<Paths extends Record<string, any>> {
|
|
13
|
+
<Path extends PathsWithMethod<Paths, "get">, Init extends MaybeOptionalInit<Paths[Path], "get">, Response extends Required<FetchResponse<Paths[Path]["get"], Init, MediaType>>, Options extends Omit<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, "get", Path>>, "queryKey" | "queryFn">>(path: Path, ...[init, options, queryClient]: RequiredKeysOf<Init> extends never ? [InitWithUnknowns<Init>?, Options?, QueryClient?] : [InitWithUnknowns<Init>, Options?, QueryClient?]): UseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;
|
|
14
|
+
<Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method] & Record<string | number, any>, Init, MediaType>>, Options extends Omit<UseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(path: Path, method: Method, ...[init, options, queryClient]: RequiredKeysOf<Init> extends never ? [InitWithUnknowns<Init>?, Options?, QueryClient?] : [InitWithUnknowns<Init>, Options?, QueryClient?]): UseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;
|
|
15
|
+
}
|
|
16
|
+
/** Bind a schema first, then call the returned hook to infer each endpoint. */
|
|
17
|
+
export declare function useApiClientQuery<Paths extends Record<string, any> = {}>(): ApiClientQuery<Paths>;
|
|
18
|
+
/** Bind a schema first, then call the returned hook to build query options. */
|
|
19
|
+
export declare function useApiClientQueryOptions<Paths extends Record<string, any> = {}>(): ApiClientQueryOptions<Paths>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useQuery as useTanstackQuery, } from "@tanstack/react-query";
|
|
2
|
+
import { useApiClient } from "../provider.js";
|
|
3
|
+
import { buildQueryFn, buildQueryKey, parseArgs, } from "../utils.js";
|
|
4
|
+
/** Bind a schema first, then call the returned hook to infer each endpoint. */
|
|
5
|
+
export function useApiClientQuery() {
|
|
6
|
+
return useQuery;
|
|
7
|
+
}
|
|
8
|
+
/** Bind a schema first, then call the returned hook to build query options. */
|
|
9
|
+
export function useApiClientQueryOptions() {
|
|
10
|
+
return useQueryOptions;
|
|
11
|
+
}
|
|
12
|
+
function useQueryOptions(path, ...args) {
|
|
13
|
+
const client = useApiClient();
|
|
14
|
+
const { method, rest } = parseArgs(args, "get");
|
|
15
|
+
const [init, options] = rest;
|
|
16
|
+
return {
|
|
17
|
+
queryKey: buildQueryKey(method, path, init),
|
|
18
|
+
queryFn: buildQueryFn(client, method, path, init),
|
|
19
|
+
...(options ?? undefined),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function useQuery(path, ...args) {
|
|
23
|
+
const client = useApiClient();
|
|
24
|
+
const { method, rest } = parseArgs(args, "get");
|
|
25
|
+
const [init, options, queryClient] = rest;
|
|
26
|
+
return useTanstackQuery({
|
|
27
|
+
queryKey: buildQueryKey(method, path, init),
|
|
28
|
+
queryFn: buildQueryFn(client, method, path, init),
|
|
29
|
+
...(options ?? undefined),
|
|
30
|
+
}, queryClient);
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type QueryClient, type UseSuspenseQueryOptions, type UseSuspenseQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import type { FetchResponse, HttpMethod, MaybeOptionalInit, MediaType, PathsWithMethod, RequiredKeysOf } from "@shipworthy/openapi-fetch";
|
|
3
|
+
import { type InferSelectReturnType, type InitWithUnknowns, type QueryKey } from "../utils.ts";
|
|
4
|
+
interface ApiClientSuspenseQuery<Paths extends Record<string, any>> {
|
|
5
|
+
<Path extends PathsWithMethod<Paths, "get">, Init extends MaybeOptionalInit<Paths[Path], "get">, Response extends Required<FetchResponse<Paths[Path]["get"], Init, MediaType>>, Options extends Omit<UseSuspenseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, "get", Path>>, "queryKey" | "queryFn">>(path: Path, ...[init, options, queryClient]: RequiredKeysOf<Init> extends never ? [InitWithUnknowns<Init>?, Options?, QueryClient?] : [InitWithUnknowns<Init>, Options?, QueryClient?]): UseSuspenseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;
|
|
6
|
+
/** Explicit method overload */
|
|
7
|
+
<Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>, Response extends Required<FetchResponse<Paths[Path][Method] & Record<string | number, any>, Init, MediaType>>, Options extends Omit<UseSuspenseQueryOptions<Response["data"], Response["error"], InferSelectReturnType<Response["data"], Options["select"]>, QueryKey<Paths, Method, Path>>, "queryKey" | "queryFn">>(path: Path, method: Method, ...[init, options, queryClient]: RequiredKeysOf<Init> extends never ? [InitWithUnknowns<Init>?, Options?, QueryClient?] : [InitWithUnknowns<Init>, Options?, QueryClient?]): UseSuspenseQueryResult<InferSelectReturnType<Response["data"], Options["select"]>, Response["error"]>;
|
|
8
|
+
}
|
|
9
|
+
export declare function useApiClientSuspenseQuery<Paths extends Record<string, any> = {}>(): ApiClientSuspenseQuery<Paths>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useSuspenseQuery as useTanstackSuspenseQuery, } from "@tanstack/react-query";
|
|
2
|
+
import { useApiClient } from "../provider.js";
|
|
3
|
+
import { buildQueryFn, buildQueryKey, parseArgs, } from "../utils.js";
|
|
4
|
+
export function useApiClientSuspenseQuery() {
|
|
5
|
+
return useSuspenseQuery;
|
|
6
|
+
}
|
|
7
|
+
function useSuspenseQuery(path, ...args) {
|
|
8
|
+
const client = useApiClient();
|
|
9
|
+
const { method, rest } = parseArgs(args, "get");
|
|
10
|
+
const [init, options, queryClient] = rest;
|
|
11
|
+
return useTanstackSuspenseQuery({
|
|
12
|
+
queryKey: buildQueryKey(method, path, init),
|
|
13
|
+
queryFn: buildQueryFn(client, method, path, init),
|
|
14
|
+
...(options ?? undefined),
|
|
15
|
+
}, queryClient);
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createApiClient } from "./client.ts";
|
|
2
|
+
export type { ApiClient } from "./client.ts";
|
|
3
|
+
export { ApiClientProvider, useApiClient } from "./provider.tsx";
|
|
4
|
+
export { useApiClientQueryOptions, useApiClientQuery } from "./hooks/use-query.ts";
|
|
5
|
+
export { useApiClientSuspenseQuery } from "./hooks/use-suspense-query.ts";
|
|
6
|
+
export { useApiClientInfiniteQuery } from "./hooks/use-infinite-query.ts";
|
|
7
|
+
export { useApiClientMutation } from "./hooks/use-mutation.ts";
|
|
8
|
+
export type { QueryKey } from "./utils.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createApiClient } from "./client.js";
|
|
2
|
+
export { ApiClientProvider, useApiClient } from "./provider.js";
|
|
3
|
+
export { useApiClientQueryOptions, useApiClientQuery } from "./hooks/use-query.js";
|
|
4
|
+
export { useApiClientSuspenseQuery } from "./hooks/use-suspense-query.js";
|
|
5
|
+
export { useApiClientInfiniteQuery } from "./hooks/use-infinite-query.js";
|
|
6
|
+
export { useApiClientMutation } from "./hooks/use-mutation.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PropsWithChildren } from "react";
|
|
2
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { type Client } from "@shipworthy/openapi-fetch";
|
|
4
|
+
export declare function useApiClient<Paths extends {} = {}>(): Client<Paths>;
|
|
5
|
+
export declare function ApiClientProvider({ children, baseUrl, queryClient, }: PropsWithChildren<{
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
queryClient?: QueryClient;
|
|
8
|
+
}>): import("react").JSX.Element;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { isServer, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import createClient from "@shipworthy/openapi-fetch";
|
|
5
|
+
const ApiClientContext = createContext(null);
|
|
6
|
+
export function useApiClient() {
|
|
7
|
+
const client = useContext(ApiClientContext);
|
|
8
|
+
if (!client)
|
|
9
|
+
throw new Error("useApiClient must be used within ApiClientProvider");
|
|
10
|
+
return client;
|
|
11
|
+
}
|
|
12
|
+
function makeQueryClient() {
|
|
13
|
+
return new QueryClient({
|
|
14
|
+
defaultOptions: { queries: { staleTime: 60 * 1000 } },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
let browserQueryClient;
|
|
18
|
+
function getQueryClient() {
|
|
19
|
+
if (isServer)
|
|
20
|
+
return makeQueryClient();
|
|
21
|
+
if (!browserQueryClient)
|
|
22
|
+
browserQueryClient = makeQueryClient();
|
|
23
|
+
return browserQueryClient;
|
|
24
|
+
}
|
|
25
|
+
export function ApiClientProvider({ children, baseUrl, queryClient = getQueryClient(), }) {
|
|
26
|
+
const client = createClient({ baseUrl });
|
|
27
|
+
return (_jsx(ApiClientContext.Provider, { value: client, children: _jsx(QueryClientProvider, { client: queryClient, children: children }) }));
|
|
28
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Client, HttpMethod, MaybeOptionalInit, PathsWithMethod } from "@shipworthy/openapi-fetch";
|
|
2
|
+
export type InferSelectReturnType<TData, TSelect> = TSelect extends (data: TData) => infer R ? R : TData;
|
|
3
|
+
export type InitWithUnknowns<Init> = Init & {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
export type QueryKey<Paths extends Record<string, any>, Method extends HttpMethod, Path extends PathsWithMethod<Paths, Method>, Init = MaybeOptionalInit<Paths[Path], Method>> = Init extends undefined ? readonly [Method, Path] : readonly [Method, Path, Init];
|
|
7
|
+
export declare function parseArgs(args: unknown[], defaultMethod: HttpMethod): {
|
|
8
|
+
method: HttpMethod;
|
|
9
|
+
rest: unknown[];
|
|
10
|
+
};
|
|
11
|
+
export declare function buildQueryFn(client: Client<{}>, method: HttpMethod, path: string, init: unknown): ({ signal }: {
|
|
12
|
+
signal: AbortSignal;
|
|
13
|
+
}) => Promise<any>;
|
|
14
|
+
export declare function buildQueryKey(method: HttpMethod, path: string, init: unknown): readonly [HttpMethod, string] | readonly [HttpMethod, string, {} | null];
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function parseArgs(args, defaultMethod) {
|
|
2
|
+
if (typeof args[0] === "string") {
|
|
3
|
+
const [method, ...rest] = args;
|
|
4
|
+
return { method: method, rest };
|
|
5
|
+
}
|
|
6
|
+
return { method: defaultMethod, rest: args };
|
|
7
|
+
}
|
|
8
|
+
export function buildQueryFn(client, method, path, init) {
|
|
9
|
+
return async ({ signal }) => {
|
|
10
|
+
const mth = method.toUpperCase();
|
|
11
|
+
const fn = client[mth];
|
|
12
|
+
const { data, error, response } = await fn(path, {
|
|
13
|
+
signal,
|
|
14
|
+
...(init ?? undefined),
|
|
15
|
+
});
|
|
16
|
+
if (error)
|
|
17
|
+
throw error;
|
|
18
|
+
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
|
|
19
|
+
return data ?? null;
|
|
20
|
+
}
|
|
21
|
+
return data;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function buildQueryKey(method, path, init) {
|
|
25
|
+
return init === undefined ? [method, path] : [method, path, init];
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipworthy/openapi-react-query",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@shipworthy/openapi-fetch": "0.0.1"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"@tanstack/react-query": "^5.104.1",
|
|
12
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ShipWorthyAI/openapi.git",
|
|
18
|
+
"directory": "packages/openapi-react-query"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"react-native": "./dist/index.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@shipworthy/typescript-config": "0.0.1",
|
|
34
|
+
"@tanstack/react-query": "^5.104.1",
|
|
35
|
+
"@types/node": "^24.10.3",
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"react": "^19.0.0",
|
|
38
|
+
"typescript": "^7.0.2"
|
|
39
|
+
},
|
|
40
|
+
"main": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|