@kubb/plugin-axios 5.0.0-beta.77 → 5.0.0-beta.80
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 +276 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +276 -88
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/generators/clientGenerator.tsx +24 -11
- package/src/plugin.ts +19 -5
- package/src/templates.ts +9 -4
- package/templates/axios.ts +288 -173
- 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.
|
|
101
|
+
*/
|
|
102
|
+
export type ContentBodySerializer = (body: unknown, contentType?: string) => unknown
|
|
103
|
+
|
|
104
|
+
/**
|
|
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.
|
|
78
108
|
*/
|
|
79
|
-
export type
|
|
109
|
+
export type Codec = {
|
|
110
|
+
serialize?: ContentBodySerializer
|
|
111
|
+
deserialize?: Deserializer
|
|
112
|
+
}
|
|
80
113
|
|
|
81
114
|
/**
|
|
82
|
-
*
|
|
83
|
-
* `ArrayBuffer`, and string bodies pass through untouched.
|
|
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`.
|
|
84
116
|
*/
|
|
85
|
-
export type
|
|
117
|
+
export type ContentType = string | { request?: string; response?: string }
|
|
86
118
|
|
|
87
119
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
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).
|
|
91
124
|
*/
|
|
92
|
-
export type
|
|
125
|
+
export type Validator<T = unknown> = StandardSchemaValidator<T>
|
|
93
126
|
|
|
94
127
|
/**
|
|
95
|
-
* A resolved security scheme carried on each generated call's `security` array
|
|
96
|
-
* to the configured `auth` resolver and places the returned token accordingly.
|
|
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,32 +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
|
-
* Extra axios config the runtime spreads onto every request, an escape hatch for
|
|
119
|
-
* it does not set itself, such as `timeout`, `proxy`, `maxRedirects`, `decompress`, and the
|
|
120
|
-
* `onUploadProgress` / `onDownloadProgress` callbacks. The runtime-owned fields (`url`, `baseURL`,
|
|
121
|
-
* `method`, `headers`, `params`, `paramsSerializer`, `data`, `transformRequest`, `signal`,
|
|
122
|
-
* `responseType`, `validateStatus`) always win over anything set here. This is the per-request analog
|
|
123
|
-
* of the `transport` instance, which stays the place for cross-cutting concerns like retries and
|
|
124
|
-
* interceptors. It mirrors `options` in `@kubb/plugin-fetch`.
|
|
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.
|
|
125
149
|
*/
|
|
126
150
|
export type AxiosOptions = AxiosRequestConfig
|
|
127
151
|
|
|
128
152
|
/**
|
|
129
|
-
* The request a generated function hands to the runtime
|
|
130
|
-
* from the grouped options; everything else is plain request configuration. `transport` carries an
|
|
131
|
-
* axios instance and `validateStatus` rides axios's own contract.
|
|
153
|
+
* The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
|
|
132
154
|
*/
|
|
133
155
|
export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
|
|
134
156
|
baseURL?: string
|
|
@@ -137,19 +159,21 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
|
|
|
137
159
|
path?: Record<string, unknown>
|
|
138
160
|
query?: unknown
|
|
139
161
|
params?: unknown
|
|
162
|
+
cookies?: Record<string, unknown>
|
|
140
163
|
body?: TBody
|
|
141
164
|
headers?: HeadersInit
|
|
165
|
+
styles?: Styles
|
|
142
166
|
signal?: AbortSignal
|
|
143
167
|
options?: AxiosOptions
|
|
144
|
-
contentType?:
|
|
168
|
+
contentType?: ContentType
|
|
145
169
|
responseType?: ResponseType
|
|
146
170
|
throwOnError?: boolean
|
|
147
171
|
validateStatus?: (status: number) => boolean
|
|
148
172
|
client?: ClientInstance<TRequest, TResponse>
|
|
149
173
|
transport?: AxiosInstance
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
174
|
+
serializer?: Serializers
|
|
175
|
+
codecs?: Record<string, Codec>
|
|
176
|
+
validator?: { request?: Validator; response?: Validator; error?: Validator }
|
|
153
177
|
security?: Array<Auth>
|
|
154
178
|
auth?: AuthResolver
|
|
155
179
|
}
|
|
@@ -168,8 +192,7 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
|
|
|
168
192
|
}
|
|
169
193
|
|
|
170
194
|
/**
|
|
171
|
-
* Client-level configuration shared by every call an instance makes
|
|
172
|
-
* 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`.
|
|
173
196
|
*/
|
|
174
197
|
export type ClientConfig = {
|
|
175
198
|
baseURL?: string
|
|
@@ -178,8 +201,8 @@ export type ClientConfig = {
|
|
|
178
201
|
throwOnError?: boolean
|
|
179
202
|
validateStatus?: (status: number) => boolean
|
|
180
203
|
transport?: AxiosInstance
|
|
181
|
-
|
|
182
|
-
|
|
204
|
+
serializer?: Serializers
|
|
205
|
+
codecs?: Record<string, Codec>
|
|
183
206
|
auth?: AuthResolver
|
|
184
207
|
}
|
|
185
208
|
|
|
@@ -190,18 +213,15 @@ export type CallResult<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>
|
|
|
190
213
|
status: number
|
|
191
214
|
data: unknown
|
|
192
215
|
error: unknown
|
|
216
|
+
contentType: string | undefined
|
|
193
217
|
request: TRequest
|
|
194
218
|
response: TResponse
|
|
195
219
|
}
|
|
196
220
|
|
|
197
|
-
/**
|
|
198
|
-
* An interceptor function for a channel value.
|
|
199
|
-
*/
|
|
200
221
|
export type InterceptorFn<T> = (value: T) => T | Promise<T>
|
|
201
222
|
|
|
202
223
|
/**
|
|
203
|
-
* A single interceptor channel
|
|
204
|
-
* 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.
|
|
205
225
|
*/
|
|
206
226
|
export type InterceptorChannel<T> = {
|
|
207
227
|
use: (fn: InterceptorFn<T>) => number
|
|
@@ -210,9 +230,7 @@ export type InterceptorChannel<T> = {
|
|
|
210
230
|
}
|
|
211
231
|
|
|
212
232
|
/**
|
|
213
|
-
* The three interceptor channels every client instance exposes, wrapping axios's native
|
|
214
|
-
* `interceptors.request` / `interceptors.response`. The `error` channel maps onto the response
|
|
215
|
-
* 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.
|
|
216
234
|
*/
|
|
217
235
|
export type Interceptors = {
|
|
218
236
|
request: InterceptorChannel<InternalAxiosRequestConfig>
|
|
@@ -234,97 +252,132 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
|
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
/**
|
|
237
|
-
* Thrown for
|
|
238
|
-
* 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.
|
|
239
256
|
*/
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
266
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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(() => {})
|
|
255
282
|
}
|
|
256
283
|
}
|
|
257
284
|
|
|
258
|
-
|
|
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
|
+
}
|
|
259
301
|
|
|
260
|
-
|
|
261
|
-
return (
|
|
262
|
-
body instanceof FormData ||
|
|
263
|
-
body instanceof URLSearchParams ||
|
|
264
|
-
body instanceof Blob ||
|
|
265
|
-
body instanceof ArrayBuffer ||
|
|
266
|
-
ArrayBuffer.isView(body) ||
|
|
267
|
-
typeof body === 'string'
|
|
268
|
-
)
|
|
269
|
-
}
|
|
302
|
+
if (!seen) return undefined
|
|
270
303
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
277
313
|
}
|
|
278
314
|
|
|
279
315
|
/**
|
|
280
|
-
*
|
|
281
|
-
* For `multipart/form-data` plain objects become `FormData` and for
|
|
282
|
-
* `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.
|
|
283
317
|
*/
|
|
284
|
-
export
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
292
331
|
}
|
|
293
|
-
return formData
|
|
294
|
-
}
|
|
295
|
-
if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
296
|
-
return new URLSearchParams(body as Record<string, string>)
|
|
297
332
|
}
|
|
298
|
-
|
|
333
|
+
|
|
334
|
+
const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
|
|
335
|
+
if (event) yield event
|
|
299
336
|
}
|
|
300
337
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
|
|
309
|
-
appendQueryValue(search, `${key}[${prop}]`, propValue)
|
|
310
|
-
}
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
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
|
|
314
345
|
}
|
|
315
346
|
|
|
316
347
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
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.
|
|
319
350
|
*/
|
|
320
|
-
export
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
324
376
|
}
|
|
325
|
-
return search.toString()
|
|
326
377
|
}
|
|
327
378
|
|
|
379
|
+
export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
|
|
380
|
+
|
|
328
381
|
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
329
382
|
if (!headers) return {}
|
|
330
383
|
const entries = Array.isArray(headers) ? headers : Object.entries(headers)
|
|
@@ -341,23 +394,30 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
|
|
|
341
394
|
}
|
|
342
395
|
|
|
343
396
|
/**
|
|
344
|
-
* Joins the
|
|
345
|
-
* (URL-encoded), and appends the serialized query. Backs `getUrl` so a URL can be constructed
|
|
346
|
-
* without sending the request.
|
|
397
|
+
* Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, backing `getUrl`.
|
|
347
398
|
*/
|
|
348
|
-
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 {
|
|
349
412
|
const path = parts
|
|
350
413
|
.filter(Boolean)
|
|
351
414
|
.join('')
|
|
352
|
-
.replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
415
|
+
.replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
|
|
353
416
|
return path + (search ? `?${search}` : '')
|
|
354
417
|
}
|
|
355
418
|
|
|
356
419
|
/**
|
|
357
|
-
* Walks the per-operation security in order and places the first resolved token on the request,
|
|
358
|
-
* mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
|
|
359
|
-
* Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
|
|
360
|
-
* `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.
|
|
361
421
|
*/
|
|
362
422
|
export async function resolveAuth(params: {
|
|
363
423
|
security: Array<Auth> | undefined
|
|
@@ -384,15 +444,13 @@ export async function resolveAuth(params: {
|
|
|
384
444
|
}
|
|
385
445
|
}
|
|
386
446
|
|
|
387
|
-
async function
|
|
388
|
-
if (!
|
|
389
|
-
return (
|
|
447
|
+
async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
|
|
448
|
+
if (!validator) return value
|
|
449
|
+
return validateStandardSchema(validator, value)
|
|
390
450
|
}
|
|
391
451
|
|
|
392
452
|
/**
|
|
393
|
-
* Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API
|
|
394
|
-
* external id is mapped onto axios's own id so `update` can swap a handler in place even though axios
|
|
395
|
-
* 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.
|
|
396
454
|
*/
|
|
397
455
|
function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
|
|
398
456
|
const ids = new Map<number, number>()
|
|
@@ -418,13 +476,32 @@ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number,
|
|
|
418
476
|
}
|
|
419
477
|
|
|
420
478
|
/**
|
|
421
|
-
*
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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`.
|
|
428
505
|
*/
|
|
429
506
|
export function createClientCore<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>(options: ClientConfig = {}): ClientInstance<TRequest, TResponse> {
|
|
430
507
|
let config: ClientConfig = { ...options }
|
|
@@ -453,11 +530,17 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
453
530
|
|
|
454
531
|
const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
|
|
455
532
|
const activeInstance = requestConfig.transport ?? config.transport ?? instance
|
|
456
|
-
const querySerializer = requestConfig.
|
|
457
|
-
const bodySerializer = requestConfig.
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
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
|
+
}
|
|
461
544
|
|
|
462
545
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
463
546
|
|
|
@@ -468,17 +551,28 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
468
551
|
query,
|
|
469
552
|
})
|
|
470
553
|
|
|
471
|
-
|
|
472
|
-
|
|
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 })
|
|
473
565
|
// A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
|
|
474
566
|
if (body instanceof FormData) {
|
|
475
567
|
delete headers['Content-Type']
|
|
476
568
|
delete headers['content-type']
|
|
477
|
-
} else if (
|
|
478
|
-
headers['Content-Type'] =
|
|
569
|
+
} else if (requestContentTypeOption) {
|
|
570
|
+
headers['Content-Type'] = requestContentTypeOption
|
|
479
571
|
}
|
|
480
572
|
const pathParams = requestConfig.path ?? {}
|
|
481
|
-
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
|
+
)
|
|
482
576
|
const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
|
|
483
577
|
|
|
484
578
|
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
@@ -493,7 +587,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
493
587
|
method: requestConfig.method ?? 'GET',
|
|
494
588
|
headers,
|
|
495
589
|
params: query,
|
|
496
|
-
paramsSerializer: (params) => querySerializer(params as Record<string, unknown
|
|
590
|
+
paramsSerializer: (params) => querySerializer(params as Record<string, unknown>, requestConfig.styles?.query),
|
|
497
591
|
data: body,
|
|
498
592
|
transformRequest: (data) => data,
|
|
499
593
|
signal: requestConfig.signal,
|
|
@@ -501,15 +595,28 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
501
595
|
validateStatus,
|
|
502
596
|
}
|
|
503
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
|
+
|
|
504
604
|
try {
|
|
505
605
|
const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
|
|
506
606
|
const isSuccess = response.status >= 200 && response.status < 300
|
|
507
|
-
const
|
|
508
|
-
|
|
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)
|
|
509
615
|
return {
|
|
510
616
|
status: response.status,
|
|
511
617
|
data,
|
|
512
618
|
error,
|
|
619
|
+
contentType,
|
|
513
620
|
request: response.config as TRequest,
|
|
514
621
|
response: response as TResponse,
|
|
515
622
|
}
|
|
@@ -520,6 +627,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
520
627
|
data: axiosError.response.data,
|
|
521
628
|
status: axiosError.response.status,
|
|
522
629
|
statusText: axiosError.response.statusText,
|
|
630
|
+
contentType: getResponseContentType(axiosError.response.headers as Record<string, unknown>),
|
|
523
631
|
request: axiosError.config as TRequest,
|
|
524
632
|
response: axiosError.response as TResponse,
|
|
525
633
|
})
|
|
@@ -534,9 +642,16 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
|
|
|
534
642
|
return config
|
|
535
643
|
}
|
|
536
644
|
client.getUrl = (requestConfig) => {
|
|
537
|
-
const querySerializer = requestConfig.
|
|
645
|
+
const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
|
|
646
|
+
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
538
647
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
539
|
-
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
|
+
})
|
|
540
655
|
}
|
|
541
656
|
client.interceptors = interceptors
|
|
542
657
|
client.createClient = (next) => createClientCore<TRequest, TResponse>({ ...config, ...next })
|