@kubb/plugin-axios 5.0.0-beta.73

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.
@@ -0,0 +1,490 @@
1
+ import axios from 'axios'
2
+ import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
3
+
4
+ /**
5
+ * HTTP status codes treated as a success. A resolved call only ever carries a body from one of
6
+ * these; everything else is an error (thrown by default, or surfaced on `error`).
7
+ */
8
+ export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
9
+
10
+ /**
11
+ * The success members of a per-status responses record (`{ '200': ...; '404': ... }`).
12
+ */
13
+ export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
14
+
15
+ /**
16
+ * The error members of a per-status responses record — every documented status that is not a 2xx.
17
+ */
18
+ export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
19
+
20
+ /**
21
+ * Converts a response record's string status key to its numeric literal (`'200'` becomes `200`).
22
+ * Non-numeric keys such as the OpenAPI `default` response stay `number`.
23
+ */
24
+ export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
25
+
26
+ /**
27
+ * One result variant for a single documented status. `status` is the numeric literal at the top
28
+ * level, so a `switch (result.status)` narrows `data` (a 2xx status) or `error` (everything else) to
29
+ * that status's payload.
30
+ */
31
+ export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
32
+ ? { status: ToStatusNumber<TStatus>; data: TResponses[TStatus]; error: undefined; request: TRequest; response: TResponse }
33
+ : { status: ToStatusNumber<TStatus>; data: undefined; error: TResponses[TStatus]; request: TRequest; response: TResponse }
34
+
35
+ /**
36
+ * The union of every documented status' result variant.
37
+ */
38
+ export type ResultUnion<TResponses, TRequest, TResponse> = {
39
+ [TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
40
+ }[keyof TResponses]
41
+
42
+ /**
43
+ * The union of just the success (2xx) status variants. Selected by status code, not by `error`, so an
44
+ * untyped (`any`) error payload can never widen a success result's `data`.
45
+ */
46
+ export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
47
+ [TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
48
+ }[Extract<keyof TResponses, SuccessStatusCode>]
49
+
50
+ /**
51
+ * The shape every generated function returns, discriminated by the top-level `status`. With
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.
56
+ */
57
+ export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = ThrowOnError extends true
58
+ ? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
59
+ ? { status: number; data: SuccessOf<TResponses>; error: undefined; request: TRequest; response: TResponse }
60
+ : SuccessResultUnion<TResponses, TRequest, TResponse>
61
+ : [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
62
+ ? { status: number; data: undefined; error: undefined; request: TRequest; response: TResponse }
63
+ : ResultUnion<TResponses, TRequest, TResponse>
64
+
65
+ /**
66
+ * The data-shaped keys of the grouped options object. `Options` subtracts these from the runtime
67
+ * `RequestConfig` and adds them back, typed per operation, from the generated `<Name>Request` type.
68
+ */
69
+ export type DataShape = { body?: unknown; headers?: unknown; path?: unknown; query?: unknown }
70
+
71
+ export type HeaderValue = string | number | boolean | null | undefined | object
72
+ export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
73
+ export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata'
74
+
75
+ /**
76
+ * Serializes the query object into a search string. Array and object members follow the configured
77
+ * style (`form` with `explode` by default; `deepObject` for nested objects).
78
+ */
79
+ export type QuerySerializer = (params: Record<string, unknown>) => string
80
+
81
+ /**
82
+ * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`,
83
+ * `ArrayBuffer`, and string bodies pass through untouched.
84
+ */
85
+ export type BodySerializer = (body: unknown, contentType?: string) => unknown
86
+
87
+ /**
88
+ * Parses a value before it is sent or after it is received, returning the parsed (and optionally
89
+ * transformed) value. Wires zod parsing through the per-call `parser.request` / `parser.response` hooks.
90
+ */
91
+ export type Parser<T = unknown> = (value: T) => T | Promise<T>
92
+
93
+ /**
94
+ * A resolved security scheme carried on each generated call's `security` array. The runtime passes it
95
+ * to the configured `auth` resolver and places the returned token accordingly.
96
+ */
97
+ export type Auth = {
98
+ type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
99
+ scheme?: 'bearer' | 'basic'
100
+ name?: string
101
+ in?: 'header' | 'query' | 'cookie'
102
+ }
103
+
104
+ /**
105
+ * The token a consumer returns for a scheme, or `undefined` to skip it. Bearer and basic schemes are
106
+ * prefixed by the runtime (basic is base64-encoded), so return the raw token or `user:password`.
107
+ */
108
+ export type AuthToken = string | undefined
109
+
110
+ /**
111
+ * Resolves the token for a security scheme: either a static token used for every scheme, or a
112
+ * callback called once per scheme on a guarded operation until one returns a token.
113
+ */
114
+ export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
115
+
116
+ /**
117
+ * The request a generated function hands to the runtime. `body` / `headers` / `path` / `query` come
118
+ * from the grouped options; everything else is plain request configuration. `transport` carries an
119
+ * axios instance and `validateStatus` rides axios's own contract.
120
+ */
121
+ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
122
+ baseURL?: string
123
+ url?: string
124
+ method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD'
125
+ path?: Record<string, unknown>
126
+ query?: unknown
127
+ params?: unknown
128
+ body?: TBody
129
+ headers?: HeadersInit
130
+ signal?: AbortSignal
131
+ contentType?: string
132
+ responseType?: ResponseType
133
+ throwOnError?: boolean
134
+ validateStatus?: (status: number) => boolean
135
+ client?: ClientInstance<TRequest, TResponse>
136
+ transport?: AxiosInstance
137
+ querySerializer?: QuerySerializer
138
+ bodySerializer?: BodySerializer
139
+ parser?: { request?: Parser; response?: Parser }
140
+ security?: Array<Auth>
141
+ auth?: AuthResolver
142
+ }
143
+
144
+ /**
145
+ * The grouped options object passed to every generated function: the request config minus the
146
+ * data-shaped keys and the literal `url`, plus the per-operation `<Name>Request`.
147
+ */
148
+ export type Options<TData extends DataShape, ThrowOnError extends boolean = true, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = Omit<
149
+ RequestConfig<unknown, TRequest, TResponse>,
150
+ keyof DataShape | 'url'
151
+ > &
152
+ TData & {
153
+ client?: ClientInstance<TRequest, TResponse>
154
+ throwOnError?: ThrowOnError
155
+ }
156
+
157
+ /**
158
+ * Client-level configuration shared by every call an instance makes. Per-call `RequestConfig`
159
+ * overrides these. `transport` is the axios instance the client sends through.
160
+ */
161
+ export type ClientConfig = {
162
+ baseURL?: string
163
+ headers?: HeadersInit
164
+ throwOnError?: boolean
165
+ validateStatus?: (status: number) => boolean
166
+ transport?: AxiosInstance
167
+ querySerializer?: QuerySerializer
168
+ bodySerializer?: BodySerializer
169
+ auth?: AuthResolver
170
+ }
171
+
172
+ /**
173
+ * The result a resolved call produces before it is cast to `RequestResult` by the generated wrapper.
174
+ */
175
+ export type CallResult<TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
176
+ status: number
177
+ data: unknown
178
+ error: unknown
179
+ request: TRequest
180
+ response: TResponse
181
+ }
182
+
183
+ /**
184
+ * An interceptor function for a channel value.
185
+ */
186
+ export type InterceptorFn<T> = (value: T) => T | Promise<T>
187
+
188
+ /**
189
+ * A single interceptor channel (request, response, or error) exposing the `use` / `eject` / `update`
190
+ * API shared with the fetch and ky runtimes, backed by axios's native interceptor managers.
191
+ */
192
+ export type InterceptorChannel<T> = {
193
+ use: (fn: InterceptorFn<T>) => number
194
+ eject: (id: number) => void
195
+ update: (id: number, fn: InterceptorFn<T>) => void
196
+ }
197
+
198
+ /**
199
+ * The three interceptor channels every client instance exposes, wrapping axios's native
200
+ * `interceptors.request` / `interceptors.response`. The `error` channel maps onto the response
201
+ * manager's rejection handler.
202
+ */
203
+ export type Interceptors = {
204
+ request: InterceptorChannel<InternalAxiosRequestConfig>
205
+ response: InterceptorChannel<AxiosResponse>
206
+ error: InterceptorChannel<AxiosError>
207
+ }
208
+
209
+ /**
210
+ * A client instance: the callable send plus configuration, interceptors, and an isolated
211
+ * `createClient` factory.
212
+ */
213
+ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosResponse> = {
214
+ <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
215
+ getConfig: () => ClientConfig
216
+ setConfig: (config: ClientConfig) => ClientConfig
217
+ interceptors: Interceptors
218
+ createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>
219
+ }
220
+
221
+ /**
222
+ * Thrown for responses outside the 2xx range, so a resolved call always means success. The parsed
223
+ * error body and the native request config / response stay reachable on the error.
224
+ */
225
+ export class ResponseError<TError = unknown, TRequest = AxiosRequestConfig, TResponse = AxiosResponse> extends Error {
226
+ data: TError
227
+ status: number
228
+ statusText: string
229
+ request: TRequest
230
+ response: TResponse
231
+
232
+ constructor(config: { data: TError; status: number; statusText: string; request: TRequest; response: TResponse }) {
233
+ super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
234
+ this.name = 'ResponseError'
235
+ this.data = config.data
236
+ this.status = config.status
237
+ this.statusText = config.statusText
238
+ this.request = config.request
239
+ this.response = config.response
240
+ }
241
+ }
242
+
243
+ export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
244
+
245
+ function isFormBody(body: unknown): boolean {
246
+ return (
247
+ body instanceof FormData ||
248
+ body instanceof URLSearchParams ||
249
+ body instanceof Blob ||
250
+ body instanceof ArrayBuffer ||
251
+ ArrayBuffer.isView(body) ||
252
+ typeof body === 'string'
253
+ )
254
+ }
255
+
256
+ /**
257
+ * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
258
+ * For `application/x-www-form-urlencoded` plain objects become `URLSearchParams`.
259
+ */
260
+ export const defaultBodySerializer: BodySerializer = (body, contentType) => {
261
+ if (body === undefined || body === null) return undefined
262
+ if (isFormBody(body)) return body
263
+ if (contentType?.includes('application/x-www-form-urlencoded')) {
264
+ return new URLSearchParams(body as Record<string, string>)
265
+ }
266
+ return JSON.stringify(body)
267
+ }
268
+
269
+ function appendQueryValue(search: URLSearchParams, key: string, value: unknown): void {
270
+ if (value === undefined || value === null) return
271
+ if (Array.isArray(value)) {
272
+ for (const item of value) appendQueryValue(search, key, item)
273
+ return
274
+ }
275
+ if (typeof value === 'object') {
276
+ for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
277
+ appendQueryValue(search, `${key}[${prop}]`, propValue)
278
+ }
279
+ return
280
+ }
281
+ search.append(key, String(value))
282
+ }
283
+
284
+ /**
285
+ * Default query serializer: arrays explode into repeated keys and nested objects use the
286
+ * `deepObject` style (`key[prop]=value`).
287
+ */
288
+ export const defaultQuerySerializer: QuerySerializer = (params) => {
289
+ const search = new URLSearchParams()
290
+ for (const [key, value] of Object.entries(params)) {
291
+ appendQueryValue(search, key, value)
292
+ }
293
+ return search.toString()
294
+ }
295
+
296
+ function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
297
+ if (!headers) return {}
298
+ const entries = Array.isArray(headers) ? headers : Object.entries(headers)
299
+ const result: Record<string, string> = {}
300
+ for (const [key, value] of entries) {
301
+ if (value === undefined || value === null) continue
302
+ result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value)
303
+ }
304
+ return result
305
+ }
306
+
307
+ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string, string> {
308
+ return Object.assign({}, ...sources.map(serializeHeaders))
309
+ }
310
+
311
+ /**
312
+ * Walks the per-operation security in order and places the first resolved token on the request,
313
+ * mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
314
+ * Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
315
+ * `name` in the header, query, or cookie.
316
+ */
317
+ export async function resolveAuth(params: {
318
+ security: Array<Auth> | undefined
319
+ auth: AuthResolver | undefined
320
+ headers: Record<string, string>
321
+ query: Record<string, unknown>
322
+ }): Promise<void> {
323
+ const { security, auth, headers, query } = params
324
+ if (!security?.length || auth === undefined) return
325
+
326
+ for (const scheme of security) {
327
+ const token = typeof auth === 'function' ? await auth(scheme) : auth
328
+ if (token === undefined) continue
329
+
330
+ if (scheme.type === 'apiKey') {
331
+ const name = scheme.name ?? 'Authorization'
332
+ if (scheme.in === 'query') query[name] = token
333
+ else if (scheme.in === 'cookie') headers.Cookie = [headers.Cookie, `${name}=${token}`].filter(Boolean).join('; ')
334
+ else headers[name] = token
335
+ } else {
336
+ headers.Authorization = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}`
337
+ }
338
+ return
339
+ }
340
+ }
341
+
342
+ async function runParser<T>(parser: Parser | undefined, value: T): Promise<T> {
343
+ if (!parser) return value
344
+ return (await parser(value)) as T
345
+ }
346
+
347
+ /**
348
+ * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API. A stable
349
+ * external id is mapped onto axios's own id so `update` can swap a handler in place even though axios
350
+ * has no native update.
351
+ */
352
+ function createInterceptorChannel<T>(register: (fn: InterceptorFn<T>) => number, ejectNative: (id: number) => void): InterceptorChannel<T> {
353
+ const ids = new Map<number, number>()
354
+ let counter = 0
355
+ return {
356
+ use(fn) {
357
+ const id = ++counter
358
+ ids.set(id, register(fn))
359
+ return id
360
+ },
361
+ eject(id) {
362
+ const nativeId = ids.get(id)
363
+ if (nativeId === undefined) return
364
+ ejectNative(nativeId)
365
+ ids.delete(id)
366
+ },
367
+ update(id, fn) {
368
+ const nativeId = ids.get(id)
369
+ if (nativeId !== undefined) ejectNative(nativeId)
370
+ ids.set(id, register(fn))
371
+ },
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`). The
377
+ * interceptor channels delegate to the instance's native managers, and `querySerializer` /
378
+ * `bodySerializer` map onto `paramsSerializer` / `transformRequest`.
379
+ *
380
+ * `throwOnError` rides `validateStatus`. With it on, axios rejects a non-2xx status and the runtime
381
+ * normalizes the error to `ResponseError`. With it off, an internal `validateStatus: () => true`
382
+ * resolves every status into `{ data, error, request, response }`.
383
+ */
384
+ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = AxiosResponse>(options: ClientConfig = {}): ClientInstance<TRequest, TResponse> {
385
+ let config: ClientConfig = { ...options }
386
+ const instance = config.transport ?? axios.create()
387
+
388
+ const requestManager = instance.interceptors.request
389
+ const responseManager = instance.interceptors.response
390
+ const interceptors: Interceptors = {
391
+ request: createInterceptorChannel<InternalAxiosRequestConfig>(
392
+ (fn) => requestManager.use(fn),
393
+ (id) => requestManager.eject(id),
394
+ ),
395
+ response: createInterceptorChannel<AxiosResponse>(
396
+ (fn) => responseManager.use(fn),
397
+ (id) => responseManager.eject(id),
398
+ ),
399
+ error: createInterceptorChannel<AxiosError>(
400
+ (fn) =>
401
+ responseManager.use(undefined, async (error: unknown) => {
402
+ await fn(error as AxiosError)
403
+ return Promise.reject(error)
404
+ }),
405
+ (id) => responseManager.eject(id),
406
+ ),
407
+ }
408
+
409
+ const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
410
+ const activeInstance = requestConfig.transport ?? config.transport ?? instance
411
+ const querySerializer = requestConfig.querySerializer ?? config.querySerializer ?? defaultQuerySerializer
412
+ const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
413
+
414
+ const headers = mergeHeaders(config.headers, requestConfig.headers)
415
+ if (requestConfig.contentType && requestConfig.contentType !== 'multipart/form-data') {
416
+ headers['Content-Type'] = requestConfig.contentType
417
+ }
418
+ const contentType = headers['Content-Type'] ?? headers['content-type']
419
+
420
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
421
+
422
+ await resolveAuth({
423
+ security: requestConfig.security,
424
+ auth: requestConfig.auth ?? config.auth,
425
+ headers,
426
+ query,
427
+ })
428
+
429
+ const validatedBody = await runParser(requestConfig.parser?.request, requestConfig.body)
430
+ const pathParams = requestConfig.path ?? {}
431
+ const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
432
+ const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
433
+
434
+ const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
435
+ const validateStatus = requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? undefined : () => true)
436
+
437
+ const axiosConfig: AxiosRequestConfig = {
438
+ url,
439
+ baseURL,
440
+ method: requestConfig.method ?? 'GET',
441
+ headers,
442
+ params: query,
443
+ paramsSerializer: (params) => querySerializer(params as Record<string, unknown>),
444
+ data: validatedBody,
445
+ transformRequest: (data) => bodySerializer(data, contentType),
446
+ signal: requestConfig.signal,
447
+ responseType: requestConfig.responseType,
448
+ validateStatus,
449
+ }
450
+
451
+ try {
452
+ const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
453
+ const isSuccess = response.status >= 200 && response.status < 300
454
+ const data = isSuccess ? await runParser(requestConfig.parser?.response, response.data) : undefined
455
+ return {
456
+ status: response.status,
457
+ data,
458
+ error: isSuccess ? undefined : response.data,
459
+ request: response.config as TRequest,
460
+ response: response as TResponse,
461
+ }
462
+ } catch (error) {
463
+ const axiosError = error as AxiosError
464
+ if (throwOnError && axiosError.response) {
465
+ throw new ResponseError({
466
+ data: axiosError.response.data,
467
+ status: axiosError.response.status,
468
+ statusText: axiosError.response.statusText,
469
+ request: axiosError.config as TRequest,
470
+ response: axiosError.response as TResponse,
471
+ })
472
+ }
473
+ throw error
474
+ }
475
+ }) as ClientInstance<TRequest, TResponse>
476
+
477
+ client.getConfig = () => config
478
+ client.setConfig = (next) => {
479
+ config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
480
+ return config
481
+ }
482
+ client.interceptors = interceptors
483
+ client.createClient = (next) => createClientCore<TRequest, TResponse>({ ...config, ...next })
484
+
485
+ return client
486
+ }
487
+
488
+ export const client = createClientCore()
489
+
490
+ export const createClient = (config?: Parameters<typeof client.createClient>[0]) => client.createClient(config)