@shipworthy/openapi-tanstack-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 +592 -2
- package/dist/generator.d.ts +19 -0
- package/dist/generator.js +57 -0
- package/dist/index.cjs +107 -0
- package/dist/index.d.cts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +105 -0
- package/package.json +73 -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,593 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shipworthy/openapi-tanstack-query
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed [TanStack Query](https://tanstack.com/query) options, keys, and mutations
|
|
4
|
+
for every operation in an OpenAPI 3.x document. Use it in a React (or React
|
|
5
|
+
Native) app that already uses `@tanstack/react-query`. The package is a thin
|
|
6
|
+
adapter over [`@shipworthy/openapi-fetch`](../openapi-fetch). Types come from
|
|
7
|
+
`@shipworthy/openapi-typescript`. Code generation only writes a small `client.ts`
|
|
8
|
+
that maps each `operationId` to `[METHOD, path]`. You then pass the returned
|
|
9
|
+
options to TanStack's own hooks (`useQuery`, `useMutation`, `useInfiniteQuery`,
|
|
10
|
+
`useSuspenseQuery`, `queryClient.fetchQuery`, and so on). The package has no
|
|
11
|
+
hooks or React provider of its own.
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Quick start](#quick-start)
|
|
17
|
+
- [Generated output](#generated-output)
|
|
18
|
+
- [CLI: `openapi generate tanstack-query`](#cli-openapi-generate-tanstack-query)
|
|
19
|
+
- [Runtime API (`@shipworthy/openapi-tanstack-query`)](#runtime-api-shipworthyopenapi-tanstack-query)
|
|
20
|
+
- [Generator API (`@shipworthy/openapi-tanstack-query/generator`)](#generator-api-shipworthyopenapi-tanstack-querygenerator)
|
|
21
|
+
- [Cache keys](#cache-keys)
|
|
22
|
+
- [Requests, responses, and errors](#requests-responses-and-errors)
|
|
23
|
+
- [Limitations and caveats](#limitations-and-caveats)
|
|
24
|
+
- [Troubleshooting](#troubleshooting)
|
|
25
|
+
- [Key facts (for AI agents)](#key-facts-for-ai-agents)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
bun add @shipworthy/openapi-tanstack-query @tanstack/react-query react
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Add the code generators as dev dependencies. `effect`,
|
|
35
|
+
`@shipworthy/openapi-generator`, and `@shipworthy/openapi-transformer` are
|
|
36
|
+
optional peer dependencies that only the `./generator` entry point needs. The
|
|
37
|
+
CLI installs the last two. The TypeScript generator bundles TypeScript 7's AST
|
|
38
|
+
modules, so your project can keep its own TypeScript version:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript effect@^4
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Requirement | Value |
|
|
45
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| Peer dependencies | `@tanstack/react-query` `^5.59.0`, `react` `^18.0.0 \|\| ^19.0.0` |
|
|
47
|
+
| Optional peers | `effect` `^4.0.0`, `@shipworthy/openapi-generator`, and `@shipworthy/openapi-transformer`, needed only for `./generator` (the CLI and programmatic generation) |
|
|
48
|
+
| Runtime dependencies | `@shipworthy/openapi-fetch` (installed automatically) |
|
|
49
|
+
| Module format | `.` ships ESM and CommonJS, each with matching declarations (`.d.ts` and `.d.cts`). `./generator` is ESM only. |
|
|
50
|
+
| Entry points | `.` (runtime) and `./generator` (generator plugin), compiled JavaScript with declarations in `dist/`. A `react-native` field points at `./dist/index.js`. |
|
|
51
|
+
| Runtime | Any environment with `fetch`, `Request`, and `Response`, or pass your own `fetch`. No TypeScript transpilation of `node_modules` is needed. |
|
|
52
|
+
| TypeScript | Declarations type-check with TypeScript `5.0` and later, using `NodeNext` (CommonJS or ESM) or `bundler` module resolution. |
|
|
53
|
+
| CLI runtime | The `openapi` binary from `@shipworthy/openapi-cli` runs on **Node.js** `>=22.19.0` or **Bun** (`bunx --bun @shipworthy/openapi-cli`). |
|
|
54
|
+
| OpenAPI versions | 3.0.x and 3.1.x. 3.0 documents are upgraded to 3.1 internally. Swagger 2.x is rejected. |
|
|
55
|
+
|
|
56
|
+
The runtime entry point declares no Node.js `engines` range. The CLI runtime requirement applies to code generation.
|
|
57
|
+
|
|
58
|
+
`@shipworthy/openapi-cli` loads the generator with
|
|
59
|
+
`import("@shipworthy/openapi-tanstack-query/generator")`. It loads
|
|
60
|
+
`@shipworthy/openapi-typescript` for `types.ts`. Both packages, plus `effect`
|
|
61
|
+
and `@shipworthy/openapi-generator`, must be resolvable from the directory where
|
|
62
|
+
you run the CLI.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
**1. Generate the client** from a local file or a URL. JSON and YAML both work,
|
|
67
|
+
and the CLI resolves `$ref`s:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
bunx --bun @shipworthy/openapi-cli generate tanstack-query ./openapi.json -o src/generated --name hotels
|
|
71
|
+
# Or generate from a URL:
|
|
72
|
+
bunx --bun @shipworthy/openapi-cli generate tanstack-query https://api.example.com/openapi.json -o src/generated --name hotels
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This writes `src/generated/hotels/types.ts` (via `@shipworthy/openapi-typescript`)
|
|
76
|
+
and `src/generated/hotels/client.ts`.
|
|
77
|
+
|
|
78
|
+
**2. Create the client and query utils once**:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// src/api.ts
|
|
82
|
+
import { createHotelsApiClient, createHotelsApiQueryUtils } from "./generated/hotels/client";
|
|
83
|
+
|
|
84
|
+
export const hotelsClient = createHotelsApiClient({
|
|
85
|
+
baseUrl: "https://hotels.example.com",
|
|
86
|
+
authToken: () => getHotelsAccessToken(), // string, or a sync/async getter; called per request
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const hotelsApi = createHotelsApiQueryUtils(hotelsClient);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**3. Pass the utils to TanStack Query hooks** inside your app's existing
|
|
93
|
+
`QueryClientProvider`:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
97
|
+
import { hotelsApi } from "./api";
|
|
98
|
+
|
|
99
|
+
function Hotels() {
|
|
100
|
+
const hotels = useQuery(
|
|
101
|
+
hotelsApi.getHotels.queryOptions({ input: { query: { country: "US", min_stars: 5 } } }),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const booking = useMutation(hotelsApi.createBooking.mutationOptions());
|
|
105
|
+
// booking.mutate({ body: bookingInput, header: { "Idempotency-Key": attemptKey } });
|
|
106
|
+
|
|
107
|
+
return <pre>{JSON.stringify(hotels.data, null, 2)}</pre>;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Inputs, response data, and error types are all inferred from the OpenAPI
|
|
112
|
+
document. You do not write type arguments.
|
|
113
|
+
|
|
114
|
+
## Generated output
|
|
115
|
+
|
|
116
|
+
`openapi generate tanstack-query <input> -o <dir> --name <name>` writes the
|
|
117
|
+
following into `<dir>/<name>/`:
|
|
118
|
+
|
|
119
|
+
- `types.ts`: `paths`, `components`, `operations`, and `webhooks` types from
|
|
120
|
+
`@shipworthy/openapi-typescript`. This file is regenerated only when it is
|
|
121
|
+
missing or stale (see [`--force`](#cli-openapi-generate-tanstack-query)).
|
|
122
|
+
- `client.ts`: overwritten on every successful run. For `--name my-pets`
|
|
123
|
+
(prefix `MyPets`), it contains:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
/**
|
|
127
|
+
* This file was auto-generated by @shipworthy/openapi-tanstack-query.
|
|
128
|
+
* Do not make direct changes to the file.
|
|
129
|
+
*
|
|
130
|
+
* @shipworthy/openapi-tanstack-query@0.0.1:<sha256 of schema contents>
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
import {
|
|
134
|
+
createApiClient,
|
|
135
|
+
createTanstackQueryUtils,
|
|
136
|
+
type TanstackQueryUtilsOptions,
|
|
137
|
+
} from "@shipworthy/openapi-tanstack-query";
|
|
138
|
+
import type { operations, paths } from "./types";
|
|
139
|
+
|
|
140
|
+
export type {
|
|
141
|
+
paths as MyPetsApiPaths,
|
|
142
|
+
components as MyPetsApiComponents,
|
|
143
|
+
operations as MyPetsApiOperations,
|
|
144
|
+
} from "./types";
|
|
145
|
+
export const createMyPetsApiClient: typeof createApiClient<paths> = createApiClient<paths>;
|
|
146
|
+
export type MyPetsApiClient = ReturnType<typeof createMyPetsApiClient>;
|
|
147
|
+
|
|
148
|
+
const routes = {
|
|
149
|
+
getPet: ["GET", "/pets/{id}"],
|
|
150
|
+
createPet: ["POST", "/pets"],
|
|
151
|
+
} as const;
|
|
152
|
+
|
|
153
|
+
export function createMyPetsApiQueryUtils(
|
|
154
|
+
client: MyPetsApiClient,
|
|
155
|
+
options?: Partial<TanstackQueryUtilsOptions>,
|
|
156
|
+
) {
|
|
157
|
+
return createTanstackQueryUtils<Pick<operations, keyof typeof routes>>(client, routes, {
|
|
158
|
+
prefix: "my-pets",
|
|
159
|
+
...options,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Naming rules:
|
|
165
|
+
|
|
166
|
+
- `<Prefix>` is the PascalCase form of `--name`. `-` and `_` separators are
|
|
167
|
+
removed and the next character is uppercased: `hotels` becomes `Hotels`, and
|
|
168
|
+
`my-pets` becomes `MyPets`.
|
|
169
|
+
- The generated exports are `create<Prefix>ApiClient`, `<Prefix>ApiClient`
|
|
170
|
+
(type), `create<Prefix>ApiQueryUtils`, `<Prefix>ApiPaths`,
|
|
171
|
+
`<Prefix>ApiComponents`, and `<Prefix>ApiOperations`.
|
|
172
|
+
- The default cache-key prefix is the raw `--name` value, such as `"my-pets"`.
|
|
173
|
+
- Each operation is available on the query utils object by its `operationId`,
|
|
174
|
+
for example `hotelsApi.getHotel`.
|
|
175
|
+
|
|
176
|
+
## CLI: `openapi generate tanstack-query`
|
|
177
|
+
|
|
178
|
+
The command is provided by `@shipworthy/openapi-cli`.
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
openapi generate tanstack-query [flags] <input>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
| Argument / flag | Type | Default | Description |
|
|
185
|
+
| ---------------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
186
|
+
| `<input>` | string | none | OpenAPI file path, or an `http:`, `https:`, or `file:` URL. Glob patterns (`*`) are rejected. |
|
|
187
|
+
| `--output`, `-o` | string | none | Directory that will contain `<name>/client.ts`. |
|
|
188
|
+
| `--name` | string | none | Client name. It sets the output subdirectory, the PascalCase export prefix, and the default cache-key prefix. It must match `^[a-zA-Z][a-zA-Z0-9]*(?:[-_][a-zA-Z0-9]+)*$`. |
|
|
189
|
+
| `--force` | boolean | `false` | Regenerate `types.ts` even when its stamp matches. |
|
|
190
|
+
|
|
191
|
+
`types.ts` is regenerated when it is missing, or when the
|
|
192
|
+
`@shipworthy/openapi-typescript@<version>:<sha256>` stamp in its header no
|
|
193
|
+
longer matches. The hash covers the schema contents, not the schema's location,
|
|
194
|
+
so moving the file does not trigger a rebuild.
|
|
195
|
+
|
|
196
|
+
Example `package.json` script:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"scripts": {
|
|
201
|
+
"generate": "openapi generate tanstack-query openapi.json -o src/generated --name hotels"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Generation fails with exit code 1, and `client.ts` is left untouched, when:
|
|
207
|
+
|
|
208
|
+
| Error message | Cause |
|
|
209
|
+
| -------------------------------------- | -------------------------------------------------------- |
|
|
210
|
+
| `<METHOD> <path>: Missing operationId` | An operation has no `operationId`. |
|
|
211
|
+
| `Duplicate operationId: <id>` | Two operations share an `operationId`. |
|
|
212
|
+
| `Unsupported operationId: <id>` | The id is `__proto__`, `constructor`, or `prototype`. |
|
|
213
|
+
| `Unsupported Swagger version: 2.x. …` | The input is Swagger 2.x. Only OpenAPI 3.x is supported. |
|
|
214
|
+
| `Globbing is unsupported.` | `<input>` contains `*`. |
|
|
215
|
+
|
|
216
|
+
## Runtime API (`@shipworthy/openapi-tanstack-query`)
|
|
217
|
+
|
|
218
|
+
Most apps use the generated `create<Prefix>ApiClient` and
|
|
219
|
+
`create<Prefix>ApiQueryUtils` wrappers. The underlying exports are listed below.
|
|
220
|
+
|
|
221
|
+
### `createApiClient<Paths>(options): ApiClient<Paths>`
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
function createApiClient<Paths extends {}>(options: ApiClientOptions): Client<Paths>;
|
|
225
|
+
|
|
226
|
+
interface ApiClientOptions extends Omit<ClientOptions, "baseUrl"> {
|
|
227
|
+
baseUrl: string; // required
|
|
228
|
+
authToken?: AuthToken | AuthTokenProvider;
|
|
229
|
+
}
|
|
230
|
+
type AuthToken = string | undefined | null;
|
|
231
|
+
type AuthTokenProvider = () => AuthToken | Promise<AuthToken>;
|
|
232
|
+
type ApiClient<Paths extends {} = {}> = Client<Paths>; // @shipworthy/openapi-fetch Client
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This function creates an `@shipworthy/openapi-fetch` client.
|
|
236
|
+
|
|
237
|
+
- **`baseUrl`** (required): the root URL for all requests. A trailing slash is
|
|
238
|
+
removed.
|
|
239
|
+
- **`authToken`** (optional): a token string, or a sync or async getter.
|
|
240
|
+
- The getter runs **before every request**.
|
|
241
|
+
- A truthy token sets `Authorization: Bearer <token>`.
|
|
242
|
+
- A falsy token (`""`, `null`, `undefined`) _removes_ any `Authorization`
|
|
243
|
+
header.
|
|
244
|
+
- If you omit `authToken`, no auth middleware is installed.
|
|
245
|
+
- If the request's signal is already aborted after the token resolves, the
|
|
246
|
+
request fails with an `AbortError`.
|
|
247
|
+
- All other `ClientOptions` from `@shipworthy/openapi-fetch` are passed
|
|
248
|
+
through, including `fetch`, `Request`, `headers`, `querySerializer`,
|
|
249
|
+
`bodySerializer`, `pathSerializer`, `requestInitExt`, and standard
|
|
250
|
+
`RequestInit` fields such as `credentials`.
|
|
251
|
+
|
|
252
|
+
The returned client is a full `openapi-fetch` client. You can call it directly
|
|
253
|
+
(`client.GET("/hotels", { params: { query } })`) or add middleware with
|
|
254
|
+
`client.use(...)`.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const client = createHotelsApiClient({
|
|
258
|
+
baseUrl: "https://hotels.example.com",
|
|
259
|
+
authToken: async () => (await session.get())?.accessToken,
|
|
260
|
+
headers: { "X-App-Version": "1.4.0" },
|
|
261
|
+
querySerializer: { array: { style: "form", explode: false } },
|
|
262
|
+
fetch: (request) => fetch(request), // e.g. a custom or instrumented fetch
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### `createTanstackQueryUtils<Operations>(client, routes, options)`
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
function createTanstackQueryUtils<Operations extends Record<string, any>>(
|
|
270
|
+
client: Client<any>,
|
|
271
|
+
routes: { [Name in keyof Operations]: OperationRoute },
|
|
272
|
+
options: TanstackQueryUtilsOptions,
|
|
273
|
+
): { [Name in keyof Operations]: OperationQueryUtils<Operations[Name]> };
|
|
274
|
+
|
|
275
|
+
type OperationRoute = readonly [Uppercase<HttpMethod>, string]; // e.g. ["GET", "/hotels/{hotel_id}"]
|
|
276
|
+
interface TanstackQueryUtilsOptions {
|
|
277
|
+
prefix: string | readonly string[]; // cache-key namespace
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This function builds one `OperationQueryUtils` object per route. The generated
|
|
282
|
+
`create<Prefix>ApiQueryUtils(client, options?)` calls it with the generated
|
|
283
|
+
`routes` and `{ prefix: "<name>" }`. Pass `options.prefix` to override the
|
|
284
|
+
namespace. This is useful when two backends with the same API share one
|
|
285
|
+
`QueryClient`:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
const staging = createHotelsApiQueryUtils(stagingClient, { prefix: "hotels-staging" });
|
|
289
|
+
const nested = createHotelsApiQueryUtils(client, { prefix: ["tenant", tenantId, "hotels"] });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
You can also call it by hand with your own `operations` type:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { createApiClient, createTanstackQueryUtils } from "@shipworthy/openapi-tanstack-query";
|
|
296
|
+
import type { operations, paths } from "./types"; // from @shipworthy/openapi-typescript
|
|
297
|
+
|
|
298
|
+
const client = createApiClient<paths>({ baseUrl: "https://api.example.com" });
|
|
299
|
+
const api = createTanstackQueryUtils<Pick<operations, "getPet">>(
|
|
300
|
+
client,
|
|
301
|
+
{ getPet: ["GET", "/pets/{id}"] },
|
|
302
|
+
{ prefix: "pets" },
|
|
303
|
+
);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### `OperationQueryUtils<Op>`
|
|
307
|
+
|
|
308
|
+
Every operation (`api.<operationId>`) exposes these members. In the table below,
|
|
309
|
+
`Input` is `OperationInput<Op>`, `Data` is `OperationData<Op>`, and `Error` is
|
|
310
|
+
`OperationError<Op>`.
|
|
311
|
+
|
|
312
|
+
| Member | Returns |
|
|
313
|
+
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| `queryOptions({ input, ...opts })` | `{ ...opts, queryKey, queryFn }` for `useQuery`, `useSuspenseQuery`, `useQueries`, `queryClient.fetchQuery` / `prefetchQuery` / `ensureQueryData`. `opts` accepts any `UseQueryOptions` except `queryKey` and `queryFn`, such as `select`, `staleTime`, and `enabled`. `input` may be `skipToken`. |
|
|
315
|
+
| `queryKey({ input })` | The exact query key, tagged with `Data` for `getQueryData` and `setQueryData`. |
|
|
316
|
+
| `infiniteOptions({ input, initialPageParam, ...opts })` | `{ ...opts, initialPageParam, queryKey, queryFn }` for `useInfiniteQuery`, `useSuspenseInfiniteQuery`, and `queryClient.fetchInfiniteQuery`. `input` is `(pageParam) => Input`, or `skipToken`. Requires TanStack's `getNextPageParam`. |
|
|
317
|
+
| `infiniteKey({ input, initialPageParam })` | The exact infinite key, tagged with `InfiniteData<Data, Page>`. |
|
|
318
|
+
| `mutationOptions(opts?)` | `{ ...opts, mutationKey, mutationFn }` for `useMutation`. `opts` accepts any `UseMutationOptions` except `mutationKey` and `mutationFn`, such as `onSuccess` and `retry`. The mutation variables are `Input` with **no** `input` wrapper. |
|
|
319
|
+
| `mutationKey()` | The mutation key. |
|
|
320
|
+
| `key({ type?, input? })` | A key prefix for partial matching, such as `invalidateQueries`, `removeQueries`, or `isFetching`. `type` is `"query" \| "infinite" \| "mutation"`. `input` may be a deep-partial `Input`. |
|
|
321
|
+
|
|
322
|
+
When an operation has no required parameters or body, `input` and the
|
|
323
|
+
options argument are optional. For example,
|
|
324
|
+
`api.listHotels.queryOptions()` is valid.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import {
|
|
328
|
+
skipToken,
|
|
329
|
+
useInfiniteQuery,
|
|
330
|
+
useMutation,
|
|
331
|
+
useQuery,
|
|
332
|
+
useQueryClient,
|
|
333
|
+
useSuspenseQuery,
|
|
334
|
+
} from "@tanstack/react-query";
|
|
335
|
+
|
|
336
|
+
// select narrows the hook's data; the cache keeps the full response
|
|
337
|
+
const hotels = useQuery(
|
|
338
|
+
hotelsApi.getHotels.queryOptions({
|
|
339
|
+
input: { query: { country: "US", min_stars: 5 } },
|
|
340
|
+
select: (response) => response.data,
|
|
341
|
+
staleTime: 60_000,
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Disabled until hotelId exists. With skipToken there is no fetch and no authToken call.
|
|
346
|
+
const hotel = useQuery(
|
|
347
|
+
hotelsApi.getHotel.queryOptions({
|
|
348
|
+
input: hotelId ? { path: { hotel_id: hotelId } } : skipToken,
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Suspense: always pass a real input (see caveats)
|
|
353
|
+
const { data } = useSuspenseQuery(
|
|
354
|
+
hotelsApi.getHotel.queryOptions({ input: { path: { hotel_id: "h_123" } } }),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Infinite / cursor pagination
|
|
358
|
+
const pages = useInfiniteQuery(
|
|
359
|
+
hotelsApi.getHotels.infiniteOptions({
|
|
360
|
+
input: (cursor: string | undefined) => ({ query: { country: "US", cursor } }),
|
|
361
|
+
initialPageParam: undefined,
|
|
362
|
+
getNextPageParam: (response) => response.pagination.next_cursor ?? undefined,
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Mutation: variables are the grouped input (path/query/header/cookie/body)
|
|
367
|
+
const queryClient = useQueryClient();
|
|
368
|
+
const cancel = useMutation(
|
|
369
|
+
hotelsApi.cancelBooking.mutationOptions({
|
|
370
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: hotelsApi.getBookings.key() }),
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
cancel.mutate({ path: { booking_id: bookingId } });
|
|
374
|
+
|
|
375
|
+
// Prefetch outside React
|
|
376
|
+
await queryClient.prefetchQuery(
|
|
377
|
+
hotelsApi.getHotels.queryOptions({ input: { query: { country: "US" } } }),
|
|
378
|
+
);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Type helpers
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
type OperationInput<Op>; // Op["parameters"] groups (path, query, header, cookie) & { body } from requestBody
|
|
385
|
+
type OperationData<Op>; // union of all documented 2xx response bodies; empty/no-content → null
|
|
386
|
+
type OperationError<Op>; // union of documented error response bodies | Error
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Use them with the generated `<Prefix>ApiOperations` type:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
import type { OperationData, OperationInput } from "@shipworthy/openapi-tanstack-query";
|
|
393
|
+
import type { HotelsApiOperations } from "./generated/hotels/client";
|
|
394
|
+
|
|
395
|
+
type GetHotelInput = OperationInput<HotelsApiOperations["getHotel"]>; // { path: { hotel_id: string }, ... }
|
|
396
|
+
type Hotel = OperationData<HotelsApiOperations["getHotel"]>;
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`OperationInput` keeps OpenAPI's parameter groups. Required groups and fields
|
|
400
|
+
are required, and `body` is required when the `requestBody` is required.
|
|
401
|
+
|
|
402
|
+
## Cache keys
|
|
403
|
+
|
|
404
|
+
Keys have the form `[...prefix, operationId, filter?]`:
|
|
405
|
+
|
|
406
|
+
| Call | Key |
|
|
407
|
+
| --------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
408
|
+
| `api.getPet.key()` | `["pets", "getPet"]` |
|
|
409
|
+
| `api.getPet.key({ type: "query" })` | `["pets", "getPet", { type: "query" }]` |
|
|
410
|
+
| `api.getPet.queryKey()` | `["pets", "getPet", { type: "query", input: {} }]` |
|
|
411
|
+
| `api.getPet.queryKey({ input: { path: { id: 1 } } })` | `["pets", "getPet", { type: "query", input: { path: { id: 1 } } }]` |
|
|
412
|
+
| `api.getPet.queryOptions({ input: skipToken }).queryKey` | `["pets", "getPet", { type: "query", skipped: true }]` |
|
|
413
|
+
| `api.list.infiniteKey({ input: (c) => ({ query: { c } }), initialPageParam: 0 })` | `["pets", "list", { type: "infinite", input: { query: { c: 0 } } }]` |
|
|
414
|
+
| `api.create.mutationKey()` | `["pets", "create", { type: "mutation" }]` |
|
|
415
|
+
| prefix `["a", "b"]`, `api.getPet.key()` | `["a", "b", "getPet"]` |
|
|
416
|
+
|
|
417
|
+
Rules:
|
|
418
|
+
|
|
419
|
+
- Because TanStack matches keys by prefix, `key()` matches every query,
|
|
420
|
+
infinite query, and mutation for that operation. Add `type` or `input` to
|
|
421
|
+
narrow the match.
|
|
422
|
+
- Regular and infinite queries never share a key.
|
|
423
|
+
- An infinite key uses the input computed from `initialPageParam`. Keep the
|
|
424
|
+
`input` function pure.
|
|
425
|
+
- Header normalization applies to the `input` in cache keys only. It does not
|
|
426
|
+
change what is sent:
|
|
427
|
+
- `Authorization`, `Proxy-Authorization`, `Cookie`, and `Set-Cookie` are
|
|
428
|
+
dropped.
|
|
429
|
+
- `undefined` header values are dropped.
|
|
430
|
+
- Remaining header names are lowercased.
|
|
431
|
+
- If no headers remain, the `header` group is omitted.
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
queryClient.invalidateQueries({ queryKey: hotelsApi.getHotel.key() });
|
|
435
|
+
queryClient.invalidateQueries({ queryKey: hotelsApi.getHotel.key({ type: "query" }) });
|
|
436
|
+
queryClient.invalidateQueries({
|
|
437
|
+
queryKey: hotelsApi.getHotel.key({ input: { path: { hotel_id: hotelId } } }),
|
|
438
|
+
});
|
|
439
|
+
queryClient.setQueryData(
|
|
440
|
+
hotelsApi.getHotel.queryKey({ input: { path: { hotel_id: hotelId } } }),
|
|
441
|
+
(old) => old, // `old` is typed as the operation's data
|
|
442
|
+
);
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Requests, responses, and errors
|
|
446
|
+
|
|
447
|
+
- **Request mapping.** Input `{ body, ...groups }` is sent as
|
|
448
|
+
`client[METHOD](path, { params: groups, body, signal })`. Serialization
|
|
449
|
+
follows `@shipworthy/openapi-fetch`.
|
|
450
|
+
- **Request body.** An omitted optional body is not sent. An explicit `{}` is
|
|
451
|
+
sent as `{}`. Bodies need not be objects; the generated types set their shape.
|
|
452
|
+
- **Validation.** The adapter does not validate or filter payloads.
|
|
453
|
+
- **Cancellation.** Query `AbortSignal`s are passed to `fetch`, so TanStack
|
|
454
|
+
cancellation aborts in-flight requests. Mutations are not given a signal.
|
|
455
|
+
- **Success.** The parsed body is returned with its response envelope intact.
|
|
456
|
+
An empty successful body, such as a `204`, resolves to `null`, not
|
|
457
|
+
`undefined`.
|
|
458
|
+
- **HTTP failure** (`!response.ok`). The query or mutation rejects with the
|
|
459
|
+
**parsed error body**, which is often a plain object rather than an `Error`.
|
|
460
|
+
If the error body is empty, it rejects with
|
|
461
|
+
`new Error("HTTP <status> <statusText>")`.
|
|
462
|
+
- **Transport failure.** The original `Error` from `fetch` or middleware is
|
|
463
|
+
rethrown.
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
const hotel = useQuery(hotelsApi.getHotel.queryOptions({ input: { path: { hotel_id } } }));
|
|
467
|
+
if (hotel.error && !(hotel.error instanceof Error)) {
|
|
468
|
+
// typed as the documented error response body, e.g. { code: string; message: string }
|
|
469
|
+
console.warn(hotel.error);
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Generator API (`@shipworthy/openapi-tanstack-query/generator`)
|
|
474
|
+
|
|
475
|
+
`@shipworthy/openapi-cli` uses these exports. Call them directly only if you
|
|
476
|
+
are building your own generator pipeline. They are built on
|
|
477
|
+
[Effect](https://effect.website) 4 and take a `ParsedService` from
|
|
478
|
+
`@shipworthy/openapi-transformer`.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import {
|
|
482
|
+
clientSource,
|
|
483
|
+
operationRoutes,
|
|
484
|
+
tanstackQuery,
|
|
485
|
+
TanstackGenerationError,
|
|
486
|
+
} from "@shipworthy/openapi-tanstack-query/generator";
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
| Export | Signature / description |
|
|
490
|
+
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
491
|
+
| `tanstackQuery` | `Generator<string, TanstackGenerationError>` (from `@shipworthy/openapi-generator`) with `id: "tanstack-query"`. `generate(model, options)` returns `Effect<string, TanstackGenerationError>`, which is the `client.ts` source with no header comment. Options: `prefix?: string` (export prefix, default `""`) and `name?: string` (cache-key prefix, default `prefix`). Other option shapes fail with `TanstackGenerationError`. |
|
|
492
|
+
| `operationRoutes(model)` | `Effect<Record<string, OperationRoute>, TanstackGenerationError>`. Maps each `operationId` to `[METHOD, path]`. Fails on a missing, duplicate, or reserved (`__proto__`, `constructor`, `prototype`) `operationId`. |
|
|
493
|
+
| `clientSource(prefix, name, routes)` | `string`. Prints `client.ts` source for the given export prefix, cache-key prefix, and routes. |
|
|
494
|
+
| `TanstackGenerationError` | Effect `Schema.TaggedError` with `_tag: "TanstackGenerationError"` and `message: string`. |
|
|
495
|
+
|
|
496
|
+
Programmatic generation with `@shipworthy/openapi-generator`, which is what
|
|
497
|
+
the CLI does:
|
|
498
|
+
|
|
499
|
+
Install `@effect/platform-node@^4` as a dev dependency for the Node.js platform layer used in this example:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { NodeServices } from "@effect/platform-node";
|
|
503
|
+
import { generate, ModuleLoader } from "@shipworthy/openapi-generator";
|
|
504
|
+
import { Effect, Layer } from "effect";
|
|
505
|
+
import { FetchHttpClient } from "effect/http";
|
|
506
|
+
|
|
507
|
+
const program = generate({
|
|
508
|
+
specifier: "@shipworthy/openapi-tanstack-query/generator",
|
|
509
|
+
exportName: "tanstackQuery",
|
|
510
|
+
source: new URL("file:///abs/path/openapi.json"),
|
|
511
|
+
pluginOptions: { prefix: "Hotels", name: "hotels" },
|
|
512
|
+
}).pipe(
|
|
513
|
+
Effect.provideService(ModuleLoader, { load: (specifier) => import(specifier) }),
|
|
514
|
+
Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const source = await Effect.runPromise(program); // client.ts source string at runtime
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
This program writes neither `types.ts` nor the stamp header. The CLI adds
|
|
521
|
+
both.
|
|
522
|
+
|
|
523
|
+
## Limitations and caveats
|
|
524
|
+
|
|
525
|
+
Generated files use extensionless `./types` imports and expect TypeScript `moduleResolution: "bundler"`. Direct `NodeNext` compilation of those generated files is not currently supported. This does not affect the package's compiled exports.
|
|
526
|
+
|
|
527
|
+
- **Every operation needs a unique `operationId`.** Generation fails otherwise.
|
|
528
|
+
- **The package has no provider or hooks.** Use your app's existing
|
|
529
|
+
`QueryClientProvider` and TanStack's hooks.
|
|
530
|
+
- **Auth is the app's job.** The app owns token exchange and refresh, and it
|
|
531
|
+
clears account-specific caches on logout. `authToken` only reads a token for
|
|
532
|
+
each request.
|
|
533
|
+
- **Idempotency keys are not generated or persisted.** If an API needs them,
|
|
534
|
+
pass them in `header`. Reuse the same key for every retry of one logical
|
|
535
|
+
attempt.
|
|
536
|
+
- **Do not pass `skipToken` options to suspense hooks.** TanStack Query's
|
|
537
|
+
types allow it, but suspense hooks reject it at runtime.
|
|
538
|
+
- **Serializers are not generated per schema.** If a query parameter needs a
|
|
539
|
+
custom encoding, set `querySerializer` in `createApiClient`.
|
|
540
|
+
- **Cookies are handled by `fetch`.** Cookie behavior follows
|
|
541
|
+
`@shipworthy/openapi-fetch` and your `fetch` implementation.
|
|
542
|
+
|
|
543
|
+
## Troubleshooting
|
|
544
|
+
|
|
545
|
+
| Symptom | Fix |
|
|
546
|
+
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
547
|
+
| `<METHOD> <path>: Missing operationId` / `Duplicate operationId: …` | Add unique `operationId`s to the OpenAPI document. |
|
|
548
|
+
| `--name must start with a letter …` | Use letters and digits, with single `-` or `_` separators, such as `hotels` or `sdk-core`. |
|
|
549
|
+
| `types.ts` was not regenerated after editing generator settings | It is regenerated only when the schema hash or generator version changes. Pass `--force`. |
|
|
550
|
+
| CLI cannot load `@shipworthy/openapi-tanstack-query/generator` | Install this package, `@shipworthy/openapi-typescript`, `@shipworthy/openapi-generator`, and `effect` in the project. |
|
|
551
|
+
| `openapi` fails to start on an older Node.js | The CLI needs Node.js `>=22.19.0`. Upgrade Node.js, or run it with `bunx --bun @shipworthy/openapi-cli`. |
|
|
552
|
+
| `error instanceof Error` is false in `onError` | This is expected for HTTP errors: the error is the parsed error body. See [errors](#requests-responses-and-errors). |
|
|
553
|
+
| Two APIs with the same `--name` overwrite each other's cache entries | Pass a different `prefix` to `create<Prefix>ApiQueryUtils(client, { prefix })`. |
|
|
554
|
+
|
|
555
|
+
## Key facts (for AI agents)
|
|
556
|
+
|
|
557
|
+
- **Install:** `@shipworthy/openapi-tanstack-query`, plus peers
|
|
558
|
+
`@tanstack/react-query@^5.59.0` and `react@^18 || ^19`. The runtime entry
|
|
559
|
+
ships ESM and CommonJS with `.d.ts`/`.d.cts` declarations for TypeScript 5.0+.
|
|
560
|
+
For `./generator`, also install the optional peers `effect@^4`,
|
|
561
|
+
`@shipworthy/openapi-generator`, and `@shipworthy/openapi-transformer`.
|
|
562
|
+
- **Generate:** run
|
|
563
|
+
`openapi generate tanstack-query <spec> -o <dir> --name <name>` from
|
|
564
|
+
`@shipworthy/openapi-cli` (`bunx --bun @shipworthy/openapi-cli`). The output is
|
|
565
|
+
`<dir>/<name>/types.ts` and `<dir>/<name>/client.ts`. Do not edit generated
|
|
566
|
+
files.
|
|
567
|
+
- **Generated exports** (Prefix is the PascalCase of `name`):
|
|
568
|
+
`create<Prefix>ApiClient`, `create<Prefix>ApiQueryUtils`,
|
|
569
|
+
`<Prefix>ApiClient`, `<Prefix>ApiPaths`, `<Prefix>ApiComponents`, and
|
|
570
|
+
`<Prefix>ApiOperations`.
|
|
571
|
+
- **Setup:**
|
|
572
|
+
```ts
|
|
573
|
+
const api = createHotelsApiQueryUtils(createHotelsApiClient({ baseUrl, authToken }));
|
|
574
|
+
```
|
|
575
|
+
`api.<operationId>` exposes `queryOptions`, `queryKey`, `infiniteOptions`,
|
|
576
|
+
`infiniteKey`, `mutationOptions`, `mutationKey`, and `key`.
|
|
577
|
+
- **Query input:** `{ input: { path?, query?, header?, cookie?, body? } }`, or
|
|
578
|
+
`input: skipToken` to disable the query.
|
|
579
|
+
- **Mutation variables:** the grouped input directly, with no `input` wrapper.
|
|
580
|
+
- **Infinite queries:** `input` is `(pageParam) => Input`. Supply
|
|
581
|
+
`initialPageParam` and `getNextPageParam`.
|
|
582
|
+
- **Data and errors:**
|
|
583
|
+
- Data is the full 2xx body. An empty body becomes `null`.
|
|
584
|
+
- HTTP errors throw the parsed error body. An empty error body throws
|
|
585
|
+
`Error("HTTP <status> <statusText>")`.
|
|
586
|
+
- **Keys:** `[...prefix, operationId, { type, input }]`. Use `key()` for
|
|
587
|
+
broad invalidation. Auth and cookie headers are excluded from keys.
|
|
588
|
+
- **Not provided:** hooks, provider, payload validation, token refresh,
|
|
589
|
+
idempotency-key generation, and per-schema serializers.
|
|
590
|
+
|
|
591
|
+
## License
|
|
592
|
+
|
|
593
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Generator } from "@shipworthy/openapi-generator";
|
|
3
|
+
import { ParsedService } from "@shipworthy/openapi-transformer";
|
|
4
|
+
import { HttpMethod } from "@shipworthy/openapi-fetch";
|
|
5
|
+
import "@tanstack/react-query";
|
|
6
|
+
//#region src/errors.d.ts
|
|
7
|
+
declare const TanstackGenerationError_base: Schema.Class<TanstackGenerationError, Schema.TaggedStruct<"TanstackGenerationError", {
|
|
8
|
+
readonly message: Schema.String;
|
|
9
|
+
}>, import("effect/Cause").YieldableError>;
|
|
10
|
+
export declare class TanstackGenerationError extends TanstackGenerationError_base {}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/query-utils.d.ts
|
|
13
|
+
type OperationRoute = readonly [Uppercase<HttpMethod>, string];
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/generator.d.ts
|
|
16
|
+
/** operationId → [METHOD, path]. Every operation needs an id. */
|
|
17
|
+
export declare const operationRoutes: (model: ParsedService) => Effect.Effect<Record<string, OperationRoute>, TanstackGenerationError, never>;
|
|
18
|
+
export declare const tanstackQuery: Generator<string, TanstackGenerationError>;
|
|
19
|
+
//#endregion
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
//#region src/errors.ts
|
|
3
|
+
var TanstackGenerationError = class extends Schema.TaggedError()("TanstackGenerationError", { message: Schema.String }) {};
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/generator.ts
|
|
6
|
+
const blockedOperationIds = /* @__PURE__ */ new Set([
|
|
7
|
+
"__proto__",
|
|
8
|
+
"constructor",
|
|
9
|
+
"prototype"
|
|
10
|
+
]);
|
|
11
|
+
const PluginOptions = Schema.Struct({
|
|
12
|
+
prefix: Schema.optionalKey(Schema.String),
|
|
13
|
+
name: Schema.optionalKey(Schema.String)
|
|
14
|
+
});
|
|
15
|
+
function reject(message) {
|
|
16
|
+
return new TanstackGenerationError({ message });
|
|
17
|
+
}
|
|
18
|
+
/** operationId → [METHOD, path]. Every operation needs an id. */
|
|
19
|
+
const operationRoutes = Effect.fnUntraced(function* (model) {
|
|
20
|
+
const routes = Object.create(null);
|
|
21
|
+
return yield* Effect.reduce(model.operations, () => routes, Effect.fnUntraced(function* (routes, operation) {
|
|
22
|
+
const label = `${operation.method} ${operation.path}`;
|
|
23
|
+
const id = operation.operationId;
|
|
24
|
+
if (!id) return yield* reject(`${label}: Missing operationId`);
|
|
25
|
+
if (blockedOperationIds.has(id)) return yield* reject(`Unsupported operationId: ${id}`);
|
|
26
|
+
if (Object.hasOwn(routes, id)) return yield* reject(`Duplicate operationId: ${id}`);
|
|
27
|
+
routes[id] = [operation.method, operation.path];
|
|
28
|
+
return routes;
|
|
29
|
+
}));
|
|
30
|
+
});
|
|
31
|
+
const tanstackQuery = {
|
|
32
|
+
id: "tanstack-query",
|
|
33
|
+
generate: Effect.fnUntraced(function* (model, options) {
|
|
34
|
+
const decoded = yield* Effect.mapError(Schema.decodeEffect(PluginOptions)(options), (cause) => reject(cause.message));
|
|
35
|
+
const prefix = decoded.prefix ?? "";
|
|
36
|
+
return clientSource(prefix, decoded.name ?? prefix, yield* operationRoutes(model));
|
|
37
|
+
})
|
|
38
|
+
};
|
|
39
|
+
function clientSource(prefix, name, routes) {
|
|
40
|
+
return `import { createApiClient, createTanstackQueryUtils, type TanstackQueryUtilsOptions } from "@shipworthy/openapi-tanstack-query";
|
|
41
|
+
import type { operations, paths } from "./types";
|
|
42
|
+
|
|
43
|
+
export type { paths as ${prefix}ApiPaths, components as ${prefix}ApiComponents, operations as ${prefix}ApiOperations } from "./types";
|
|
44
|
+
export const create${prefix}ApiClient: typeof createApiClient<paths> = createApiClient<paths>;
|
|
45
|
+
export type ${prefix}ApiClient = ReturnType<typeof create${prefix}ApiClient>;
|
|
46
|
+
|
|
47
|
+
const routes = {
|
|
48
|
+
${Object.entries(routes).map(([id, route]) => ` ${JSON.stringify(id)}: ${JSON.stringify(route)},`).join("\n")}
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export function create${prefix}ApiQueryUtils(client: ${prefix}ApiClient, options?: Partial<TanstackQueryUtilsOptions>) {
|
|
52
|
+
return createTanstackQueryUtils<Pick<operations, keyof typeof routes>>(client, routes, { prefix: ${JSON.stringify(name)}, ...options });
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { TanstackGenerationError, operationRoutes, tanstackQuery };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _shipworthy_openapi_fetch = require("@shipworthy/openapi-fetch");
|
|
3
|
+
let _tanstack_react_query = require("@tanstack/react-query");
|
|
4
|
+
//#region src/client.ts
|
|
5
|
+
function createApiClient({ authToken, ...options }) {
|
|
6
|
+
const client = (0, _shipworthy_openapi_fetch.createClient)(options);
|
|
7
|
+
if (authToken !== void 0) client.use({ async onRequest({ request }) {
|
|
8
|
+
const token = typeof authToken === "function" ? await authToken() : authToken;
|
|
9
|
+
if (request.signal?.aborted) {
|
|
10
|
+
const error = /* @__PURE__ */ new Error("Request aborted");
|
|
11
|
+
error.name = "AbortError";
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
if (token) request.headers.set("Authorization", `Bearer ${token}`);
|
|
15
|
+
else request.headers.delete("Authorization");
|
|
16
|
+
return request;
|
|
17
|
+
} });
|
|
18
|
+
return client;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/query-utils.ts
|
|
22
|
+
function cacheInput(input = {}) {
|
|
23
|
+
const { header, ...rest } = input;
|
|
24
|
+
if (!header) return rest;
|
|
25
|
+
const entries = Object.entries(header).filter(([name, value]) => value !== void 0 && ![
|
|
26
|
+
"authorization",
|
|
27
|
+
"proxy-authorization",
|
|
28
|
+
"cookie",
|
|
29
|
+
"set-cookie"
|
|
30
|
+
].includes(name.toLowerCase()));
|
|
31
|
+
return entries.length ? {
|
|
32
|
+
...rest,
|
|
33
|
+
header: Object.fromEntries(entries.map(([name, value]) => [name.toLowerCase(), value]))
|
|
34
|
+
} : rest;
|
|
35
|
+
}
|
|
36
|
+
function createTanstackQueryUtils(client, routes, { prefix }) {
|
|
37
|
+
const namespace = typeof prefix === "string" ? [prefix] : [...prefix];
|
|
38
|
+
return Object.fromEntries(Object.entries(routes).map(([name, [method, path]]) => {
|
|
39
|
+
const request = async (input = {}, signal) => {
|
|
40
|
+
const { body, ...params } = input;
|
|
41
|
+
const { data, error, response } = await client[method](path, {
|
|
42
|
+
params,
|
|
43
|
+
body,
|
|
44
|
+
signal
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) throw error ?? /* @__PURE__ */ new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
47
|
+
return data ?? null;
|
|
48
|
+
};
|
|
49
|
+
const key = (options = {}) => {
|
|
50
|
+
const { input, ...filter } = options;
|
|
51
|
+
if (input !== void 0) filter.input = cacheInput(input);
|
|
52
|
+
return Object.keys(filter).length ? [
|
|
53
|
+
...namespace,
|
|
54
|
+
name,
|
|
55
|
+
filter
|
|
56
|
+
] : [...namespace, name];
|
|
57
|
+
};
|
|
58
|
+
const queryKey = ({ input } = {}) => key({
|
|
59
|
+
type: "query",
|
|
60
|
+
input: input ?? {}
|
|
61
|
+
});
|
|
62
|
+
const infiniteKey = ({ input, initialPageParam }) => key({
|
|
63
|
+
type: "infinite",
|
|
64
|
+
input: input(initialPageParam)
|
|
65
|
+
});
|
|
66
|
+
const mutationKey = () => key({ type: "mutation" });
|
|
67
|
+
return [name, {
|
|
68
|
+
key,
|
|
69
|
+
queryKey,
|
|
70
|
+
infiniteKey,
|
|
71
|
+
mutationKey,
|
|
72
|
+
queryOptions({ input, ...options } = {}) {
|
|
73
|
+
return {
|
|
74
|
+
...options,
|
|
75
|
+
queryKey: input === _tanstack_react_query.skipToken ? key({
|
|
76
|
+
type: "query",
|
|
77
|
+
skipped: true
|
|
78
|
+
}) : queryKey({ input }),
|
|
79
|
+
queryFn: input === _tanstack_react_query.skipToken ? _tanstack_react_query.skipToken : ({ signal }) => request(input, signal)
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
infiniteOptions({ input, ...options }) {
|
|
83
|
+
return {
|
|
84
|
+
...options,
|
|
85
|
+
queryKey: input === _tanstack_react_query.skipToken ? key({
|
|
86
|
+
type: "infinite",
|
|
87
|
+
skipped: true
|
|
88
|
+
}) : infiniteKey({
|
|
89
|
+
input,
|
|
90
|
+
initialPageParam: options.initialPageParam
|
|
91
|
+
}),
|
|
92
|
+
queryFn: input === _tanstack_react_query.skipToken ? _tanstack_react_query.skipToken : ({ pageParam, signal }) => request(input(pageParam), signal)
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
mutationOptions(options = {}) {
|
|
96
|
+
return {
|
|
97
|
+
...options,
|
|
98
|
+
mutationKey: mutationKey(),
|
|
99
|
+
mutationFn: (input) => request(input)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}];
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
exports.createApiClient = createApiClient;
|
|
107
|
+
exports.createTanstackQueryUtils = createTanstackQueryUtils;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Client, ClientOptions, ErrorResponse, HttpMethod, MediaType, RequestBodyOption, RequiredKeysOf, ResponseObjectMap, SuccessResponse } from "@shipworthy/openapi-fetch";
|
|
2
|
+
import { DataTag, InfiniteData, InfiniteQueryPageParamsOptions, QueryFunction, QueryKey, QueryObserverOptions, SkipToken, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
3
|
+
//#region src/client.d.ts
|
|
4
|
+
type AuthToken = string | undefined | null;
|
|
5
|
+
type AuthTokenProvider = () => AuthToken | Promise<AuthToken>;
|
|
6
|
+
interface ApiClientOptions extends Omit<ClientOptions, "baseUrl"> {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
authToken?: AuthToken | AuthTokenProvider;
|
|
9
|
+
}
|
|
10
|
+
export declare function createApiClient<Paths extends {}>({ authToken, ...options }: ApiClientOptions): Client<Paths>;
|
|
11
|
+
type ApiClient<Paths extends {} = {}> = Client<Paths>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/operation.d.ts
|
|
14
|
+
/** Preserve OpenAPI's parameter groups and add its typed request body. */
|
|
15
|
+
type OperationInput<Op> = (Op extends {
|
|
16
|
+
parameters: infer Parameters;
|
|
17
|
+
} ? Parameters : {}) & RequestBodyOption<Op>;
|
|
18
|
+
type Responses<Op> = Extract<ResponseObjectMap<Op>, Record<string | number, unknown>>;
|
|
19
|
+
type NormalizeData<T> = [T] extends [never] ? null : Exclude<T, undefined> | (undefined extends T ? null : never);
|
|
20
|
+
type JsonResponses<Op> = { [Status in keyof Responses<Op>]: Responses<Op>[Status] extends {
|
|
21
|
+
content: {
|
|
22
|
+
"application/json": infer Data;
|
|
23
|
+
};
|
|
24
|
+
} ? {
|
|
25
|
+
content: {
|
|
26
|
+
"application/json": Data;
|
|
27
|
+
};
|
|
28
|
+
} : Responses<Op>[Status]; };
|
|
29
|
+
type OperationData<Op> = NormalizeData<SuccessResponse<JsonResponses<Op>, MediaType>>;
|
|
30
|
+
type OperationError<Op> = ErrorResponse<Responses<Op>, MediaType> | Error;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/query-utils.d.ts
|
|
33
|
+
type InputOf<O> = OperationInput<O>;
|
|
34
|
+
type DataOf<O> = OperationData<O>;
|
|
35
|
+
type ErrorOf<O> = OperationError<O>;
|
|
36
|
+
type Field<K extends string, T> = RequiredKeysOf<T> extends never ? { [P in K]?: T; } : { [P in K]: T; };
|
|
37
|
+
type Request<O> = Field<"input", InputOf<O>>;
|
|
38
|
+
type Args<T> = RequiredKeysOf<T> extends never ? [options?: T] : [options: T];
|
|
39
|
+
type Variables<O> = InputOf<O>;
|
|
40
|
+
type PartialInput<T> = T extends readonly unknown[] ? T : T extends object ? { [K in keyof T]?: PartialInput<T[K]>; } : T;
|
|
41
|
+
type Key<Data> = DataTag<QueryKey, Data>;
|
|
42
|
+
type QueryConfig<O, Selected> = Omit<UseQueryOptions<DataOf<O>, ErrorOf<O>, Selected, Key<DataOf<O>>>, "queryKey" | "queryFn">;
|
|
43
|
+
type QueryResult<O, Selected, Fn> = QueryConfig<O, Selected> & {
|
|
44
|
+
queryKey: Key<DataOf<O>>;
|
|
45
|
+
queryFn: Fn;
|
|
46
|
+
};
|
|
47
|
+
type InfiniteConfig<O, Page, Selected> = Omit<QueryObserverOptions<DataOf<O>, ErrorOf<O>, Selected, InfiniteData<DataOf<O>, Page>, Key<InfiniteData<DataOf<O>, Page>>, Page> & InfiniteQueryPageParamsOptions<DataOf<O>, Page>, "queryKey" | "queryFn" | "suspense">;
|
|
48
|
+
type InfiniteResult<O, Page, Selected, Fn> = InfiniteConfig<O, Page, Selected> & {
|
|
49
|
+
queryKey: Key<InfiniteData<DataOf<O>, Page>>;
|
|
50
|
+
queryFn: Fn;
|
|
51
|
+
};
|
|
52
|
+
type InfiniteInput<O, Page> = {
|
|
53
|
+
input: (pageParam: Page) => InputOf<O>;
|
|
54
|
+
initialPageParam: Page;
|
|
55
|
+
};
|
|
56
|
+
interface OperationQueryUtils<O> {
|
|
57
|
+
key(options?: {
|
|
58
|
+
type?: "query" | "infinite" | "mutation";
|
|
59
|
+
input?: PartialInput<InputOf<O>>;
|
|
60
|
+
}): QueryKey;
|
|
61
|
+
queryKey(...args: Args<Request<O>>): Key<DataOf<O>>;
|
|
62
|
+
queryOptions<Selected = DataOf<O>>(...args: Args<Request<O> & QueryConfig<O, Selected>>): QueryResult<O, Selected, QueryFunction<DataOf<O>, Key<DataOf<O>>>>;
|
|
63
|
+
queryOptions<Selected = DataOf<O>>(options: Omit<Request<O>, "input"> & {
|
|
64
|
+
input: InputOf<O> | SkipToken;
|
|
65
|
+
} & QueryConfig<O, Selected>): QueryResult<O, Selected, QueryFunction<DataOf<O>, Key<DataOf<O>>> | SkipToken>;
|
|
66
|
+
infiniteKey<Page>(options: InfiniteInput<O, Page>): Key<InfiniteData<DataOf<O>, Page>>;
|
|
67
|
+
infiniteOptions<Page, Selected = InfiniteData<DataOf<O>, Page>>(options: InfiniteInput<O, Page> & InfiniteConfig<O, Page, Selected>): InfiniteResult<O, Page, Selected, QueryFunction<DataOf<O>, Key<InfiniteData<DataOf<O>, Page>>, Page>>;
|
|
68
|
+
infiniteOptions<Page, Selected = InfiniteData<DataOf<O>, Page>>(options: Omit<InfiniteInput<O, Page>, "input"> & {
|
|
69
|
+
input: ((pageParam: Page) => InputOf<O>) | SkipToken;
|
|
70
|
+
} & InfiniteConfig<O, Page, Selected>): InfiniteResult<O, Page, Selected, QueryFunction<DataOf<O>, Key<InfiniteData<DataOf<O>, Page>>, Page> | SkipToken>;
|
|
71
|
+
mutationKey(): QueryKey;
|
|
72
|
+
mutationOptions<Context = unknown>(options?: Omit<UseMutationOptions<DataOf<O>, ErrorOf<O>, Variables<O>, Context>, "mutationKey" | "mutationFn">): UseMutationOptions<DataOf<O>, ErrorOf<O>, Variables<O>, Context> & {
|
|
73
|
+
mutationKey: QueryKey;
|
|
74
|
+
mutationFn: (variables: Variables<O>) => Promise<DataOf<O>>;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
interface TanstackQueryUtilsOptions {
|
|
78
|
+
prefix: string | readonly string[];
|
|
79
|
+
}
|
|
80
|
+
type OperationRoute = readonly [Uppercase<HttpMethod>, string];
|
|
81
|
+
export declare function createTanstackQueryUtils<Operations extends Record<string, any>>(client: Client<any>, routes: { [Name in keyof Operations]: OperationRoute; }, { prefix }: TanstackQueryUtilsOptions): { [Name in keyof Operations]: OperationQueryUtils<Operations[Name]>; };
|
|
82
|
+
//#endregion
|
|
83
|
+
export type { ApiClient, ApiClientOptions, AuthToken, AuthTokenProvider, OperationData, OperationError, OperationInput, OperationQueryUtils, OperationRoute, TanstackQueryUtilsOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Client, ClientOptions, ErrorResponse, HttpMethod, MediaType, RequestBodyOption, RequiredKeysOf, ResponseObjectMap, SuccessResponse } from "@shipworthy/openapi-fetch";
|
|
2
|
+
import { DataTag, InfiniteData, InfiniteQueryPageParamsOptions, QueryFunction, QueryKey, QueryObserverOptions, SkipToken, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
3
|
+
//#region src/client.d.ts
|
|
4
|
+
type AuthToken = string | undefined | null;
|
|
5
|
+
type AuthTokenProvider = () => AuthToken | Promise<AuthToken>;
|
|
6
|
+
interface ApiClientOptions extends Omit<ClientOptions, "baseUrl"> {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
authToken?: AuthToken | AuthTokenProvider;
|
|
9
|
+
}
|
|
10
|
+
export declare function createApiClient<Paths extends {}>({ authToken, ...options }: ApiClientOptions): Client<Paths>;
|
|
11
|
+
type ApiClient<Paths extends {} = {}> = Client<Paths>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/operation.d.ts
|
|
14
|
+
/** Preserve OpenAPI's parameter groups and add its typed request body. */
|
|
15
|
+
type OperationInput<Op> = (Op extends {
|
|
16
|
+
parameters: infer Parameters;
|
|
17
|
+
} ? Parameters : {}) & RequestBodyOption<Op>;
|
|
18
|
+
type Responses<Op> = Extract<ResponseObjectMap<Op>, Record<string | number, unknown>>;
|
|
19
|
+
type NormalizeData<T> = [T] extends [never] ? null : Exclude<T, undefined> | (undefined extends T ? null : never);
|
|
20
|
+
type JsonResponses<Op> = { [Status in keyof Responses<Op>]: Responses<Op>[Status] extends {
|
|
21
|
+
content: {
|
|
22
|
+
"application/json": infer Data;
|
|
23
|
+
};
|
|
24
|
+
} ? {
|
|
25
|
+
content: {
|
|
26
|
+
"application/json": Data;
|
|
27
|
+
};
|
|
28
|
+
} : Responses<Op>[Status]; };
|
|
29
|
+
type OperationData<Op> = NormalizeData<SuccessResponse<JsonResponses<Op>, MediaType>>;
|
|
30
|
+
type OperationError<Op> = ErrorResponse<Responses<Op>, MediaType> | Error;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/query-utils.d.ts
|
|
33
|
+
type InputOf<O> = OperationInput<O>;
|
|
34
|
+
type DataOf<O> = OperationData<O>;
|
|
35
|
+
type ErrorOf<O> = OperationError<O>;
|
|
36
|
+
type Field<K extends string, T> = RequiredKeysOf<T> extends never ? { [P in K]?: T; } : { [P in K]: T; };
|
|
37
|
+
type Request<O> = Field<"input", InputOf<O>>;
|
|
38
|
+
type Args<T> = RequiredKeysOf<T> extends never ? [options?: T] : [options: T];
|
|
39
|
+
type Variables<O> = InputOf<O>;
|
|
40
|
+
type PartialInput<T> = T extends readonly unknown[] ? T : T extends object ? { [K in keyof T]?: PartialInput<T[K]>; } : T;
|
|
41
|
+
type Key<Data> = DataTag<QueryKey, Data>;
|
|
42
|
+
type QueryConfig<O, Selected> = Omit<UseQueryOptions<DataOf<O>, ErrorOf<O>, Selected, Key<DataOf<O>>>, "queryKey" | "queryFn">;
|
|
43
|
+
type QueryResult<O, Selected, Fn> = QueryConfig<O, Selected> & {
|
|
44
|
+
queryKey: Key<DataOf<O>>;
|
|
45
|
+
queryFn: Fn;
|
|
46
|
+
};
|
|
47
|
+
type InfiniteConfig<O, Page, Selected> = Omit<QueryObserverOptions<DataOf<O>, ErrorOf<O>, Selected, InfiniteData<DataOf<O>, Page>, Key<InfiniteData<DataOf<O>, Page>>, Page> & InfiniteQueryPageParamsOptions<DataOf<O>, Page>, "queryKey" | "queryFn" | "suspense">;
|
|
48
|
+
type InfiniteResult<O, Page, Selected, Fn> = InfiniteConfig<O, Page, Selected> & {
|
|
49
|
+
queryKey: Key<InfiniteData<DataOf<O>, Page>>;
|
|
50
|
+
queryFn: Fn;
|
|
51
|
+
};
|
|
52
|
+
type InfiniteInput<O, Page> = {
|
|
53
|
+
input: (pageParam: Page) => InputOf<O>;
|
|
54
|
+
initialPageParam: Page;
|
|
55
|
+
};
|
|
56
|
+
interface OperationQueryUtils<O> {
|
|
57
|
+
key(options?: {
|
|
58
|
+
type?: "query" | "infinite" | "mutation";
|
|
59
|
+
input?: PartialInput<InputOf<O>>;
|
|
60
|
+
}): QueryKey;
|
|
61
|
+
queryKey(...args: Args<Request<O>>): Key<DataOf<O>>;
|
|
62
|
+
queryOptions<Selected = DataOf<O>>(...args: Args<Request<O> & QueryConfig<O, Selected>>): QueryResult<O, Selected, QueryFunction<DataOf<O>, Key<DataOf<O>>>>;
|
|
63
|
+
queryOptions<Selected = DataOf<O>>(options: Omit<Request<O>, "input"> & {
|
|
64
|
+
input: InputOf<O> | SkipToken;
|
|
65
|
+
} & QueryConfig<O, Selected>): QueryResult<O, Selected, QueryFunction<DataOf<O>, Key<DataOf<O>>> | SkipToken>;
|
|
66
|
+
infiniteKey<Page>(options: InfiniteInput<O, Page>): Key<InfiniteData<DataOf<O>, Page>>;
|
|
67
|
+
infiniteOptions<Page, Selected = InfiniteData<DataOf<O>, Page>>(options: InfiniteInput<O, Page> & InfiniteConfig<O, Page, Selected>): InfiniteResult<O, Page, Selected, QueryFunction<DataOf<O>, Key<InfiniteData<DataOf<O>, Page>>, Page>>;
|
|
68
|
+
infiniteOptions<Page, Selected = InfiniteData<DataOf<O>, Page>>(options: Omit<InfiniteInput<O, Page>, "input"> & {
|
|
69
|
+
input: ((pageParam: Page) => InputOf<O>) | SkipToken;
|
|
70
|
+
} & InfiniteConfig<O, Page, Selected>): InfiniteResult<O, Page, Selected, QueryFunction<DataOf<O>, Key<InfiniteData<DataOf<O>, Page>>, Page> | SkipToken>;
|
|
71
|
+
mutationKey(): QueryKey;
|
|
72
|
+
mutationOptions<Context = unknown>(options?: Omit<UseMutationOptions<DataOf<O>, ErrorOf<O>, Variables<O>, Context>, "mutationKey" | "mutationFn">): UseMutationOptions<DataOf<O>, ErrorOf<O>, Variables<O>, Context> & {
|
|
73
|
+
mutationKey: QueryKey;
|
|
74
|
+
mutationFn: (variables: Variables<O>) => Promise<DataOf<O>>;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
interface TanstackQueryUtilsOptions {
|
|
78
|
+
prefix: string | readonly string[];
|
|
79
|
+
}
|
|
80
|
+
type OperationRoute = readonly [Uppercase<HttpMethod>, string];
|
|
81
|
+
export declare function createTanstackQueryUtils<Operations extends Record<string, any>>(client: Client<any>, routes: { [Name in keyof Operations]: OperationRoute; }, { prefix }: TanstackQueryUtilsOptions): { [Name in keyof Operations]: OperationQueryUtils<Operations[Name]>; };
|
|
82
|
+
//#endregion
|
|
83
|
+
export type { ApiClient, ApiClientOptions, AuthToken, AuthTokenProvider, OperationData, OperationError, OperationInput, OperationQueryUtils, OperationRoute, TanstackQueryUtilsOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createClient } from "@shipworthy/openapi-fetch";
|
|
2
|
+
import { skipToken } from "@tanstack/react-query";
|
|
3
|
+
//#region src/client.ts
|
|
4
|
+
function createApiClient({ authToken, ...options }) {
|
|
5
|
+
const client = createClient(options);
|
|
6
|
+
if (authToken !== void 0) client.use({ async onRequest({ request }) {
|
|
7
|
+
const token = typeof authToken === "function" ? await authToken() : authToken;
|
|
8
|
+
if (request.signal?.aborted) {
|
|
9
|
+
const error = /* @__PURE__ */ new Error("Request aborted");
|
|
10
|
+
error.name = "AbortError";
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
if (token) request.headers.set("Authorization", `Bearer ${token}`);
|
|
14
|
+
else request.headers.delete("Authorization");
|
|
15
|
+
return request;
|
|
16
|
+
} });
|
|
17
|
+
return client;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/query-utils.ts
|
|
21
|
+
function cacheInput(input = {}) {
|
|
22
|
+
const { header, ...rest } = input;
|
|
23
|
+
if (!header) return rest;
|
|
24
|
+
const entries = Object.entries(header).filter(([name, value]) => value !== void 0 && ![
|
|
25
|
+
"authorization",
|
|
26
|
+
"proxy-authorization",
|
|
27
|
+
"cookie",
|
|
28
|
+
"set-cookie"
|
|
29
|
+
].includes(name.toLowerCase()));
|
|
30
|
+
return entries.length ? {
|
|
31
|
+
...rest,
|
|
32
|
+
header: Object.fromEntries(entries.map(([name, value]) => [name.toLowerCase(), value]))
|
|
33
|
+
} : rest;
|
|
34
|
+
}
|
|
35
|
+
function createTanstackQueryUtils(client, routes, { prefix }) {
|
|
36
|
+
const namespace = typeof prefix === "string" ? [prefix] : [...prefix];
|
|
37
|
+
return Object.fromEntries(Object.entries(routes).map(([name, [method, path]]) => {
|
|
38
|
+
const request = async (input = {}, signal) => {
|
|
39
|
+
const { body, ...params } = input;
|
|
40
|
+
const { data, error, response } = await client[method](path, {
|
|
41
|
+
params,
|
|
42
|
+
body,
|
|
43
|
+
signal
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) throw error ?? /* @__PURE__ */ new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
46
|
+
return data ?? null;
|
|
47
|
+
};
|
|
48
|
+
const key = (options = {}) => {
|
|
49
|
+
const { input, ...filter } = options;
|
|
50
|
+
if (input !== void 0) filter.input = cacheInput(input);
|
|
51
|
+
return Object.keys(filter).length ? [
|
|
52
|
+
...namespace,
|
|
53
|
+
name,
|
|
54
|
+
filter
|
|
55
|
+
] : [...namespace, name];
|
|
56
|
+
};
|
|
57
|
+
const queryKey = ({ input } = {}) => key({
|
|
58
|
+
type: "query",
|
|
59
|
+
input: input ?? {}
|
|
60
|
+
});
|
|
61
|
+
const infiniteKey = ({ input, initialPageParam }) => key({
|
|
62
|
+
type: "infinite",
|
|
63
|
+
input: input(initialPageParam)
|
|
64
|
+
});
|
|
65
|
+
const mutationKey = () => key({ type: "mutation" });
|
|
66
|
+
return [name, {
|
|
67
|
+
key,
|
|
68
|
+
queryKey,
|
|
69
|
+
infiniteKey,
|
|
70
|
+
mutationKey,
|
|
71
|
+
queryOptions({ input, ...options } = {}) {
|
|
72
|
+
return {
|
|
73
|
+
...options,
|
|
74
|
+
queryKey: input === skipToken ? key({
|
|
75
|
+
type: "query",
|
|
76
|
+
skipped: true
|
|
77
|
+
}) : queryKey({ input }),
|
|
78
|
+
queryFn: input === skipToken ? skipToken : ({ signal }) => request(input, signal)
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
infiniteOptions({ input, ...options }) {
|
|
82
|
+
return {
|
|
83
|
+
...options,
|
|
84
|
+
queryKey: input === skipToken ? key({
|
|
85
|
+
type: "infinite",
|
|
86
|
+
skipped: true
|
|
87
|
+
}) : infiniteKey({
|
|
88
|
+
input,
|
|
89
|
+
initialPageParam: options.initialPageParam
|
|
90
|
+
}),
|
|
91
|
+
queryFn: input === skipToken ? skipToken : ({ pageParam, signal }) => request(input(pageParam), signal)
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
mutationOptions(options = {}) {
|
|
95
|
+
return {
|
|
96
|
+
...options,
|
|
97
|
+
mutationKey: mutationKey(),
|
|
98
|
+
mutationFn: (input) => request(input)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}];
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { createApiClient, createTanstackQueryUtils };
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipworthy/openapi-tanstack-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
|
+
"@shipworthy/openapi-generator": "0.0.1",
|
|
12
|
+
"@shipworthy/openapi-transformer": "0.0.1",
|
|
13
|
+
"@tanstack/react-query": "^5.59.0",
|
|
14
|
+
"effect": "^4.0.0",
|
|
15
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/ShipWorthyAI/openapi.git",
|
|
21
|
+
"directory": "packages/openapi-tanstack-query"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"react-native": "./dist/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./dist/index.d.cts",
|
|
33
|
+
"default": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./generator": {
|
|
37
|
+
"types": "./dist/generator.d.ts",
|
|
38
|
+
"default": "./dist/generator.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "rm -rf dist && tsdown",
|
|
44
|
+
"test": "vitest run"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@shipworthy/openapi-generator": "0.0.1",
|
|
48
|
+
"@shipworthy/openapi-transformer": "0.0.1",
|
|
49
|
+
"@shipworthy/typescript-config": "0.0.1",
|
|
50
|
+
"@tanstack/react-query": "^5.104.1",
|
|
51
|
+
"@types/node": "^24.10.3",
|
|
52
|
+
"effect": "^4.0.0",
|
|
53
|
+
"react": "^19.0.0",
|
|
54
|
+
"tsdown": "0.23.0",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
|
+
"vitest": "5.0.3"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@shipworthy/openapi-generator": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@shipworthy/openapi-transformer": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"effect": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"main": "./dist/index.cjs",
|
|
70
|
+
"module": "./dist/index.js",
|
|
71
|
+
"types": "./dist/index.d.cts",
|
|
72
|
+
"publishConfig": {
|
|
73
|
+
"access": "public"
|
|
74
|
+
}
|
|
75
|
+
}
|