@kubb/plugin-axios 5.0.0-beta.73 → 5.0.0-beta.76

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/src/plugin.ts CHANGED
@@ -75,9 +75,7 @@ export const pluginAxios = definePlugin<PluginAxios>((options) => {
75
75
  ctx.setResolver(resolved.resolver)
76
76
  ctx.setMacros([...defaultMacros, ...(options.macros ?? [])])
77
77
 
78
- for (const gen of selectedGenerators) {
79
- ctx.addGenerator(gen)
80
- }
78
+ ctx.addGenerator(...selectedGenerators)
81
79
 
82
80
  const root = path.resolve(ctx.config.root, ctx.config.output.path)
83
81
 
@@ -86,7 +86,8 @@ export type BodySerializer = (body: unknown, contentType?: string) => unknown
86
86
 
87
87
  /**
88
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.
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).
90
91
  */
91
92
  export type Parser<T = unknown> = (value: T) => T | Promise<T>
92
93
 
@@ -136,7 +137,7 @@ export type RequestConfig<TBody = unknown, TRequest = AxiosRequestConfig, TRespo
136
137
  transport?: AxiosInstance
137
138
  querySerializer?: QuerySerializer
138
139
  bodySerializer?: BodySerializer
139
- parser?: { request?: Parser; response?: Parser }
140
+ parser?: { request?: Parser; response?: Parser; error?: Parser }
140
141
  security?: Array<Auth>
141
142
  auth?: AuthResolver
142
143
  }
@@ -214,6 +215,7 @@ export type ClientInstance<TRequest = AxiosRequestConfig, TResponse = AxiosRespo
214
215
  <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
215
216
  getConfig: () => ClientConfig
216
217
  setConfig: (config: ClientConfig) => ClientConfig
218
+ getUrl: <TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>) => string
217
219
  interceptors: Interceptors
218
220
  createClient: (config?: ClientConfig) => ClientInstance<TRequest, TResponse>
219
221
  }
@@ -253,13 +255,30 @@ function isFormBody(body: unknown): boolean {
253
255
  )
254
256
  }
255
257
 
258
+ function appendFormDataValue(formData: FormData, key: string, value: unknown): void {
259
+ if (value === undefined || value === null) return
260
+ if (value instanceof Blob) formData.append(key, value)
261
+ else if (value instanceof Date) formData.append(key, value.toISOString())
262
+ else if (typeof value === 'object') formData.append(key, JSON.stringify(value))
263
+ else formData.append(key, String(value))
264
+ }
265
+
256
266
  /**
257
267
  * Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
258
- * For `application/x-www-form-urlencoded` plain objects become `URLSearchParams`.
268
+ * For `multipart/form-data` plain objects become `FormData` and for
269
+ * `application/x-www-form-urlencoded` they become `URLSearchParams`.
259
270
  */
260
271
  export const defaultBodySerializer: BodySerializer = (body, contentType) => {
261
272
  if (body === undefined || body === null) return undefined
262
273
  if (isFormBody(body)) return body
274
+ if (contentType?.includes('multipart/form-data')) {
275
+ const formData = new FormData()
276
+ for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
277
+ if (Array.isArray(value)) for (const item of value) appendFormDataValue(formData, key, item)
278
+ else appendFormDataValue(formData, key, value)
279
+ }
280
+ return formData
281
+ }
263
282
  if (contentType?.includes('application/x-www-form-urlencoded')) {
264
283
  return new URLSearchParams(body as Record<string, string>)
265
284
  }
@@ -308,6 +327,19 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
308
327
  return Object.assign({}, ...sources.map(serializeHeaders))
309
328
  }
310
329
 
330
+ /**
331
+ * Joins the base and request URL parts, interpolates `{param}` segments from the path params
332
+ * (URL-encoded), and appends the serialized query. Backs `getUrl` so a URL can be constructed
333
+ * without sending the request.
334
+ */
335
+ function serializeUrl(parts: Array<string | undefined>, pathParams: Record<string, unknown>, search: string): string {
336
+ const path = parts
337
+ .filter(Boolean)
338
+ .join('')
339
+ .replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
340
+ return path + (search ? `?${search}` : '')
341
+ }
342
+
311
343
  /**
312
344
  * Walks the per-operation security in order and places the first resolved token on the request,
313
345
  * mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
@@ -412,10 +444,7 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
412
444
  const bodySerializer = requestConfig.bodySerializer ?? config.bodySerializer ?? defaultBodySerializer
413
445
 
414
446
  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']
447
+ const requestContentType = requestConfig.contentType ?? headers['Content-Type'] ?? headers['content-type']
419
448
 
420
449
  const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
421
450
 
@@ -427,6 +456,14 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
427
456
  })
428
457
 
429
458
  const validatedBody = await runParser(requestConfig.parser?.request, requestConfig.body)
459
+ const body = bodySerializer(validatedBody, requestContentType)
460
+ // A FormData body must keep its Content-Type unset so axios appends the multipart boundary.
461
+ if (body instanceof FormData) {
462
+ delete headers['Content-Type']
463
+ delete headers['content-type']
464
+ } else if (requestConfig.contentType) {
465
+ headers['Content-Type'] = requestConfig.contentType
466
+ }
430
467
  const pathParams = requestConfig.path ?? {}
431
468
  const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) => encodeURIComponent(String(pathParams[key] ?? '')))
432
469
  const baseURL = [config.baseURL, requestConfig.baseURL].filter(Boolean).join('') || undefined
@@ -441,8 +478,8 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
441
478
  headers,
442
479
  params: query,
443
480
  paramsSerializer: (params) => querySerializer(params as Record<string, unknown>),
444
- data: validatedBody,
445
- transformRequest: (data) => bodySerializer(data, contentType),
481
+ data: body,
482
+ transformRequest: (data) => data,
446
483
  signal: requestConfig.signal,
447
484
  responseType: requestConfig.responseType,
448
485
  validateStatus,
@@ -452,10 +489,11 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
452
489
  const response = await activeInstance.request<unknown, AxiosResponse>(axiosConfig)
453
490
  const isSuccess = response.status >= 200 && response.status < 300
454
491
  const data = isSuccess ? await runParser(requestConfig.parser?.response, response.data) : undefined
492
+ const error = isSuccess ? undefined : await runParser(requestConfig.parser?.error, response.data)
455
493
  return {
456
494
  status: response.status,
457
495
  data,
458
- error: isSuccess ? undefined : response.data,
496
+ error,
459
497
  request: response.config as TRequest,
460
498
  response: response as TResponse,
461
499
  }
@@ -479,6 +517,11 @@ export function createClientCore<TRequest = AxiosRequestConfig, TResponse = Axio
479
517
  config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } }
480
518
  return config
481
519
  }
520
+ client.getUrl = (requestConfig) => {
521
+ const querySerializer = requestConfig.querySerializer ?? config.querySerializer ?? defaultQuerySerializer
522
+ const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
523
+ return serializeUrl([config.baseURL, requestConfig.baseURL, requestConfig.url], requestConfig.path ?? {}, querySerializer(query))
524
+ }
482
525
  client.interceptors = interceptors
483
526
  client.createClient = (next) => createClientCore<TRequest, TResponse>({ ...config, ...next })
484
527