@kubb/plugin-client 6.0.3 → 6.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2 -2
- package/dist/index.js +2 -2
- package/docs/guides/calling-operations.md +20 -17
- package/docs/guides/migrate-to-v6.md +31 -38
- package/docs/guides/streaming.md +106 -0
- package/docs/guides/validate-requests-and-responses.md +33 -25
- package/docs/guides/write-your-client.md +172 -104
- package/docs/options.md +48 -45
- package/docs/overview.md +15 -9
- package/kubb-extension.json +1 -1
- package/package.json +3 -3
- package/docs/guides/authentication.md +0 -75
package/dist/index.cjs
CHANGED
|
@@ -33,11 +33,11 @@ let _kubb_renderer_jsx_jsx_runtime = require("@kubb/renderer-jsx/jsx-runtime");
|
|
|
33
33
|
let node_url = require("node:url");
|
|
34
34
|
// kubb-extension.json
|
|
35
35
|
var compatibility = {
|
|
36
|
-
"kubb": ">=5.
|
|
36
|
+
"kubb": ">=5.19.0",
|
|
37
37
|
"node": ">=22.22"
|
|
38
38
|
};
|
|
39
39
|
// package.json
|
|
40
|
-
var version = "6.0.
|
|
40
|
+
var version = "6.0.4";
|
|
41
41
|
// ../../internals/utils/src/reserved.ts
|
|
42
42
|
/**
|
|
43
43
|
* JavaScript and Java reserved words.
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,11 @@ import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
// kubb-extension.json
|
|
9
9
|
var compatibility = {
|
|
10
|
-
"kubb": ">=5.
|
|
10
|
+
"kubb": ">=5.19.0",
|
|
11
11
|
"node": ">=22.22"
|
|
12
12
|
};
|
|
13
13
|
// package.json
|
|
14
|
-
var version = "6.0.
|
|
14
|
+
var version = "6.0.4";
|
|
15
15
|
// ../../internals/utils/src/reserved.ts
|
|
16
16
|
/**
|
|
17
17
|
* JavaScript and Java reserved words.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Call operations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Call a generated function with one grouped options object. It returns your client's promise, shaped by your `RequestResult` type.
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
6
|
import { addPet, getPetById } from './gen/clients'
|
|
@@ -11,31 +11,34 @@ console.log(created.data, created.response.status)
|
|
|
11
11
|
const pet = await getPetById({ path: { petId: 1 } })
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The examples set [`integerType: 'number'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#integertype) on the adapter
|
|
14
|
+
The examples set [`integerType: 'number'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#integertype) on the adapter. Without it, `petId` is a `bigint` and takes `1n`.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
| Key | Holds |
|
|
17
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `path`, `query`, `headers`, `cookies`, `body` | The request values, typed from the spec. |
|
|
19
|
+
| `throwOnError` | Overrides [`throwOnErrorDefault`](https://kubb.dev/extensions/kubb-labs/plugin-client/options#throwonerrordefault) for this call. |
|
|
20
|
+
| `client` | Another client function for this call, such as a signed or per-tenant client. |
|
|
21
|
+
| Any other key | Passed to your client as is, such as `signal`. |
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
Path keys keep their spec names unless you set [`pathParamsCasing`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#pathparamscasing). See [Path parameter names](#path-parameter-names).
|
|
19
24
|
|
|
20
|
-
##
|
|
25
|
+
## Handle errors
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
Your client decides what `throwOnError` does. With the [client from scratch](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client#write-the-client), a non-2xx status throws, and `throwOnError: false` returns the error as a value:
|
|
23
28
|
|
|
24
29
|
```typescript
|
|
25
30
|
const result = await getPetById({ path: { petId: 1 }, throwOnError: false })
|
|
26
31
|
|
|
27
|
-
if (result.error === undefined)
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
if (result.error !== undefined) {
|
|
31
|
-
console.log('failed', result.response.status)
|
|
32
|
-
}
|
|
32
|
+
if (result.error === undefined) console.log('pet', result.data)
|
|
33
|
+
else console.log('failed', result.response.status)
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
Next, [validate requests and responses](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/validate-requests-and-responses).
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
## Less common cases
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
### Path parameter names
|
|
41
|
+
|
|
42
|
+
When the spec names a path parameter in kebab or snake case, such as `{pet-id}`, the path key keeps that name: pass `path: { 'pet-id': 1 }`.
|
|
43
|
+
|
|
44
|
+
Set [`pathParamsCasing: 'camelcase'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#pathparamscasing) on the adapter (`@kubb/adapter-oas` 5.20.0 or later) to pass `path: { petId: 1 }` instead. The HTTP request stays the same.
|
|
@@ -1,45 +1,35 @@
|
|
|
1
1
|
# Migrate to v6
|
|
2
2
|
|
|
3
|
-
`@kubb/plugin-client` 6 calls your client with type arguments and streams through `client.stream`.
|
|
3
|
+
Update your client module for `@kubb/plugin-client` 6, which calls your client with type arguments and streams through `client.stream`.
|
|
4
4
|
|
|
5
5
|
## What to change
|
|
6
6
|
|
|
7
|
-
| Your code | What to do
|
|
8
|
-
| ------------------------------------------------------------------------------- |
|
|
9
|
-
| Exports a `client` that takes no type arguments | Make it
|
|
10
|
-
| Exports `toEventStream` or `EventStreamResult` | Add a
|
|
11
|
-
| Declares `responseType` on `RequestConfig`, or lacks `contentType` or `cookies` | Add the new fields
|
|
12
|
-
| Reads `security` entries as single schemes | Handle arrays and the new fields
|
|
13
|
-
| Imports a `…Status2XX` type, or writes a `'2XX'` key | Write
|
|
14
|
-
| Groups by tag in an OpenAPI 3.2 document whose tags set `parent` | Update the import paths
|
|
15
|
-
| Has `servers` on a path or operation, or reads XML, YAML, CSV, or binary bodies | Check [
|
|
16
|
-
| Wants `.unwrap()` or a call that resolves to the body | Set `returnType
|
|
7
|
+
| Your code | What to do |
|
|
8
|
+
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| Exports a `client` that takes no type arguments | Make it [generic](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#a-generic-client-call). |
|
|
10
|
+
| Exports `toEventStream` or `EventStreamResult` | Add a [stream method](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#a-stream-method-instead-of-toeventstream). |
|
|
11
|
+
| Declares `responseType` on `RequestConfig`, or lacks `contentType` or `cookies` | Add the [new fields](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#new-fields-on-the-request-config). |
|
|
12
|
+
| Reads `security` entries as single schemes | Handle [arrays and the new fields](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#security-requirements). |
|
|
13
|
+
| Imports a `…Status2XX` type, or writes a `'2XX'` key | Write [ranges in lower case](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#status-ranges). |
|
|
14
|
+
| Groups by tag in an OpenAPI 3.2 document whose tags set `parent` | Update the [import paths](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#tag-folders). |
|
|
15
|
+
| Has `servers` on a path or operation, or reads XML, YAML, CSV, or binary bodies | It compiles. Check the [behavior changes](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#behavior-changes). |
|
|
16
|
+
| Wants `.unwrap()` or a call that resolves to the body | Optional. Set [`returnType`](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/migrate-to-v6#return-type). |
|
|
17
17
|
|
|
18
18
|
## Upgrade steps
|
|
19
19
|
|
|
20
|
-
1. Upgrade `kubb` and `@kubb/plugin-client` together. v6 needs Kubb 5.
|
|
20
|
+
1. Upgrade `kubb` and `@kubb/plugin-client` together. v6 needs Kubb 5.19.0 or later:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm install --save-dev kubb@latest @kubb/plugin-client@6
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
2. Update your client module with the sections below.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npx kubb generate
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
4. Run the type checker. A missing export or field in your client module shows up as a compile error in the generated files:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npx tsc --noEmit
|
|
38
|
-
```
|
|
27
|
+
3. Regenerate with `npx kubb generate`.
|
|
28
|
+
4. Run `npx tsc --noEmit`. A missing export or field shows up as a compile error in the generated files.
|
|
39
29
|
|
|
40
30
|
## A generic client call
|
|
41
31
|
|
|
42
|
-
A generated function
|
|
32
|
+
A generated function calls `client<TResponses, ThrowOnError>(config)`:
|
|
43
33
|
|
|
44
34
|
```typescript
|
|
45
35
|
export const client = <TResponses, ThrowOnError extends boolean = true>(config: RequestConfig) =>
|
|
@@ -67,7 +57,7 @@ client.stream = async <TResponses, ThrowOnError extends boolean = true, TEvent =
|
|
|
67
57
|
}
|
|
68
58
|
```
|
|
69
59
|
|
|
70
|
-
|
|
60
|
+
See [Stream Server-Sent Events and JSON Lines](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/streaming) for every stream field and for JSON Lines.
|
|
71
61
|
|
|
72
62
|
## New fields on the request config
|
|
73
63
|
|
|
@@ -79,21 +69,22 @@ export type RequestConfig = {
|
|
|
79
69
|
contentType?: string | { request?: string; response?: string }
|
|
80
70
|
// Replaces `responseType`. Binary downloads pass 'blob'.
|
|
81
71
|
parseAs?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream'
|
|
72
|
+
// Server-Sent Events only.
|
|
82
73
|
eventShape?: 'event' | 'data'
|
|
83
|
-
|
|
74
|
+
rawData?: boolean | Array<string>
|
|
75
|
+
// Name and value pairs for `in: cookie` parameters. Join them into a `Cookie` header on the server.
|
|
84
76
|
cookies?: object
|
|
85
77
|
// `style`, `explode`, `allowReserved`, and `contentType` per parameter, grouped by location.
|
|
86
78
|
styles?: object
|
|
87
79
|
}
|
|
88
80
|
```
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
> [!NOTE]
|
|
83
|
+
> Browsers ignore a `Cookie` header set by script and send their own cookies. See [Cookie parameters with the cookie style](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client#cookie-parameters-with-the-cookie-style) for the OpenAPI 3.2 `cookie` style.
|
|
91
84
|
|
|
92
85
|
## Security requirements
|
|
93
86
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
An `oauth2` scheme now also passes `flows` and `metadataUrl`, and an `openIdConnect` scheme passes `url`. Accept them on your auth type:
|
|
87
|
+
An entry is an array when one requirement needs several schemes together, such as an API key and a session cookie. Send all of them, or skip the entry. `oauth2` now also passes `flows` and `metadataUrl`, and `openIdConnect` passes `url`:
|
|
97
88
|
|
|
98
89
|
```typescript
|
|
99
90
|
export type Auth = {
|
|
@@ -110,7 +101,7 @@ export type Auth = {
|
|
|
110
101
|
|
|
111
102
|
## Status ranges
|
|
112
103
|
|
|
113
|
-
|
|
104
|
+
Generated names and keys write status ranges in lower case. Rename the imports and any range keys you wrote by hand in a responses record or `validator.error` map. An upper-case key such as `'2XX'` is no longer read:
|
|
114
105
|
|
|
115
106
|
```diff
|
|
116
107
|
- import type { ListPetsStatus4XX } from './gen/types/ListPets'
|
|
@@ -119,7 +110,7 @@ A status range is written in lower case in generated names and keys. Rename the
|
|
|
119
110
|
|
|
120
111
|
## Tag folders
|
|
121
112
|
|
|
122
|
-
With `group: { type: 'tag' }`, a tag whose OpenAPI 3.2 `parent` is `store` now writes to `store/payment
|
|
113
|
+
With `group: { type: 'tag' }`, a tag whose OpenAPI 3.2 `parent` is `store` now writes to `store/payment/`, not `payment/`. One relative `importPath` cannot serve both folder depths. Pass `group.name` to keep one folder per tag, or set `importPath` to a package name or alias:
|
|
123
114
|
|
|
124
115
|
```typescript
|
|
125
116
|
pluginClient({ importPath: '../../../client', group: { type: 'tag', name: ({ group }) => group } })
|
|
@@ -129,11 +120,13 @@ pluginClient({ importPath: '../../../client', group: { type: 'tag', name: ({ gro
|
|
|
129
120
|
|
|
130
121
|
These compile without errors, so check them by hand:
|
|
131
122
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
| Change | What your client must do |
|
|
124
|
+
| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
125
|
+
| A path or operation with its own `servers` passes that server as `baseURL`. | Use `config.baseURL` when set. |
|
|
126
|
+
| A `2xx` key is a success, and an exact code such as `200` wins over a range before it. | Nothing, unless you read the responses record by hand. |
|
|
127
|
+
| XML, YAML, and CSV responses are typed `string`. Image, audio, video, PDF, and `application/octet-stream` responses are `Blob`. | Read a `Blob` when the call passes `parseAs: 'blob'`, which it does when every success type is binary. |
|
|
128
|
+
| An operation whose only success type is JSON Lines returns a `StreamResult`. | Parse one item per line in `client.stream`. |
|
|
136
129
|
|
|
137
130
|
## Return type
|
|
138
131
|
|
|
139
|
-
`returnType` is new and defaults to `'plain'`, so existing code keeps working.
|
|
132
|
+
`returnType` is new and defaults to `'plain'`, so existing code keeps working. `'full'` adds `.unwrap()`, and `'data'` resolves to the body when `throwOnError` is `true`. Both `'full'` and `'data'` import extra helpers from your module. See [returnType](https://kubb.dev/extensions/kubb-labs/plugin-client/options#returntype).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Stream Server-Sent Events and JSON Lines
|
|
2
|
+
|
|
3
|
+
Add `client.stream` and its types to your client module, so streamed operations compile and yield typed events or lines.
|
|
4
|
+
|
|
5
|
+
## Add a stream method
|
|
6
|
+
|
|
7
|
+
When the spec has a Server-Sent Events or JSON Lines operation, its generated function calls `client.stream` instead of `client`:
|
|
8
|
+
|
|
9
|
+
::code-group
|
|
10
|
+
|
|
11
|
+
```typescript [src/gen/clients/chat/streamChat.ts]
|
|
12
|
+
import type { Options, StreamResult, SuccessOf, TypedEvent } from '../../../client'
|
|
13
|
+
import type { StreamChatOptions, StreamChatResponses } from '../../types/StreamChat'
|
|
14
|
+
import { client } from '../../../client'
|
|
15
|
+
|
|
16
|
+
export function streamChat<ThrowOnError extends boolean = true>(
|
|
17
|
+
options: Options<StreamChatOptions, ThrowOnError> = {},
|
|
18
|
+
): Promise<StreamResult<StreamChatResponses, ThrowOnError, TypedEvent<SuccessOf<StreamChatResponses>>>> {
|
|
19
|
+
const { client: apiClient = client, ...config } = options
|
|
20
|
+
|
|
21
|
+
return apiClient.stream<StreamChatResponses, ThrowOnError, TypedEvent<SuccessOf<StreamChatResponses>>>({
|
|
22
|
+
method: 'POST',
|
|
23
|
+
url: '/chat',
|
|
24
|
+
contentType: { response: 'text/event-stream' },
|
|
25
|
+
eventShape: 'event',
|
|
26
|
+
rawData: true,
|
|
27
|
+
...config,
|
|
28
|
+
throwOnError: config.throwOnError ?? true,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```typescript [src/client.ts]
|
|
34
|
+
export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226' | '2xx'>]
|
|
35
|
+
|
|
36
|
+
export type ServerSentEvent<TData = unknown> = { data: TData; event: string; id?: string; retry?: number }
|
|
37
|
+
|
|
38
|
+
export type TypedEvent<TEvent> = TEvent & Pick<ServerSentEvent, 'id' | 'retry'>
|
|
39
|
+
|
|
40
|
+
export type StreamResult<TResponses, ThrowOnError extends boolean = true, TEvent = ServerSentEvent<SuccessOf<TResponses>>> = {
|
|
41
|
+
stream: AsyncGenerator<TEvent>
|
|
42
|
+
response: Response
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
client.stream = async <TResponses, ThrowOnError extends boolean = true, TEvent = ServerSentEvent<SuccessOf<TResponses>>>(
|
|
46
|
+
config: RequestConfig,
|
|
47
|
+
): Promise<StreamResult<TResponses, ThrowOnError, TEvent>> => {
|
|
48
|
+
// send the request, then parse the body into TEvent values
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
::
|
|
53
|
+
|
|
54
|
+
| Call | Must return |
|
|
55
|
+
| -------------------------------------------------------- | -------------------------------------------------------- |
|
|
56
|
+
| `client<TResponses, ThrowOnError>(config)` | `Promise<RequestResult<TResponses, ThrowOnError>>` |
|
|
57
|
+
| `client.stream<TResponses, ThrowOnError, TItem>(config)` | `Promise<StreamResult<TResponses, ThrowOnError, TItem>>` |
|
|
58
|
+
|
|
59
|
+
## Export the stream types
|
|
60
|
+
|
|
61
|
+
Your module needs these on top of the [contract](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client#check-the-contract):
|
|
62
|
+
|
|
63
|
+
| Export | Kind | Needed when |
|
|
64
|
+
| ------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `client.stream`, `StreamResult` | method, type | The spec has Server-Sent Events or JSON Lines operations. |
|
|
66
|
+
| `StreamResultWithHeaders` | type | A stream declares response headers. It replaces `StreamResult`. With `returnType: 'data'`, `UnwrappedStreamResultWithHeaders` replaces `UnwrappedStreamResult`. A stream that types only the payload also imports `SuccessOf` and `ServerSentEvent`. |
|
|
67
|
+
| `SuccessOf`, `TypedEvent` | type | A stream names each event (both) or streams JSON Lines (`SuccessOf`). |
|
|
68
|
+
|
|
69
|
+
## Type each item
|
|
70
|
+
|
|
71
|
+
The third type argument, `TItem`, follows the document:
|
|
72
|
+
|
|
73
|
+
| The document | `TItem` |
|
|
74
|
+
| ---------------------- | ----------------------------------------------------------------------------------- |
|
|
75
|
+
| Names each event | `TypedEvent<SuccessOf<TResponses>>` |
|
|
76
|
+
| Streams JSON Lines | `SuccessOf<TResponses>` |
|
|
77
|
+
| Types only the payload | Left out. Give `TItem` a default, such as `ServerSentEvent<SuccessOf<TResponses>>`. |
|
|
78
|
+
|
|
79
|
+
## Read the stream fields
|
|
80
|
+
|
|
81
|
+
Declare these fields on `RequestConfig`. The call passes them next to the [usual fields](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client#check-the-contract):
|
|
82
|
+
|
|
83
|
+
| Field | Passed when | Value |
|
|
84
|
+
| ------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
85
|
+
| `contentType` | The operation has a body, streams, or offers `text/event-stream` | `request` is the body's media type, `response` the one to ask for in `Accept`. |
|
|
86
|
+
| `eventShape` | A Server-Sent Events operation | `'event'` or `'data'`: what `validator.response` checks on each event. |
|
|
87
|
+
| `rawData` | A Server-Sent Events operation types `data` as a string | `true`, or the names of those events. Keep their `data` as text. |
|
|
88
|
+
|
|
89
|
+
## Parse JSON Lines
|
|
90
|
+
|
|
91
|
+
A JSON Lines call, such as `application/jsonl` or `application/x-ndjson`, passes the media type in `contentType.response` and expects one parsed item per line.
|
|
92
|
+
|
|
93
|
+
The shared runtime's `sse.ts` exports both parsers: `parseEventStream` reads events and applies `rawData`, and `parseJsonLines` reads one item per line.
|
|
94
|
+
|
|
95
|
+
## Streams with full or data return types
|
|
96
|
+
|
|
97
|
+
With [`returnType`](https://kubb.dev/extensions/kubb-labs/plugin-client/options#returntype) `'full'` or `'data'`, every operation imports helpers from your module, and streams import more types:
|
|
98
|
+
|
|
99
|
+
| `returnType` | Extra exports | Stream operations also import |
|
|
100
|
+
| ------------ | -------------------------------------------------------- | ---------------------------------------- |
|
|
101
|
+
| `'full'` | `withUnwrap`, `resolveThrowOnError`, `Unwrappable` | `StreamOptions`, `StreamResult` |
|
|
102
|
+
| `'data'` | `unwrapResult`, `resolveThrowOnError`, `UnwrappedResult` | `StreamOptions`, `UnwrappedStreamResult` |
|
|
103
|
+
|
|
104
|
+
`'full'` and `'data'` need a `StreamResult` with `data` and `error`, so the minimal type in [Add a stream method](#add-a-stream-method) is not enough. `@kubb/plugin-client/templates/core.ts` exports all of these.
|
|
105
|
+
|
|
106
|
+
Next, build the client [on the shared runtime](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client#write-the-client), which ships `client.stream`.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Validate requests and responses
|
|
2
2
|
|
|
3
|
-
`@kubb/plugin-
|
|
3
|
+
Pass Zod schemas from `@kubb/plugin-zod` to your client and run them there.
|
|
4
4
|
|
|
5
|
-
::
|
|
5
|
+
::steps{level="2"}
|
|
6
|
+
|
|
7
|
+
## Pass the schemas
|
|
8
|
+
|
|
9
|
+
Add `pluginZod()` and set [`validator`](https://kubb.dev/extensions/kubb-labs/plugin-client/options#validator). Without `pluginZod()`, generation stops with an error.
|
|
6
10
|
|
|
7
11
|
```typescript [kubb.config.ts]
|
|
8
12
|
import { defineConfig } from 'kubb/config'
|
|
@@ -21,9 +25,14 @@ export default defineConfig({
|
|
|
21
25
|
})
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
| `validator` | Keys on `config.validator` |
|
|
29
|
+
| --------------------- | ------------------------------------- |
|
|
30
|
+
| `'zod'` | `response`, `error` |
|
|
31
|
+
| `{ response: 'zod' }` | `response`, `error` |
|
|
32
|
+
| `{ request: 'zod' }` | `request` |
|
|
33
|
+
| `{ params: 'zod' }` | `path`, `query`, `headers`, `cookies` |
|
|
25
34
|
|
|
26
|
-
Each generated call now
|
|
35
|
+
Each generated call now carries the schemas:
|
|
27
36
|
|
|
28
37
|
```typescript
|
|
29
38
|
apiClient<AddPetResponses, ThrowOnError>({
|
|
@@ -34,9 +43,11 @@ apiClient<AddPetResponses, ThrowOnError>({
|
|
|
34
43
|
})
|
|
35
44
|
```
|
|
36
45
|
|
|
37
|
-
A `validator` passed on one call
|
|
46
|
+
A `validator` passed on one call merges per key: `{ request }` adds a request schema and keeps the generated response schema. A per-call `error` replaces the whole error map.
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
## Run them in your client
|
|
49
|
+
|
|
50
|
+
Add a `validator` field to your `RequestConfig` and run each schema with `~standard.validate`, the [Standard Schema](https://standardschema.dev) method that Zod implements:
|
|
40
51
|
|
|
41
52
|
```typescript [src/client.ts]
|
|
42
53
|
type Result = { value: unknown; issues?: undefined } | { issues: ReadonlyArray<unknown> }
|
|
@@ -53,32 +64,29 @@ if (result && 'issues' in result && result.issues) throw new Error('Response val
|
|
|
53
64
|
const validatedBody = result && 'value' in result ? result.value : body
|
|
54
65
|
```
|
|
55
66
|
|
|
56
|
-
Return `validatedBody` as `data`, so a schema that transforms the value takes effect.
|
|
57
|
-
|
|
58
|
-
`error` is keyed by status, such as `{ 404: schema, '5xx': schema, default: schema }`. Pick the exact status, then its range, then `default`, and skip validation when none matches.
|
|
67
|
+
Return `validatedBody` as `data`, so a schema that transforms the value takes effect.
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
`error` is keyed by status, such as `{ 404: schema, '5xx': schema, default: schema }`. Try the exact status, then its range, then `default`. If none matches, skip validation.
|
|
61
70
|
|
|
62
71
|
## Validate params
|
|
63
72
|
|
|
64
|
-
With `validator: { params: 'zod' }`, each call also passes
|
|
73
|
+
With `validator: { params: 'zod' }`, each call also passes its path, query, header, and cookie schemas:
|
|
65
74
|
|
|
66
75
|
```typescript
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
validator
|
|
72
|
-
|
|
73
|
-
query: putEventQuerySchema,
|
|
74
|
-
headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape),
|
|
75
|
-
...config.validator,
|
|
76
|
-
},
|
|
77
|
-
})
|
|
76
|
+
validator: {
|
|
77
|
+
path: putEventPathSchema,
|
|
78
|
+
query: putEventQuerySchema,
|
|
79
|
+
headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape),
|
|
80
|
+
...config.validator,
|
|
81
|
+
}
|
|
78
82
|
```
|
|
79
83
|
|
|
80
|
-
Add `path`, `query`, `headers`, and `cookies` to the `validator` field of your `RequestConfig`.
|
|
84
|
+
1. Add `path`, `query`, `headers`, and `cookies` to the `validator` field of your `RequestConfig`.
|
|
85
|
+
2. Run each on `config.path`, `config.query`, `config.headers`, or `config.cookies` before you build the URL and headers.
|
|
86
|
+
3. Send the values they return. The path and query schemas strip keys the spec does not declare.
|
|
87
|
+
4. Export `toHeadersStandardSchema` from your `importPath` module. Copy it from `@kubb/plugin-client/templates/standardSchema.ts`.
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
> [!WARNING]
|
|
90
|
+
> If your `toHeadersStandardSchema` returns the schema unchanged, it strips undeclared headers, such as an `Authorization` header your client adds. The template keeps them and matches declared headers in any casing.
|
|
83
91
|
|
|
84
|
-
|
|
92
|
+
::
|
|
@@ -1,48 +1,45 @@
|
|
|
1
1
|
# Write your client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build the client module that every generated function imports through `importPath`. You own its transport, auth, and error handling.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
::steps{level="2"}
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
| --------------- | -------- | -------------------------------------------------------------------------------------------- |
|
|
9
|
-
| `client` | function | Sends the request. Every generated function calls it, unless a call passes its own `client`. |
|
|
10
|
-
| `Options` | type | The grouped options object a generated function accepts. |
|
|
11
|
-
| `RequestResult` | type | What a generated function resolves to. |
|
|
7
|
+
## Check the contract
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
Export the names the generated files import:
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
| Export | Needed when |
|
|
12
|
+
| -------------------------------------------------------- | ------------------------------------------------ |
|
|
13
|
+
| `client` function, `Options` type | Always. |
|
|
14
|
+
| `RequestResult` type | `returnType` is `'plain'` (default) or `'full'`. |
|
|
15
|
+
| `withUnwrap`, `resolveThrowOnError`, `Unwrappable` | `returnType: 'full'`. |
|
|
16
|
+
| `unwrapResult`, `resolveThrowOnError`, `UnwrappedResult` | `returnType: 'data'`. |
|
|
17
|
+
| `toHeadersStandardSchema` function | `validator: { params: 'zod' }`. |
|
|
18
|
+
| `RequestConfig`, `ResponseErrorConfig` types | A query plugin calls the generated functions. |
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
A generated function calls `client<TResponses, ThrowOnError>(config)`, which must return `Promise<RequestResult<TResponses, ThrowOnError>>`.
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
| `RequestConfig` | type | The per-call `client` options a hook accepts, minus the request fields. |
|
|
22
|
-
| `ResponseErrorConfig` | type | The error type of a failed hook, given the spec's error type. |
|
|
22
|
+
> [!NOTE]
|
|
23
|
+
> The query plugins and `@kubb/plugin-mcp` call with `throwOnError: true` and read `data` off the result, so `RequestResult` needs a `data` field.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
Declare each `config` field your spec produces on `RequestConfig`, or the generated files fail to compile:
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
| Field | Passed when | Value |
|
|
28
|
+
| --------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `method`, `url`, `throwOnError` | Always | The upper-case method, the path template such as `'/pet/{petId}'`, and the caller's `throwOnError` or the default. |
|
|
30
|
+
| `path`, `query`, `headers`, `cookies`, `body` | The caller passes them | Typed from the spec. `cookies` holds name and value pairs. |
|
|
31
|
+
| `baseURL` | The path or operation declares its own `servers` | That server's URL. Use it when set. |
|
|
32
|
+
| `security` | The operation has security requirements | See [Authenticate](#authenticate). |
|
|
33
|
+
| `styles` | A parameter sets `style`, `explode`, `allowReserved`, or `contentType` | See [Serialize parameters](#serialize-parameters). |
|
|
34
|
+
| `contentType` | The operation has a body or streams | `request` is the body's media type, `response` the `Accept` value. |
|
|
35
|
+
| `parseAs` | Every success content type is binary | `'blob'` |
|
|
36
|
+
| `validator` | The `validator` option is set | See [Validate requests and responses](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/validate-requests-and-responses). |
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
A spec with Server-Sent Events or JSON Lines operations needs `client.stream` and more exports and fields. See [Stream Server-Sent Events and JSON Lines](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/streaming).
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
2. In `client.ts`, export the three types the core imports, listed below.
|
|
32
|
-
3. Write a `Transport` function that sends a `ResolvedRequest` and returns a web `Response`. If your library does not return one, wrap its bytes in `new Response(bytes, { status, headers })`.
|
|
33
|
-
4. Export `createLazyClient((config) => createClientCore({ defaultTransport, ...config }))` as `client`. Register the form and stream media types your spec needs with `client.setConfig({ mediaTypes: { 'multipart/form-data': multipartFormData, ...streamMediaTypes } })`, and add `export * from './core.ts'` and `export * from './sse.ts'`. The core has no serializers of its own, so also set `serializer: { query: serializeQuery, path: serializePath }` from `serializers/query.ts` and `serializers/path.ts`, or their styled versions, and `header` and `cookie` for a spec with such parameters.
|
|
34
|
-
|
|
35
|
-
| Export | Purpose | On fetch | On axios |
|
|
36
|
-
| ---------------- | ------------------------------------------------------- | ------------- | -------------------- |
|
|
37
|
-
| `NativeRequest` | The request object on a result and a `ResponseError`. | `Request` | `AxiosRequestConfig` |
|
|
38
|
-
| `NativeResponse` | The response object on a result and a `ResponseError`. | `Response` | `AxiosResponse` |
|
|
39
|
-
| `NativeOptions` | The `options` a call passes to your HTTP library as is. | `RequestInit` | `AxiosRequestConfig` |
|
|
40
|
-
|
|
41
|
-
[`@kubb/plugin-fetch/templates/client.ts`](https://github.com/kubb-labs/plugins/blob/main/packages/plugin-fetch/templates/client.ts) is a complete example.
|
|
42
|
-
|
|
43
|
-
## What a generated function looks like
|
|
40
|
+
Other keys the caller passes, such as `signal`, arrive through `...config`. A generated function looks like this:
|
|
44
41
|
|
|
45
|
-
```typescript
|
|
42
|
+
```typescript [src/gen/clients/pet/getPetById.ts]
|
|
46
43
|
import type { Options, RequestResult } from '../../../client'
|
|
47
44
|
import type { GetPetByIdOptions, GetPetByIdResponses } from '../../types/GetPetById'
|
|
48
45
|
import { client } from '../../../client'
|
|
@@ -62,75 +59,15 @@ export function getPetById<ThrowOnError extends boolean = true>(
|
|
|
62
59
|
}
|
|
63
60
|
```
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Read the body by the response `Content-Type`. An operation with only binary success content also passes `parseAs: 'blob'`.
|
|
68
|
-
|
|
69
|
-
A call can also pass `contentType`: `request` is the body's media type, and `response` is the media type to ask for in `Accept`. A streamed operation always passes its media type, such as `contentType: { response: 'text/event-stream' }` or `contentType: { response: 'application/jsonl' }`. A call whose response offers `text/event-stream` next to another content type passes `text/event-stream` too, unless the caller picks another one. `RequestConfig` needs the field.
|
|
70
|
-
|
|
71
|
-
## Serialize parameters
|
|
72
|
-
|
|
73
|
-
When the spec sets `style`, `explode`, `allowReserved`, or `contentType` on a parameter, the generated call also passes `styles`, grouped by location and keyed by the parameter name:
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
styles: {
|
|
77
|
-
path: { petId: { style: 'matrix', explode: true } },
|
|
78
|
-
query: { tags: { style: 'pipeDelimited', explode: false } },
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
A parameter without an entry uses the default for its location. The shared runtime's `serializers.ts` applies these styles. A client you write from scratch declares `styles` on `RequestConfig` and applies the styles itself, or ignores them when the spec only uses the defaults.
|
|
83
|
-
|
|
84
|
-
### Query
|
|
85
|
-
|
|
86
|
-
Query parameters default to the `form` style. Arrays explode into repeated keys unless the spec
|
|
87
|
-
says otherwise, and `spaceDelimited`, `pipeDelimited`, and `deepObject` change how arrays and
|
|
88
|
-
objects collapse.
|
|
62
|
+
## Write the client
|
|
89
63
|
|
|
90
|
-
|
|
91
|
-
| ---------------- | --------- | ------------------- | ---------------- |
|
|
92
|
-
| `form` (default) | `true` | `{ id: [3, 4, 5] }` | `id=3&id=4&id=5` |
|
|
93
|
-
| `form` | `false` | `{ id: [3, 4, 5] }` | `id=3,4,5` |
|
|
94
|
-
| `spaceDelimited` | `false` | `{ id: [3, 4, 5] }` | `id=3%204%205` |
|
|
95
|
-
| `pipeDelimited` | `false` | `{ id: [3, 4, 5] }` | `id=3\|4\|5` |
|
|
96
|
-
| `deepObject` | `n/a` | `{ a: { b: 1 } }` | `a%5Bb%5D=1` |
|
|
64
|
+
Write the module from scratch, or build it on the runtime that `@kubb/plugin-fetch` and `@kubb/plugin-axios` use.
|
|
97
65
|
|
|
98
|
-
|
|
99
|
-
so the delimiter only shows with `explode: false`.
|
|
66
|
+
:::tabs
|
|
100
67
|
|
|
101
|
-
|
|
68
|
+
::::tabs-item{label="From scratch"}
|
|
102
69
|
|
|
103
|
-
|
|
104
|
-
`.` and `matrix` prefixes a `;name=` segment. The results below are the serialized segment for a
|
|
105
|
-
parameter named `id`.
|
|
106
|
-
|
|
107
|
-
| Style | `explode` | Input | Result |
|
|
108
|
-
| ------------------ | --------- | ---------------- | ----------------- |
|
|
109
|
-
| `simple` (default) | `false` | `[3, 4, 5]` | `3,4,5` |
|
|
110
|
-
| `label` | `true` | `[3, 4, 5]` | `.3.4.5` |
|
|
111
|
-
| `matrix` | `true` | `[3, 4, 5]` | `;id=3;id=4;id=5` |
|
|
112
|
-
| `simple` | `false` | `{ x: 1, y: 2 }` | `x,1,y,2` |
|
|
113
|
-
|
|
114
|
-
### Header and cookie
|
|
115
|
-
|
|
116
|
-
Header parameters use the `simple` style and cookie parameters use the `form` style, so the metadata
|
|
117
|
-
for these locations carries only `explode`, plus `style: 'cookie'` for a cookie parameter with the
|
|
118
|
-
OpenAPI 3.2 `cookie` style. Header values are sent as-is. Cookie values are URL-encoded into a single
|
|
119
|
-
`Cookie` header. The `cookie` style sends them unencoded, as RFC 6265 describes, defaults `explode` to
|
|
120
|
-
`true`, and throws on a value with a character such as `;` or a space, or on an exploded object key that is not a valid
|
|
121
|
-
cookie name, such as `a=b`.
|
|
122
|
-
|
|
123
|
-
| Location | `explode` | Input | Result |
|
|
124
|
-
| ---------------------- | --------- | --------------------------------- | ------------------------- |
|
|
125
|
-
| header | `false` | `[3, 4]` | `X-Ids: 3,4` |
|
|
126
|
-
| header | `true` | `{ role: 'admin' }` | `X-Filter: role=admin` |
|
|
127
|
-
| cookie | `false` | `{ session: 'abc', ids: [1, 2] }` | `session=abc; ids=1,2` |
|
|
128
|
-
| cookie | `true` | `{ ids: [1, 2] }` | `ids=1; ids=2` |
|
|
129
|
-
| cookie, `cookie` style | default | `{ sid: 'a%20b', ids: [1, 2] }` | `sid=a%20b; ids=1; ids=2` |
|
|
130
|
-
|
|
131
|
-
## A minimal client
|
|
132
|
-
|
|
133
|
-
The client below uses `fetch`. Replace the transport with the HTTP library you use.
|
|
70
|
+
This client uses `fetch` and reads JSON. Replace `send` with your HTTP library.
|
|
134
71
|
|
|
135
72
|
```typescript [src/client.ts]
|
|
136
73
|
export type Auth = {
|
|
@@ -180,17 +117,28 @@ export const settings = { baseURL: 'https://petstore3.swagger.io/api/v3' }
|
|
|
180
117
|
async function send(config: RequestConfig) {
|
|
181
118
|
const path: Record<string, unknown> = { ...config.path }
|
|
182
119
|
const url = new URL((config.baseURL ?? settings.baseURL) + config.url.replace(/\{(\w+)\}/g, (_, key) => encodeURIComponent(String(path[key]))))
|
|
120
|
+
// The default `form` style repeats the key for each array item and sends each object property as its own parameter. Other styles: see Serialize parameters.
|
|
121
|
+
for (const [key, value] of Object.entries(config.query ?? {})) {
|
|
122
|
+
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)
|
|
123
|
+
for (const [name, entry] of isObject ? Object.entries(value) : [[key, value]]) {
|
|
124
|
+
for (const item of Array.isArray(entry) ? entry : [entry]) {
|
|
125
|
+
if (item !== undefined) url.searchParams.append(name, String(item))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
183
129
|
const cookie = Object.entries(config.cookies ?? {})
|
|
184
130
|
.filter(([, value]) => value != null)
|
|
185
131
|
.map(([name, value]) => `${name}=${encodeURIComponent(String(value))}`)
|
|
186
132
|
.join('; ')
|
|
133
|
+
// `Headers` matches names in any case, so a caller's `content-type` or `authorization` still wins.
|
|
134
|
+
const headers = new Headers(config.headers as Record<string, string>)
|
|
135
|
+
if (config.body !== undefined && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json')
|
|
136
|
+
// On the server this adds to the caller's cookies. Browsers drop a script-set `Cookie` header and send their own.
|
|
137
|
+
if (cookie) headers.set('Cookie', [headers.get('Cookie'), cookie].filter(Boolean).join('; '))
|
|
187
138
|
|
|
188
139
|
const response = await fetch(url, {
|
|
189
140
|
method: config.method,
|
|
190
|
-
headers
|
|
191
|
-
...(config.headers as Record<string, string>),
|
|
192
|
-
...(cookie ? { Cookie: cookie } : {}),
|
|
193
|
-
},
|
|
141
|
+
headers,
|
|
194
142
|
body: config.body === undefined ? undefined : JSON.stringify(config.body),
|
|
195
143
|
signal: config.signal,
|
|
196
144
|
})
|
|
@@ -206,7 +154,127 @@ export const client = <TResponses, ThrowOnError extends boolean = true>(config:
|
|
|
206
154
|
send(config) as Promise<RequestResult<TResponses, ThrowOnError>>
|
|
207
155
|
```
|
|
208
156
|
|
|
209
|
-
The [example project](https://github.com/kubb-labs/plugins/tree/main/examples/client)
|
|
157
|
+
The [example project](https://github.com/kubb-labs/plugins/tree/main/examples/client) adds a base URL setter and an async auth hook.
|
|
158
|
+
|
|
159
|
+
::::
|
|
160
|
+
|
|
161
|
+
::::tabs-item{label="On the shared runtime"}
|
|
162
|
+
|
|
163
|
+
Reuse the request pipeline of the fetch and axios clients and write only the transport:
|
|
164
|
+
|
|
165
|
+
1. Copy `core.ts`, `mediaTypes.ts`, `serializers.ts`, `standardSchema.ts`, `sse.ts`, and the `mediaTypes/` and `serializers/` files your spec needs from `@kubb/plugin-client/templates` next to your `client.ts`.
|
|
166
|
+
2. In `client.ts`, export the `NativeRequest`, `NativeResponse`, and `NativeOptions` types the core imports. On fetch they are `Request`, `Response`, and `RequestInit`.
|
|
167
|
+
3. Write a `Transport` function that sends a `ResolvedRequest` and returns a web `Response`. Wrap other results in `new Response(bytes, { status, headers })`. For a 204, 205, or 304 status, pass `null` as the body, or the constructor throws.
|
|
168
|
+
4. Export `createLazyClient((config) => createClientCore({ defaultTransport, ...config }))` as `client`, and add `export * from './core.ts'` and `export * from './sse.ts'`.
|
|
169
|
+
5. Register what the core does not ship: `client.setConfig({ mediaTypes: { 'multipart/form-data': multipartFormData, ...streamMediaTypes }, serializer: { query: serializeQuery, path: serializePath } })`. Add `header` and `cookie` serializers when the spec has such parameters.
|
|
170
|
+
|
|
171
|
+
[`@kubb/plugin-fetch/templates/client.ts`](https://github.com/kubb-labs/plugins/blob/main/packages/plugin-fetch/templates/client.ts) is a complete example.
|
|
172
|
+
|
|
173
|
+
::::
|
|
174
|
+
|
|
175
|
+
:::
|
|
176
|
+
|
|
177
|
+
## Authenticate
|
|
178
|
+
|
|
179
|
+
The plugin sends no credentials. Each call passes the operation's security requirements as `security`, and your client decides what to send:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
security: [{ type: 'apiKey', name: 'api_key', in: 'header' }, { type: 'oauth2' }]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Shape | Meaning |
|
|
186
|
+
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
187
|
+
| No `security` field | The operation is public. Skip auth. |
|
|
188
|
+
| One entry per requirement | Meeting one requirement is enough. |
|
|
189
|
+
| An array inside the list | One requirement needs these schemes together, such as an API key and a session cookie. Send all of them. |
|
|
190
|
+
| `type: 'http'` with `scheme` | `'bearer'` or `'basic'`. Other HTTP schemes arrive as `'bearer'`. |
|
|
191
|
+
| `type: 'oauth2'` with `flows`, `metadataUrl` | The flow URLs to get a token. |
|
|
192
|
+
| `type: 'openIdConnect'` with `url` | The discovery URL. |
|
|
193
|
+
|
|
194
|
+
Await a token before the request, so it can come from a secrets store or a refresh call:
|
|
195
|
+
|
|
196
|
+
:::code-group
|
|
197
|
+
|
|
198
|
+
```typescript [src/client.ts]
|
|
199
|
+
type GetToken = () => Promise<string | undefined>
|
|
200
|
+
|
|
201
|
+
let getToken: GetToken | undefined
|
|
202
|
+
|
|
203
|
+
export function setAuth(resolve: GetToken) {
|
|
204
|
+
getToken = resolve
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// inside send(), after `headers` and before fetch(). This sends bearer tokens only, not API keys or basic credentials.
|
|
208
|
+
const bearer = config.security?.flat().some((auth) => auth.type !== 'apiKey' && auth.scheme !== 'basic')
|
|
209
|
+
const token = bearer ? await getToken?.() : undefined
|
|
210
|
+
if (token && !headers.has('Authorization')) headers.set('Authorization', `Bearer ${token}`)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```typescript [src/index.ts]
|
|
214
|
+
import { setAuth } from './client'
|
|
215
|
+
|
|
216
|
+
setAuth(async () => readTokenFromSecretsStore())
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
:::
|
|
220
|
+
|
|
221
|
+
For request signing, such as AWS SigV4, pass a different `client` on the call:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
await getPetById({ path: { petId: 1 }, client: createSignedClient() })
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Serialize parameters
|
|
228
|
+
|
|
229
|
+
When the spec sets `style`, `explode`, `allowReserved`, or `contentType` on a parameter, the call passes `styles`, grouped by location and keyed by parameter name:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
styles: {
|
|
233
|
+
path: { petId: { style: 'matrix', explode: true } },
|
|
234
|
+
query: { tags: { style: 'pipeDelimited', explode: false } },
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- A parameter without an entry uses its location's default: `form` for query and cookie, `simple` for path and header.
|
|
239
|
+
- The shared runtime's `serializers.ts` applies every style.
|
|
240
|
+
- A client written from scratch applies them itself, or ignores `styles` when the spec only uses the defaults.
|
|
241
|
+
- See [Cookie parameters with the cookie style](#cookie-parameters-with-the-cookie-style) for the OpenAPI 3.2 `cookie` style.
|
|
242
|
+
|
|
243
|
+
The [plugin-fetch serialization guide](https://kubb.dev/extensions/kubb-labs/plugin-fetch/guides/serialization) shows the output of each style.
|
|
244
|
+
|
|
245
|
+
## Register it
|
|
246
|
+
|
|
247
|
+
Point `importPath` at the module, resolved from the generated files:
|
|
248
|
+
|
|
249
|
+
```typescript [kubb.config.ts]
|
|
250
|
+
import { defineConfig } from 'kubb/config'
|
|
251
|
+
import { pluginTs } from '@kubb/plugin-ts'
|
|
252
|
+
import { pluginClient } from '@kubb/plugin-client'
|
|
253
|
+
|
|
254
|
+
export default defineConfig({
|
|
255
|
+
input: './petStore.yaml',
|
|
256
|
+
output: { path: './src/gen' },
|
|
257
|
+
plugins: [
|
|
258
|
+
pluginTs(),
|
|
259
|
+
pluginClient({ group: { type: 'tag' }, importPath: '../../../client' }),
|
|
260
|
+
],
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Run `npx kubb generate`, then `npx tsc --noEmit`. A missing export or `RequestConfig` field shows up as a compile error in the generated files. With a relative `importPath`, the query plugins resolve it from the same generated file, so one path serves both.
|
|
265
|
+
|
|
266
|
+
::
|
|
267
|
+
|
|
268
|
+
## Less common cases
|
|
269
|
+
|
|
270
|
+
### Cookie parameters with the cookie style
|
|
271
|
+
|
|
272
|
+
When a cookie parameter sets the OpenAPI 3.2 `cookie` style, the call passes `style: 'cookie'` in `styles`:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
styles: { cookie: { session: { style: 'cookie' } } }
|
|
276
|
+
```
|
|
210
277
|
|
|
211
|
-
|
|
212
|
-
|
|
278
|
+
- Send its values without percent-encoding.
|
|
279
|
+
- OpenAPI 3.2 defaults `explode` to `true` for this style.
|
|
280
|
+
- The shared runtime throws a `TypeError` when a value has characters a cookie cannot carry.
|
package/docs/options.md
CHANGED
|
@@ -4,25 +4,25 @@ Pass these options to `pluginClient()`. Shared options link to [Shared plugin op
|
|
|
4
4
|
|
|
5
5
|
## Options overview
|
|
6
6
|
|
|
7
|
-
| Option | Purpose
|
|
8
|
-
| ------------------------------------------------------------------------- |
|
|
9
|
-
| [`importPath`](#importpath) | Import specifier of your client module.
|
|
10
|
-
| [`
|
|
11
|
-
| [`
|
|
12
|
-
| [`
|
|
13
|
-
| [`
|
|
14
|
-
| [`
|
|
15
|
-
| [`include`](https://kubb.dev/docs/5.x/reference/plugin-options#include) | Keep only operations that match.
|
|
16
|
-
| [`exclude`](https://kubb.dev/docs/5.x/reference/plugin-options#exclude) | Skip operations that match.
|
|
17
|
-
| [`override`](https://kubb.dev/docs/5.x/reference/plugin-options#override) | Apply different options per pattern
|
|
18
|
-
| [`resolver`](https://kubb.dev/docs/5.x/reference/plugin-options#resolver) | Customize generated names and file paths.
|
|
19
|
-
| [`macros`](https://kubb.dev/docs/5.x/reference/plugin-options#macros) | Rewrite AST nodes before printing.
|
|
7
|
+
| Option | Purpose | Default |
|
|
8
|
+
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------ |
|
|
9
|
+
| [`importPath`](#importpath) | Import specifier of your client module. | Required |
|
|
10
|
+
| [`returnType`](#returntype) | What a generated function resolves to. | `'plain'` |
|
|
11
|
+
| [`throwOnErrorDefault`](#throwonerrordefault) | `throwOnError` value each call passes to your client. | `true` |
|
|
12
|
+
| [`validator`](#validator) | Zod schemas to pass to your client. | `false` |
|
|
13
|
+
| [`output`](https://kubb.dev/docs/5.x/reference/plugin-options#output) | Where files are written. | `{ path: 'clients', barrel: { type: 'named' } }` |
|
|
14
|
+
| [`group`](https://kubb.dev/docs/5.x/reference/plugin-options#group) | Split output by tag or path. | None |
|
|
15
|
+
| [`include`](https://kubb.dev/docs/5.x/reference/plugin-options#include) | Keep only operations that match. | None |
|
|
16
|
+
| [`exclude`](https://kubb.dev/docs/5.x/reference/plugin-options#exclude) | Skip operations that match. | `[]` |
|
|
17
|
+
| [`override`](https://kubb.dev/docs/5.x/reference/plugin-options#override) | Apply different options per pattern, except `returnType` and `throwOnErrorDefault`. | `[]` |
|
|
18
|
+
| [`resolver`](https://kubb.dev/docs/5.x/reference/plugin-options#resolver) | Customize generated names and file paths. | `resolverClient` |
|
|
19
|
+
| [`macros`](https://kubb.dev/docs/5.x/reference/plugin-options#macros) | Rewrite AST nodes before printing. | `[]`, run after the built-in client macros |
|
|
20
20
|
|
|
21
21
|
## Option details
|
|
22
22
|
|
|
23
23
|
### importPath
|
|
24
24
|
|
|
25
|
-
Import specifier of your client module
|
|
25
|
+
Import specifier of your client module, written into every generated import as given. It must resolve from the generated file: use a relative path, a package name, or an alias.
|
|
26
26
|
|
|
27
27
|
| | |
|
|
28
28
|
| -------- | -------- |
|
|
@@ -30,35 +30,15 @@ Import specifier of your client module. The plugin writes it into every generate
|
|
|
30
30
|
| Required | `true` |
|
|
31
31
|
|
|
32
32
|
```typescript
|
|
33
|
-
pluginClient({ importPath: '../../../client' }) //
|
|
33
|
+
pluginClient({ importPath: '../../../client' }) // from src/gen/clients/<tag>/ to src/client.ts
|
|
34
34
|
pluginClient({ importPath: '@my-org/api-client' }) // a package
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
The module must export `client`, plus the `Options` and `RequestResult` types. See [write your client](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client).
|
|
40
|
-
|
|
41
|
-
### throwOnErrorDefault
|
|
42
|
-
|
|
43
|
-
Default for the `throwOnError` field that every generated function passes to your client. A call that sets `throwOnError` itself wins. Your client decides what the flag does: the [example client](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client) throws on a non-2xx status when it is `true` and returns the error as a value when it is `false`.
|
|
44
|
-
|
|
45
|
-
| | |
|
|
46
|
-
| -------- | --------- |
|
|
47
|
-
| Type | `boolean` |
|
|
48
|
-
| Required | `false` |
|
|
49
|
-
| Default | `true` |
|
|
50
|
-
|
|
51
|
-
The setting applies to the whole plugin, so you cannot set it in `override`.
|
|
37
|
+
Without `group`, files sit one folder higher, so use `../../client`. [Write your client](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client) lists the exports the module needs.
|
|
52
38
|
|
|
53
39
|
### returnType
|
|
54
40
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- `'plain'`: returns what your `client` returns.
|
|
58
|
-
- `'full'`: wraps the call in `withUnwrap`, so the caller can `await` it or call `.unwrap()`. Your module must export `withUnwrap`, `resolveThrowOnError`, and the `Unwrappable` type.
|
|
59
|
-
- `'data'`: passes the call through `unwrapResult` and resolves to the body. Your module must export `unwrapResult`, `resolveThrowOnError`, and the `UnwrappedResult` type.
|
|
60
|
-
|
|
61
|
-
`@kubb/plugin-client/templates/core.ts` exports all of these, so your module can re-export them from there.
|
|
41
|
+
What a generated function resolves to.
|
|
62
42
|
|
|
63
43
|
| | |
|
|
64
44
|
| -------- | ----------------------------- |
|
|
@@ -66,18 +46,43 @@ Shape of the value a generated operation resolves to.
|
|
|
66
46
|
| Required | `false` |
|
|
67
47
|
| Default | `'plain'` |
|
|
68
48
|
|
|
49
|
+
::field-group
|
|
50
|
+
|
|
51
|
+
:::field{name="'plain'"}
|
|
52
|
+
Default. Returns what your `client` returns.
|
|
53
|
+
:::
|
|
54
|
+
|
|
55
|
+
:::field{name="'full'"}
|
|
56
|
+
The caller can `await` the call or call `.unwrap()`. Your module must export `withUnwrap`, `resolveThrowOnError`, and the `Unwrappable` type.
|
|
57
|
+
:::
|
|
58
|
+
|
|
59
|
+
:::field{name="'data'"}
|
|
60
|
+
Resolves to the body when `throwOnError` is `true`, and to the full result when it is `false`. Your module must export `unwrapResult`, `resolveThrowOnError`, and the `UnwrappedResult` type.
|
|
61
|
+
:::
|
|
62
|
+
|
|
63
|
+
::
|
|
64
|
+
|
|
65
|
+
You can re-export these helpers from `@kubb/plugin-client/templates/core.ts`.
|
|
66
|
+
|
|
69
67
|
```typescript
|
|
70
68
|
pluginClient({ importPath: '../../client', returnType: 'full' })
|
|
69
|
+
// with integerType: 'number' on the adapter
|
|
71
70
|
// const pet = await getPetById({ path: { petId: 1 } }).unwrap()
|
|
72
71
|
```
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
### throwOnErrorDefault
|
|
74
|
+
|
|
75
|
+
The `throwOnError` value every generated function passes to your client, unless the call sets its own. Your client decides what the flag does.
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
| | |
|
|
78
|
+
| -------- | --------- |
|
|
79
|
+
| Type | `boolean` |
|
|
80
|
+
| Required | `false` |
|
|
81
|
+
| Default | `true` |
|
|
77
82
|
|
|
78
83
|
### validator
|
|
79
84
|
|
|
80
|
-
Passes Zod schemas from `@kubb/plugin-zod` to your client on `config.validator`.
|
|
85
|
+
Passes Zod schemas from `@kubb/plugin-zod` to your client on `config.validator`. Your client runs them. See [Validate requests and responses](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/validate-requests-and-responses).
|
|
81
86
|
|
|
82
87
|
| | |
|
|
83
88
|
| -------- | ------------------------------------------------------------------------- |
|
|
@@ -88,7 +93,7 @@ Passes Zod schemas from `@kubb/plugin-zod` to your client on `config.validator`.
|
|
|
88
93
|
::field-group
|
|
89
94
|
|
|
90
95
|
:::field{name="false"}
|
|
91
|
-
Default
|
|
96
|
+
Default. Passes nothing.
|
|
92
97
|
:::
|
|
93
98
|
|
|
94
99
|
:::field{name="'zod'"}
|
|
@@ -96,12 +101,10 @@ Passes the response and error schemas.
|
|
|
96
101
|
:::
|
|
97
102
|
|
|
98
103
|
:::field{name="{ request?: 'zod', params?: 'zod', response?: 'zod' }"}
|
|
99
|
-
Opts in per
|
|
104
|
+
Opts in per slot: `request` for the body, `params` for the path, query, header, and cookie schemas, and `response` for the response and error schemas. `params` also imports `toHeadersStandardSchema` from your module.
|
|
100
105
|
:::
|
|
101
106
|
|
|
102
107
|
::
|
|
103
108
|
|
|
104
109
|
> [!IMPORTANT]
|
|
105
|
-
> Add `pluginZod()` to
|
|
106
|
-
|
|
107
|
-
With `params: 'zod'`, generated calls also import `toHeadersStandardSchema` from your [`importPath`](#importpath) module, so that module must export it. See [validate requests and responses](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/validate-requests-and-responses).
|
|
110
|
+
> Add `pluginZod()` to `plugins` when `validator` is set, or generation stops with an error.
|
package/docs/overview.md
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
# Client
|
|
2
2
|
|
|
3
|
-
The Client plugin turns each
|
|
3
|
+
The Client plugin turns each OpenAPI operation into a typed function that calls your own client module, with no Fetch, axios, or Kubb runtime dependency.
|
|
4
4
|
|
|
5
|
-
Each function takes grouped `path`, `query`, `headers`, and `body` parameters and passes the operation's auth requirements
|
|
5
|
+
- Each function takes grouped `path`, `query`, `headers`, `cookies`, and `body` parameters, and passes your client the operation's auth requirements as `security`.
|
|
6
|
+
- A call can pass its own `client` to replace the default.
|
|
7
|
+
- The query plugins and `@kubb/plugin-mcp` can call the generated functions.
|
|
8
|
+
- For a ready-made client, use [`@kubb/plugin-fetch`](https://kubb.dev/extensions/kubb-labs/plugin-fetch) or [`@kubb/plugin-axios`](https://kubb.dev/extensions/kubb-labs/plugin-axios) instead.
|
|
6
9
|
|
|
7
10
|
## Setup
|
|
8
11
|
|
|
9
|
-
- Set the required `importPath`
|
|
12
|
+
- Set the required `importPath` to your client module.
|
|
10
13
|
- Add `@kubb/plugin-ts` for the operation types, or `@kubb/plugin-zod` with `inferred: true`.
|
|
11
|
-
-
|
|
14
|
+
- When you set `validator`, add `@kubb/plugin-zod` and install `zod` v4. Generation fails without the plugin.
|
|
12
15
|
|
|
13
16
|
## Options worth knowing
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
| Option | What it does |
|
|
19
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `importPath` | Your client module, resolved from each generated file. |
|
|
21
|
+
| `returnType` | `'plain'` returns what your `client` returns. `'full'` adds `.unwrap()`. `'data'` resolves to the body. |
|
|
22
|
+
| `validator` | Passes Zod schemas to your client. |
|
|
23
|
+
| `throwOnErrorDefault` | The `throwOnError` value each call passes. Defaults to `true`. |
|
|
18
24
|
|
|
19
|
-
See [Options](https://kubb.dev/extensions/kubb-labs/plugin-client/options) for every option
|
|
25
|
+
See [Options](https://kubb.dev/extensions/kubb-labs/plugin-client/options) for every option. Next, [write your client](https://kubb.dev/extensions/kubb-labs/plugin-client/guides/write-your-client).
|
|
20
26
|
|
|
21
27
|
## Example
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
This config groups the functions by tag and points them at `src/client.ts`.
|
|
24
30
|
|
|
25
31
|
```typescript [kubb.config.ts]
|
|
26
32
|
import { defineConfig } from 'kubb/config'
|
package/kubb-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-client",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.4",
|
|
4
4
|
"description": "Generate typed API operations against your own client module.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@kubb/kit": "^5.20.1",
|
|
65
|
-
"@kubb/plugin-ts": "^5.5.
|
|
66
|
-
"@kubb/plugin-zod": "^5.9.
|
|
65
|
+
"@kubb/plugin-ts": "^5.5.4",
|
|
66
|
+
"@kubb/plugin-zod": "^5.9.4",
|
|
67
67
|
"@kubb/renderer-jsx": "^5.20.1"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# Authenticate
|
|
2
|
-
|
|
3
|
-
Auth lives in your `client`. The plugin does not send credentials itself. It tells your client which schemes each operation needs, and your client decides what to do.
|
|
4
|
-
|
|
5
|
-
## The `security` list
|
|
6
|
-
|
|
7
|
-
Each generated function passes the operation's security schemes from the spec as `security`:
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
apiClient<GetPetByIdResponses, ThrowOnError>({
|
|
11
|
-
method: 'GET',
|
|
12
|
-
url: '/pet/{petId}',
|
|
13
|
-
security: [{ type: 'apiKey', name: 'api_key', in: 'header' }, { type: 'oauth2' }],
|
|
14
|
-
...config,
|
|
15
|
-
})
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Each entry is one security requirement, and meeting one is enough. A requirement that needs several schemes together, such as an API key and a session cookie, is an array, and your client sends all of them: `security: [[{ type: 'apiKey', name: 'api_key', in: 'header' }, { type: 'apiKey', name: 'sid', in: 'cookie' }]]`.
|
|
19
|
-
|
|
20
|
-
An operation without security requirements passes no `security` field. Use that to skip auth on public endpoints.
|
|
21
|
-
|
|
22
|
-
An `oauth2` entry carries its `flows` with their URLs, and its `metadataUrl` when the spec sets `oauth2MetadataUrl`. An `openIdConnect` entry carries its discovery `url`. Your client can use them to run the flow, such as the OpenAPI 3.2 `deviceAuthorization` flow, and then send the token as a bearer token:
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
security: [{ type: 'oauth2', flows: { deviceAuthorization: { deviceAuthorizationUrl: 'https://auth.example.com/device', tokenUrl: 'https://auth.example.com/token' } }, scopes: ['read'] }]
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Add a bearer token from an async source
|
|
29
|
-
|
|
30
|
-
The client can `await` anything before it sends the request. The client below awaits a token function you register with `setAuth`, so the token can come from a secrets store or a refresh call:
|
|
31
|
-
|
|
32
|
-
::code-group
|
|
33
|
-
|
|
34
|
-
```typescript [src/client.ts]
|
|
35
|
-
type GetToken = () => Promise<string | undefined>
|
|
36
|
-
|
|
37
|
-
let getToken: GetToken | undefined
|
|
38
|
-
|
|
39
|
-
export function setAuth(resolve: GetToken) {
|
|
40
|
-
getToken = resolve
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function client(config: RequestConfig) {
|
|
44
|
-
const token = config.security?.length ? await getToken?.() : undefined
|
|
45
|
-
|
|
46
|
-
const response = await fetch(url, {
|
|
47
|
-
method: config.method,
|
|
48
|
-
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(config.headers as Record<string, string>) },
|
|
49
|
-
// ...
|
|
50
|
-
})
|
|
51
|
-
// ...
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
```typescript [src/index.ts]
|
|
56
|
-
import { setAuth } from './client'
|
|
57
|
-
|
|
58
|
-
setAuth(async () => readTokenFromSecretsStore())
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
::
|
|
62
|
-
|
|
63
|
-
## Sign requests or use several auth methods
|
|
64
|
-
|
|
65
|
-
Read `config.security` and pick per operation, or pass a different `client` on a call. Both ways work for request signing, such as AWS SigV4, and for services that use different credentials:
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
import { createSignedClient } from './clients/iam'
|
|
69
|
-
|
|
70
|
-
await getPetById({ path: { petId: 1 }, client: createSignedClient() })
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The examples set [`integerType: 'number'`](https://kubb.dev/extensions/kubb-labs/adapter-oas/options#integertype) on the adapter, so an `int64` id such as `petId` is a `number`. With the default it is a `bigint` and the call takes `1n`.
|
|
74
|
-
|
|
75
|
-
A client passed on a call replaces the default for that call only.
|