@kubb/plugin-axios 5.0.0-beta.76 → 5.0.0-beta.79
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 +242 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +242 -87
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/generators/clientGenerator.tsx +24 -11
- package/src/plugin.ts +19 -5
- package/src/templates.ts +9 -4
- package/templates/axios.ts +297 -166
- package/templates/serializers.ts +424 -0
- package/templates/standardSchema.ts +54 -0
package/templates/axios.ts
CHANGED
|
@@ -1,36 +1,56 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
2
|
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
|
3
|
+
import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, serializeCookies } from './serializers'
|
|
4
|
+
import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
|
|
5
|
+
import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
* HTTP status codes treated as a success
|
|
6
|
-
* these; everything else is an error (thrown by default, or surfaced on `error`).
|
|
8
|
+
* HTTP status codes treated as a success, everything else is an error.
|
|
7
9
|
*/
|
|
8
10
|
export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* The success members of a per-status responses record
|
|
13
|
+
* The success members of a per-status responses record.
|
|
12
14
|
*/
|
|
13
15
|
export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* The error members of a per-status responses record
|
|
18
|
+
* The error members of a per-status responses record, every documented status that is not a 2xx.
|
|
17
19
|
*/
|
|
18
20
|
export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
* Converts a response record's string status key to its numeric literal
|
|
22
|
-
* Non-numeric keys such as the OpenAPI `default` response stay `number`.
|
|
23
|
+
* Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`.
|
|
23
24
|
*/
|
|
24
25
|
export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
* The plain body of a per-status response, unwrapping the `{ contentType; data }` union so an error result keeps the bare body union on `error`.
|
|
29
|
+
*/
|
|
30
|
+
export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The success variant for a single status, flattened so the negotiated `contentType` sits next to `data` and `switch (result.contentType)` narrows it.
|
|
34
|
+
*/
|
|
35
|
+
export type SuccessVariant<TStatus, TEntry, TRequest, TResponse> = TEntry extends { contentType: string; data: unknown }
|
|
36
|
+
? TEntry extends { contentType: infer TContentType; data: infer TData }
|
|
37
|
+
? { status: ToStatusNumber<TStatus>; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse }
|
|
38
|
+
: never
|
|
39
|
+
: { status: ToStatusNumber<TStatus>; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One result variant for a single documented status, keyed by the numeric `status` so a `switch (result.status)` narrows `data` or `error`.
|
|
30
43
|
*/
|
|
31
44
|
export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
|
|
32
|
-
?
|
|
33
|
-
: {
|
|
45
|
+
? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
|
|
46
|
+
: {
|
|
47
|
+
status: ToStatusNumber<TStatus>
|
|
48
|
+
data: undefined
|
|
49
|
+
error: DataOf<TResponses[TStatus]>
|
|
50
|
+
contentType: string | undefined
|
|
51
|
+
request: TRequest
|
|
52
|
+
response: TResponse
|
|
53
|
+
}
|
|
34
54
|
|
|
35
55
|
/**
|
|
36
56
|
* The union of every documented status' result variant.
|
|
@@ -40,60 +60,72 @@ export type ResultUnion<TResponses, TRequest, TResponse> = {
|
|
|
40
60
|
}[keyof TResponses]
|
|
41
61
|
|
|
42
62
|
/**
|
|
43
|
-
* The union of just the success (2xx) status variants
|
|
44
|
-
* untyped (`any`) error payload can never widen a success result's `data`.
|
|
63
|
+
* The union of just the success (2xx) status variants, selected by status code so an untyped error payload can never widen `data`.
|
|
45
64
|
*/
|
|
46
65
|
export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
|
|
47
66
|
[TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
|
|
48
67
|
}[Extract<keyof TResponses, SuccessStatusCode>]
|
|
49
68
|
|
|
50
69
|
/**
|
|
51
|
-
* The shape every generated function returns, discriminated by the top-level `status
|
|
52
|
-
* `throwOnError` (the default) a resolved call always means success, so the result is the union of the
|
|
53
|
-
* 2xx variants and `error` is `undefined`; without it every documented status is a variant, so a
|
|
54
|
-
* `switch (result.status)` (or narrowing on `error`) narrows `data` and `error` to that status's
|
|
55
|
-
* payload. Operations with no typed responses fall back to a `status`/`request`/`response`-only result.
|
|
70
|
+
* The shape every generated function returns, discriminated by the top-level `status`, narrowing to the 2xx variants under `throwOnError` and to every documented status without it.
|
|
56
71
|
*/
|
|
57
72
|
export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = ThrowOnError extends true
|
|
58
73
|
? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
|
|
59
|
-
? {
|
|
74
|
+
? {
|
|
75
|
+
status: number
|
|
76
|
+
data: SuccessOf<TResponses>
|
|
77
|
+
error: undefined
|
|
78
|
+
contentType: string | undefined
|
|
79
|
+
request: TRequest
|
|
80
|
+
response: TResponse
|
|
81
|
+
}
|
|
60
82
|
: SuccessResultUnion<TResponses, TRequest, TResponse>
|
|
61
83
|
: [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
|
|
62
|
-
? { status: number; data: undefined; error: undefined; request: TRequest; response: TResponse }
|
|
84
|
+
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
|
|
63
85
|
: ResultUnion<TResponses, TRequest, TResponse>
|
|
64
86
|
|
|
65
87
|
/**
|
|
66
|
-
* The data-shaped keys of the grouped options object
|
|
67
|
-
* `RequestConfig` and adds them back, typed per operation, from the generated `<Name>Request` type.
|
|
88
|
+
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
|
|
68
89
|
*/
|
|
69
|
-
export type DataShape = { body?: unknown; headers?: unknown; path?: unknown; query?: unknown }
|
|
90
|
+
export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
|
|
70
91
|
|
|
71
|
-
export type HeaderValue = string | number | boolean | null | undefined | object
|
|
72
|
-
export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
|
|
73
92
|
export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata'
|
|
74
93
|
|
|
75
94
|
/**
|
|
76
|
-
*
|
|
77
|
-
|
|
95
|
+
* Turns a raw response body into a parsed value, registered per media type as a codec's `deserialize` to handle formats the runtime does not decode itself.
|
|
96
|
+
*/
|
|
97
|
+
export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Serializes a request body for a single media type, registered per content type as a codec's `serialize` to encode formats the default serializer does not handle.
|
|
78
101
|
*/
|
|
79
|
-
export type
|
|
102
|
+
export type ContentBodySerializer = (body: unknown, contentType?: string) => unknown
|
|
80
103
|
|
|
81
104
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
105
|
+
* A per-content-type codec registered on `codecs`, keyed by content type. `serialize` encodes the
|
|
106
|
+
* request body for that media type and `deserialize` decodes the response body. Either half is
|
|
107
|
+
* optional, so a codec can handle one direction.
|
|
84
108
|
*/
|
|
85
|
-
export type
|
|
109
|
+
export type Codec = {
|
|
110
|
+
serialize?: ContentBodySerializer
|
|
111
|
+
deserialize?: Deserializer
|
|
112
|
+
}
|
|
86
113
|
|
|
87
114
|
/**
|
|
88
|
-
*
|
|
89
|
-
* transformed) value. Wires zod parsing through the per-call `parser.request` / `parser.response` /
|
|
90
|
-
* `parser.error` hooks (`error` runs on the error body when a non-2xx call does not throw).
|
|
115
|
+
* The per-call content type selection, where a bare string sets the request content type and the object form also sets the response format sent as `Accept`.
|
|
91
116
|
*/
|
|
92
|
-
export type
|
|
117
|
+
export type ContentType = string | { request?: string; response?: string }
|
|
93
118
|
|
|
94
119
|
/**
|
|
95
|
-
* A
|
|
96
|
-
*
|
|
120
|
+
* A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
|
|
121
|
+
* it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
|
|
122
|
+
* `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
|
|
123
|
+
* non-2xx call does not throw).
|
|
124
|
+
*/
|
|
125
|
+
export type Validator<T = unknown> = StandardSchemaValidator<T>
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
|
|
97
129
|
*/
|
|
98
130
|
export type Auth = {
|
|
99
131
|
type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
|
|
@@ -103,21 +135,22 @@ export type Auth = {
|
|
|
103
135
|
}
|
|
104
136
|
|
|
105
137
|
/**
|
|
106
|
-
* The token a consumer returns for a scheme, or `undefined` to skip it.
|
|
107
|
-
* prefixed by the runtime (basic is base64-encoded), so return the raw token or `user:password`.
|
|
138
|
+
* The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
|
|
108
139
|
*/
|
|
109
140
|
export type AuthToken = string | undefined
|
|
110
141
|
|
|
111
142
|
/**
|
|
112
|
-
* Resolves the token for a security scheme
|
|
113
|
-
* callback called once per scheme on a guarded operation until one returns a token.
|
|
143
|
+
* Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token.
|
|
114
144
|
*/
|
|
115
145
|
export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
|
|
116
146
|
|
|
117
147
|
/**
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
* Extra axios config the runtime spreads onto every request, an escape hatch for per-call fields it does not set itself such as `timeout`, `proxy`, and the progress callbacks.
|
|
149
|
+
*/
|
|
150
|
+
export type AxiosOptions = AxiosRequestConfig
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
|
|
121
154
|
*/
|
|
122
155
|
export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
|
|
123
156
|
baseURL?: string
|
|
@@ -126,18 +159,21 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
|
|
|
126
159
|
path?: Record<string, unknown>
|
|
127
160
|
query?: unknown
|
|
128
161
|
params?: unknown
|
|
162
|
+
cookies?: Record<string, unknown>
|
|
129
163
|
body?: TBody
|
|
130
164
|
headers?: HeadersInit
|
|
165
|
+
styles?: Styles
|
|
131
166
|
signal?: AbortSignal
|
|
132
|
-
|
|
167
|
+
options?: AxiosOptions
|
|
168
|
+
contentType?: ContentType
|
|
133
169
|
responseType?: ResponseType
|
|
134
170
|
throwOnError?: boolean
|
|
135
171
|
validateStatus?: (status: number) => boolean
|
|
136
172
|
client?: ClientInstance<TRequest, TResponse>
|
|
137
173
|
transport?: AxiosInstance
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
serializer?: Serializers
|
|
175
|
+
codecs?: Record<string, Codec>
|
|
176
|
+
validator?: { request?: Validator; response?: Validator; error?: Validator }
|
|
141
177
|
security?: Array<Auth>
|
|
142
178
|
auth?: AuthResolver
|
|
143
179
|
}
|
|
@@ -156,17 +192,17 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
|
|
|
156
192
|
}
|
|
157
193
|
|
|
158
194
|
/**
|
|
159
|
-
* Client-level configuration shared by every call an instance makes
|
|
160
|
-
* overrides these. `transport` is the axios instance the client sends through.
|
|
195
|
+
* Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
|
|
161
196
|
*/
|
|
162
197
|
export type ClientConfig = {
|
|
163
198
|
baseURL?: string
|
|
164
199
|
headers?: HeadersInit
|
|
200
|
+
options?: AxiosOptions
|
|
165
201
|
throwOnError?: boolean
|
|
166
202
|
validateStatus?: (status: number) => boolean
|
|
167
203
|
transport?: AxiosInstance
|
|
168
|
-
|
|
169
|
-
|
|
204
|
+
serializer?: Serializers
|
|
205
|
+
codecs?: Record<string, Codec>
|
|
170
206
|
auth?: AuthResolver
|
|
171
207
|
}
|
|
172
208
|
|
|
@@ -177,18 +213,15 @@ export type CallResult<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>
|
|
|
177
213
|
status: number
|
|
178
214
|
data: unknown
|
|
179
215
|
error: unknown
|
|
216
|
+
contentType: string | undefined
|
|
180
217
|
request: TRequest
|
|
181
218
|
response: TResponse
|
|
182
219
|
}
|
|
183
220
|
|
|
184
|
-
/**
|
|
185
|
-
* An interceptor function for a channel value.
|
|
186
|
-
*/
|
|
187
221
|
export type InterceptorFn<T> = (value: T) => T | Promise<T>
|
|
188
222
|
|
|
189
223
|
/**
|
|
190
|
-
* A single interceptor channel
|
|
191
|
-
* API shared with the fetch and ky runtimes, backed by axios's native interceptor managers.
|
|
224
|
+
* A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API, backed by axios's native interceptor managers.
|
|
192
225
|
*/
|
|
193
226
|
export type InterceptorChannel<T> = {
|
|
194
227
|
use: (fn: InterceptorFn<T>) => number
|
|
@@ -197,9 +230,7 @@ export type InterceptorChannel<T> = {
|
|
|
197
230
|
}
|
|
198
231
|
|
|
199
232
|
/**
|
|
200
|
-
* The three interceptor channels every client instance exposes, wrapping axios's native
|
|
201
|
-
* `interceptors.request` / `interceptors.response`. The `error` channel maps onto the response
|
|
202
|
-
* manager's rejection handler.
|
|
233
|
+
* The three interceptor channels every client instance exposes, wrapping axios's native managers with `error` mapped onto the response rejection handler.
|
|
203
234
|
*/
|
|
204
235
|
export type Interceptors = {
|
|
205
236
|
request: InterceptorChannel<InternalAxiosRequestConfig>
|
|
@@ -221,97 +252,132 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
|
|
|
221
252
|
}
|
|
222
253
|
|
|
223
254
|
/**
|
|
224
|
-
* Thrown for
|
|
225
|
-
* error body and the native request config / response stay reachable on the error.
|
|
255
|
+
* Thrown for a non-2xx response, so a resolved call always means success.
|
|
226
256
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
257
|
+
/**
|
|
258
|
+
* One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
|
|
259
|
+
*/
|
|
260
|
+
export type ServerSentEvent<TData = unknown> = {
|
|
261
|
+
data: TData
|
|
262
|
+
event?: string
|
|
263
|
+
id?: string
|
|
264
|
+
retry?: number
|
|
265
|
+
}
|
|
233
266
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
267
|
+
async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
|
|
268
|
+
if (!('getReader' in stream)) {
|
|
269
|
+
yield* stream
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const reader = stream.getReader()
|
|
274
|
+
try {
|
|
275
|
+
while (true) {
|
|
276
|
+
const { done, value } = await reader.read()
|
|
277
|
+
if (done) return
|
|
278
|
+
yield value
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
await reader.cancel().catch(() => {})
|
|
242
282
|
}
|
|
243
283
|
}
|
|
244
284
|
|
|
245
|
-
|
|
285
|
+
function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
|
|
286
|
+
const data: Array<string> = []
|
|
287
|
+
const event: ServerSentEvent<TData> = { data: undefined as TData }
|
|
288
|
+
let seen = false
|
|
289
|
+
|
|
290
|
+
for (const line of raw.split('\n')) {
|
|
291
|
+
if (!line || line.startsWith(':')) continue
|
|
292
|
+
seen = true
|
|
293
|
+
const index = line.indexOf(':')
|
|
294
|
+
const field = index === -1 ? line : line.slice(0, index)
|
|
295
|
+
const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
|
|
296
|
+
if (field === 'data') data.push(value)
|
|
297
|
+
else if (field === 'event') event.event = value
|
|
298
|
+
else if (field === 'id') event.id = value
|
|
299
|
+
else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
|
|
300
|
+
}
|
|
246
301
|
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
body instanceof FormData ||
|
|
250
|
-
body instanceof URLSearchParams ||
|
|
251
|
-
body instanceof Blob ||
|
|
252
|
-
body instanceof ArrayBuffer ||
|
|
253
|
-
ArrayBuffer.isView(body) ||
|
|
254
|
-
typeof body === 'string'
|
|
255
|
-
)
|
|
256
|
-
}
|
|
302
|
+
if (!seen) return undefined
|
|
257
303
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
304
|
+
if (data.length) {
|
|
305
|
+
const joined = data.join('\n')
|
|
306
|
+
try {
|
|
307
|
+
event.data = JSON.parse(joined) as TData
|
|
308
|
+
} catch {
|
|
309
|
+
event.data = joined as TData
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return event
|
|
264
313
|
}
|
|
265
314
|
|
|
266
315
|
/**
|
|
267
|
-
*
|
|
268
|
-
* For `multipart/form-data` plain objects become `FormData` and for
|
|
269
|
-
* `application/x-www-form-urlencoded` they become `URLSearchParams`.
|
|
316
|
+
* Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
|
|
270
317
|
*/
|
|
271
|
-
export
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
318
|
+
export async function* parseEventStream<TData = unknown>(
|
|
319
|
+
stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
320
|
+
): AsyncGenerator<ServerSentEvent<TData>> {
|
|
321
|
+
const decoder = new TextDecoder()
|
|
322
|
+
const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
|
|
323
|
+
let buffer = ''
|
|
324
|
+
|
|
325
|
+
for await (const chunk of readBytes(stream)) {
|
|
326
|
+
const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
|
|
327
|
+
buffer = blocks.pop() ?? ''
|
|
328
|
+
for (const block of blocks) {
|
|
329
|
+
const event = parseEvent<TData>(block)
|
|
330
|
+
if (event) yield event
|
|
279
331
|
}
|
|
280
|
-
return formData
|
|
281
332
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return JSON.stringify(body)
|
|
333
|
+
|
|
334
|
+
const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
|
|
335
|
+
if (event) yield event
|
|
286
336
|
}
|
|
287
337
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
|
|
296
|
-
appendQueryValue(search, `${key}[${prop}]`, propValue)
|
|
297
|
-
}
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
search.append(key, String(value))
|
|
338
|
+
/**
|
|
339
|
+
* The resolved shape returned by a generated `text/event-stream` operation: the typed event
|
|
340
|
+
* `stream` plus the native `response`.
|
|
341
|
+
*/
|
|
342
|
+
export type EventStreamResult<TData = unknown, TResponse = AxiosResponse> = {
|
|
343
|
+
stream: AsyncGenerator<ServerSentEvent<TData>>
|
|
344
|
+
response: TResponse
|
|
301
345
|
}
|
|
302
346
|
|
|
303
347
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
348
|
+
* Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
|
|
349
|
+
* the parsed events as a typed async iterator. Generated SSE operations call this.
|
|
306
350
|
*/
|
|
307
|
-
export
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
351
|
+
export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise<EventStreamResult<TData>> {
|
|
352
|
+
const { data, response } = await result
|
|
353
|
+
return {
|
|
354
|
+
response,
|
|
355
|
+
stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
|
|
360
|
+
data: TError
|
|
361
|
+
status: number
|
|
362
|
+
statusText: string
|
|
363
|
+
contentType: string | undefined
|
|
364
|
+
request: TRequest
|
|
365
|
+
response: TResponse
|
|
366
|
+
|
|
367
|
+
constructor(config: { data: TError; status: number; statusText: string; contentType?: string; request: TRequest; response: TResponse }) {
|
|
368
|
+
super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
|
|
369
|
+
this.name = 'ResponseError'
|
|
370
|
+
this.data = config.data
|
|
371
|
+
this.status = config.status
|
|
372
|
+
this.statusText = config.statusText
|
|
373
|
+
this.contentType = config.contentType
|
|
374
|
+
this.request = config.request
|
|
375
|
+
this.response = config.response
|
|
311
376
|
}
|
|
312
|
-
return search.toString()
|
|
313
377
|
}
|
|
314
378
|
|
|
379
|
+
export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
|
|
380
|
+
|
|
315
381
|
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
316
382
|
if (!headers) return {}
|
|
317
383
|
const entries = Array.isArray(headers) ? headers : Object.entries(headers)
|
|
@@ -328,23 +394,30 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
|
|
|
328
394
|
}
|
|
329
395
|
|
|
330
396
|
/**
|
|
331
|
-
* Joins the
|
|
332
|
-
* (URL-encoded), and appends the serialized query. Backs `getUrl` so a URL can be constructed
|
|
333
|
-
* without sending the request.
|
|
397
|
+
* Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, backing `getUrl`.
|
|
334
398
|
*/
|
|
335
|
-
function serializeUrl(
|
|
399
|
+
function serializeUrl({
|
|
400
|
+
parts,
|
|
401
|
+
pathParams,
|
|
402
|
+
search,
|
|
403
|
+
pathSerializer = defaultPathSerializer,
|
|
404
|
+
pathStyles,
|
|
405
|
+
}: {
|
|
406
|
+
parts: Array<string | undefined>
|
|
407
|
+
pathParams: Record<string, unknown>
|
|
408
|
+
search: string
|
|
409
|
+
pathSerializer?: PathSerializer
|
|
410
|
+
pathStyles?: Record<string, PathParamStyle>
|
|
411
|
+
}): string {
|
|
336
412
|
const path = parts
|
|
337
413
|
.filter(Boolean)
|
|
338
414
|
.join('')
|
|
339
|
-
.replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
415
|
+
.replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
|
|
340
416
|
return path + (search ? `?${search}` : '')
|
|
341
417
|
}
|
|
342
418
|
|
|
343
419
|
/**
|
|
344
|
-
* Walks the per-operation security in order and places the first resolved token on the request,
|
|
345
|
-
* mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
|
|
346
|
-
* Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
|
|
347
|
-
* `name` in the header, query, or cookie.
|
|
420
|
+
* Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
|
|
348
421
|
*/
|
|
349
422
|
export async function resolveAuth(params: {
|
|
350
423
|
security: Array<Auth> | undefined
|
|
@@ -371,15 +444,13 @@ export async function resolveAuth(params: {
|
|
|
371
444
|
}
|
|
372
445
|
}
|
|
373
446
|
|
|
374
|
-
async function
|
|
375
|
-
if (!
|
|
376
|
-
return (
|
|
447
|
+
async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
|
|
448
|
+
if (!validator) return value
|
|
449
|
+
return validateStandardSchema(validator, value)
|
|
377
450
|
}
|
|
378
451
|
|
|
379
452
|
/**
|
|
380
|
-
* Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API
|
|
381
|
-
* external id is mapped onto axios's own id so `update` can swap a handler in place even though axios
|
|
382
|
-
* has no native update.
|
|
453
|
+
* Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place.
|
|
383
454
|
*/
|
|
384
455
|
function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
|
|
385
456
|
const ids = new Map<number, number>()
|
|
@@ -405,13 +476,32 @@ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number,
|
|
|
405
476
|
}
|
|
406
477
|
|
|
407
478
|
/**
|
|
408
|
-
*
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
479
|
+
* The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
|
|
480
|
+
*/
|
|
481
|
+
function baseContentType(value: string | null | undefined): string | undefined {
|
|
482
|
+
if (!value) return undefined
|
|
483
|
+
return value.split(';')[0]!.trim().toLowerCase() || undefined
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Reads the negotiated response content type from the response headers as a base media type.
|
|
488
|
+
*/
|
|
489
|
+
function getResponseContentType(headers: Record<string, unknown> | undefined): string | undefined {
|
|
490
|
+
if (!headers) return undefined
|
|
491
|
+
const value = headers['content-type'] ?? headers['Content-Type']
|
|
492
|
+
return baseContentType(typeof value === 'string' ? value : undefined)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Normalizes the `contentType` option to its `{ request, response }` form, treating a bare string as the request content type.
|
|
497
|
+
*/
|
|
498
|
+
function resolveContentType(contentType: ContentType | undefined): { request?: string; response?: string } {
|
|
499
|
+
if (typeof contentType === 'string') return { request: contentType }
|
|
500
|
+
return contentType ?? {}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Builds the shared client core bound to an axios instance (defaulting to `axios.create()`), with `throwOnError` riding axios's `validateStatus`.
|
|
415
505
|
*/
|
|
416
506
|
export function createClientCore<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>(options: ClientConfig = {}): ClientInstance<TRequest, TResponse> {
|
|
417
507
|
let config: ClientConfig = { ...options }
|
|
@@ -440,11 +530,17 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
440
530
|
|
|
441
531
|
const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
|
|
442
532
|
const activeInstance = requestConfig.transport ?? config.transport ?? instance
|
|
443
|
-
const querySerializer = requestConfig.
|
|
444
|
-
const bodySerializer = requestConfig.
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
|
|
533
|
+
const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
|
|
534
|
+
const bodySerializer = requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer
|
|
535
|
+
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
536
|
+
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
537
|
+
|
|
538
|
+
const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
|
|
539
|
+
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
540
|
+
const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
|
|
541
|
+
if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
|
|
542
|
+
headers['Accept'] = responseContentType
|
|
543
|
+
}
|
|
448
544
|
|
|
449
545
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
450
546
|
|
|
@@ -455,29 +551,43 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
455
551
|
query,
|
|
456
552
|
})
|
|
457
553
|
|
|
458
|
-
|
|
459
|
-
|
|
554
|
+
if (requestConfig.cookies) {
|
|
555
|
+
const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
|
|
556
|
+
if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body)
|
|
560
|
+
const requestContentTypeBase = baseContentType(requestContentType)
|
|
561
|
+
const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
|
|
562
|
+
const body = contentCodec?.serialize
|
|
563
|
+
? contentCodec.serialize(validatedBody, requestContentType)
|
|
564
|
+
: bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
|
|
460
565
|
// A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
|
|
461
566
|
if (body instanceof FormData) {
|
|
462
567
|
delete headers['Content-Type']
|
|
463
568
|
delete headers['content-type']
|
|
464
|
-
} else if (
|
|
465
|
-
headers['Content-Type'] =
|
|
569
|
+
} else if (requestContentTypeOption) {
|
|
570
|
+
headers['Content-Type'] = requestContentTypeOption
|
|
466
571
|
}
|
|
467
572
|
const pathParams = requestConfig.path ?? {}
|
|
468
|
-
const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
573
|
+
const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
574
|
+
pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }),
|
|
575
|
+
)
|
|
469
576
|
const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
|
|
470
577
|
|
|
471
578
|
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
472
579
|
const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
|
|
473
580
|
|
|
581
|
+
const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
|
|
582
|
+
|
|
474
583
|
const axiosConfig: AxiosRequestConfig = {
|
|
584
|
+
...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, …
|
|
475
585
|
url,
|
|
476
586
|
baseURL,
|
|
477
587
|
method: requestConfig.method ?? 'GET',
|
|
478
588
|
headers,
|
|
479
589
|
params: query,
|
|
480
|
-
paramsSerializer: (params) => querySerializer(params as Record<string, unknown
|
|
590
|
+
paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
|
|
481
591
|
data: body,
|
|
482
592
|
transformRequest: (data) => data,
|
|
483
593
|
signal: requestConfig.signal,
|
|
@@ -485,15 +595,28 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
485
595
|
validateStatus,
|
|
486
596
|
}
|
|
487
597
|
|
|
598
|
+
// Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser;
|
|
599
|
+
// the default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter.
|
|
600
|
+
if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) {
|
|
601
|
+
axiosConfig.adapter = 'fetch'
|
|
602
|
+
}
|
|
603
|
+
|
|
488
604
|
try {
|
|
489
605
|
const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
|
|
490
606
|
const isSuccess = response.status >= 200 && response.status < 300
|
|
491
|
-
const
|
|
492
|
-
|
|
607
|
+
const contentType = getResponseContentType(response.headers as Record<string, unknown>)
|
|
608
|
+
let decoded: unknown = response.data
|
|
609
|
+
if (contentType) {
|
|
610
|
+
const codec = codecs[contentType]
|
|
611
|
+
if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType)
|
|
612
|
+
}
|
|
613
|
+
const data = isSuccess ? await runValidator(requestConfig.validator?.response, decoded) : undefined
|
|
614
|
+
const error = isSuccess ? undefined : await runValidator(requestConfig.validator?.error, decoded)
|
|
493
615
|
return {
|
|
494
616
|
status: response.status,
|
|
495
617
|
data,
|
|
496
618
|
error,
|
|
619
|
+
contentType,
|
|
497
620
|
request: response.config as TRequest,
|
|
498
621
|
response: response as TResponse,
|
|
499
622
|
}
|
|
@@ -504,6 +627,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
504
627
|
data: axiosError.response.data,
|
|
505
628
|
status: axiosError.response.status,
|
|
506
629
|
statusText: axiosError.response.statusText,
|
|
630
|
+
contentType: getResponseContentType(axiosError.response.headers as Record<string, unknown>),
|
|
507
631
|
request: axiosError.config as TRequest,
|
|
508
632
|
response: axiosError.response as TResponse,
|
|
509
633
|
})
|
|
@@ -518,9 +642,16 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
518
642
|
return config
|
|
519
643
|
}
|
|
520
644
|
client.getUrl = (requestConfig) => {
|
|
521
|
-
const querySerializer = requestConfig.
|
|
645
|
+
const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
|
|
646
|
+
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
522
647
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
523
|
-
return serializeUrl(
|
|
648
|
+
return serializeUrl({
|
|
649
|
+
parts: [config.baseURL, requestConfig.baseURL, requestConfig.url],
|
|
650
|
+
pathParams: requestConfig.path ?? {},
|
|
651
|
+
search: querySerializer(query, requestConfig.styles?.query),
|
|
652
|
+
pathSerializer,
|
|
653
|
+
pathStyles: requestConfig.styles?.path,
|
|
654
|
+
})
|
|
524
655
|
}
|
|
525
656
|
client.interceptors = interceptors
|
|
526
657
|
client.createClient = (next) => createClientCore<TRequest, TResponse>({ ...config, ...next })
|