@kubb/plugin-fetch 5.0.0-beta.77 → 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 +7 -7
- package/src/generators/clientGenerator.tsx +24 -11
- package/src/plugin.ts +19 -5
- package/src/templates.ts +9 -4
- package/templates/fetch.ts +300 -174
- package/templates/serializers.ts +424 -0
- package/templates/standardSchema.ts +54 -0
package/templates/fetch.ts
CHANGED
|
@@ -1,33 +1,54 @@
|
|
|
1
|
+
import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, serializeCookies } from './serializers'
|
|
2
|
+
import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers'
|
|
3
|
+
import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema.ts'
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
* HTTP status codes treated as a success
|
|
3
|
-
* these; everything else is an error (thrown by default, or surfaced on `error`).
|
|
6
|
+
* HTTP status codes treated as a success, everything else is an error.
|
|
4
7
|
*/
|
|
5
8
|
export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
* The success members of a per-status responses record
|
|
11
|
+
* The success members of a per-status responses record.
|
|
9
12
|
*/
|
|
10
13
|
export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
|
-
* The error members of a per-status responses record
|
|
16
|
+
* The error members of a per-status responses record, every documented status that is not a 2xx.
|
|
14
17
|
*/
|
|
15
18
|
export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
|
-
* Converts a response record's string status key to its numeric literal
|
|
19
|
-
* Non-numeric keys such as the OpenAPI `default` response stay `number`.
|
|
21
|
+
* Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`.
|
|
20
22
|
*/
|
|
21
23
|
export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}` ? TNumber : number
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
* The plain body of a per-status response, unwrapping the `{ contentType; data }` union so an error result keeps the bare body union on `error`.
|
|
27
|
+
*/
|
|
28
|
+
export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The success variant for a single status, flattened so the negotiated `contentType` sits next to `data` and `switch (result.contentType)` narrows it.
|
|
32
|
+
*/
|
|
33
|
+
export type SuccessVariant<TStatus, TEntry, TRequest, TResponse> = TEntry extends { contentType: string; data: unknown }
|
|
34
|
+
? TEntry extends { contentType: infer TContentType; data: infer TData }
|
|
35
|
+
? { status: ToStatusNumber<TStatus>; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse }
|
|
36
|
+
: never
|
|
37
|
+
: { status: ToStatusNumber<TStatus>; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One result variant for a single documented status, keyed by the numeric `status` so a `switch (result.status)` narrows `data` or `error`.
|
|
27
41
|
*/
|
|
28
42
|
export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
|
|
29
|
-
?
|
|
30
|
-
: {
|
|
43
|
+
? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
|
|
44
|
+
: {
|
|
45
|
+
status: ToStatusNumber<TStatus>
|
|
46
|
+
data: undefined
|
|
47
|
+
error: DataOf<TResponses[TStatus]>
|
|
48
|
+
contentType: string | undefined
|
|
49
|
+
request: TRequest
|
|
50
|
+
response: TResponse
|
|
51
|
+
}
|
|
31
52
|
|
|
32
53
|
/**
|
|
33
54
|
* The union of every documented status' result variant.
|
|
@@ -37,61 +58,73 @@ export type ResultUnion<TResponses, TRequest, TResponse> = {
|
|
|
37
58
|
}[keyof TResponses]
|
|
38
59
|
|
|
39
60
|
/**
|
|
40
|
-
* The union of just the success (2xx) status variants
|
|
41
|
-
* untyped (`any`) error payload can never widen a success result's `data`.
|
|
61
|
+
* The union of just the success (2xx) status variants, selected by status code so an untyped error payload can never widen `data`.
|
|
42
62
|
*/
|
|
43
63
|
export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
|
|
44
64
|
[TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
|
|
45
65
|
}[Extract<keyof TResponses, SuccessStatusCode>]
|
|
46
66
|
|
|
47
67
|
/**
|
|
48
|
-
* The shape every generated function returns, discriminated by the top-level `status
|
|
49
|
-
* `throwOnError` (the default) a resolved call always means success, so the result is the union of the
|
|
50
|
-
* 2xx variants and `error` is `undefined`; without it every documented status is a variant, so a
|
|
51
|
-
* `switch (result.status)` (or narrowing on `error`) narrows `data` and `error` to that status's
|
|
52
|
-
* payload. Operations with no typed responses fall back to a `status`/`request`/`response`-only result.
|
|
68
|
+
* 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.
|
|
53
69
|
*/
|
|
54
70
|
export type RequestResult<TResponses, ThrowOnError extends boolean = true, TRequest = Request, TResponse = Response> = ThrowOnError extends true
|
|
55
71
|
? [SuccessResultUnion<TResponses, TRequest, TResponse>] extends [never]
|
|
56
|
-
? {
|
|
72
|
+
? {
|
|
73
|
+
status: number
|
|
74
|
+
data: SuccessOf<TResponses>
|
|
75
|
+
error: undefined
|
|
76
|
+
contentType: string | undefined
|
|
77
|
+
request: TRequest
|
|
78
|
+
response: TResponse
|
|
79
|
+
}
|
|
57
80
|
: SuccessResultUnion<TResponses, TRequest, TResponse>
|
|
58
81
|
: [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
|
|
59
|
-
? { status: number; data: undefined; error: undefined; request: TRequest; response: TResponse }
|
|
82
|
+
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
|
|
60
83
|
: ResultUnion<TResponses, TRequest, TResponse>
|
|
61
84
|
|
|
62
85
|
/**
|
|
63
|
-
* The data-shaped keys of the grouped options object
|
|
64
|
-
* `RequestConfig` and adds them back, typed per operation, from the generated `<Name>Request` type.
|
|
86
|
+
* The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation.
|
|
65
87
|
*/
|
|
66
|
-
export type DataShape = { body?: unknown; headers?: unknown; path?: unknown; query?: unknown }
|
|
88
|
+
export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
|
|
67
89
|
|
|
68
|
-
export type HeaderValue = string | number | boolean | null | undefined | object
|
|
69
|
-
export type HeadersInit = Array<[string, HeaderValue]> | Record<string, HeaderValue>
|
|
70
90
|
export type RequestCredentials = 'omit' | 'same-origin' | 'include'
|
|
71
91
|
export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
|
|
72
92
|
|
|
73
93
|
/**
|
|
74
|
-
*
|
|
75
|
-
* style (`form` with `explode` by default; `deepObject` for nested objects).
|
|
94
|
+
* 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.
|
|
76
95
|
*/
|
|
77
|
-
export type
|
|
96
|
+
export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
|
|
78
97
|
|
|
79
98
|
/**
|
|
80
|
-
* Serializes
|
|
81
|
-
* `ArrayBuffer`, and string bodies pass through untouched.
|
|
99
|
+
* 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.
|
|
82
100
|
*/
|
|
83
|
-
export type
|
|
101
|
+
export type ContentBodySerializer = (body: unknown, contentType?: string) => BodyInit | undefined
|
|
84
102
|
|
|
85
103
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
104
|
+
* A per-content-type codec registered on `codecs`, keyed by content type. `serialize` encodes the
|
|
105
|
+
* request body for that media type and `deserialize` decodes the response body. Either half is
|
|
106
|
+
* optional, so a codec can handle one direction.
|
|
89
107
|
*/
|
|
90
|
-
export type
|
|
108
|
+
export type Codec = {
|
|
109
|
+
serialize?: ContentBodySerializer
|
|
110
|
+
deserialize?: Deserializer
|
|
111
|
+
}
|
|
91
112
|
|
|
92
113
|
/**
|
|
93
|
-
*
|
|
94
|
-
|
|
114
|
+
* 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`.
|
|
115
|
+
*/
|
|
116
|
+
export type ContentType = string | { request?: string; response?: string }
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
|
|
120
|
+
* it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
|
|
121
|
+
* `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
|
|
122
|
+
* non-2xx call does not throw).
|
|
123
|
+
*/
|
|
124
|
+
export type Validator<T = unknown> = StandardSchemaValidator<T>
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver.
|
|
95
128
|
*/
|
|
96
129
|
export type Auth = {
|
|
97
130
|
type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
|
|
@@ -101,28 +134,22 @@ export type Auth = {
|
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
/**
|
|
104
|
-
* The token a consumer returns for a scheme, or `undefined` to skip it.
|
|
105
|
-
* prefixed by the runtime (basic is base64-encoded), so return the raw token or `user:password`.
|
|
137
|
+
* The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
|
|
106
138
|
*/
|
|
107
139
|
export type AuthToken = string | undefined
|
|
108
140
|
|
|
109
141
|
/**
|
|
110
|
-
* Resolves the token for a security scheme
|
|
111
|
-
* callback called once per scheme on a guarded operation until one returns a token.
|
|
142
|
+
* Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token.
|
|
112
143
|
*/
|
|
113
144
|
export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
|
|
114
145
|
|
|
115
146
|
/**
|
|
116
|
-
* Extra `fetch` init the transport spreads onto every `Request`, an escape hatch for the
|
|
117
|
-
* runtime does not set itself: `cache`, `mode`, `redirect`, `keepalive`, `duplex`, and Next.js's
|
|
118
|
-
* non-standard `next` (`{ revalidate, tags }`). `method`, `headers`, `body`, `signal`, and
|
|
119
|
-
* `credentials` are always controlled by the runtime and override anything set here.
|
|
147
|
+
* Extra `fetch` init the transport spreads onto every `Request`, an escape hatch for fields the runtime does not set itself such as `cache`, `redirect`, and Next.js's `next`.
|
|
120
148
|
*/
|
|
121
149
|
export type FetchOptions = RequestInit & { next?: Record<string, unknown> }
|
|
122
150
|
|
|
123
151
|
/**
|
|
124
|
-
* The request a generated function hands to the runtime
|
|
125
|
-
* from the grouped options; everything else is plain request configuration.
|
|
152
|
+
* The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
|
|
126
153
|
*/
|
|
127
154
|
export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Response> = {
|
|
128
155
|
baseURL?: string
|
|
@@ -131,19 +158,21 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
|
|
|
131
158
|
path?: Record<string, unknown>
|
|
132
159
|
query?: unknown
|
|
133
160
|
params?: unknown
|
|
161
|
+
cookies?: Record<string, unknown>
|
|
134
162
|
body?: TBody
|
|
135
163
|
headers?: HeadersInit
|
|
164
|
+
styles?: Styles
|
|
136
165
|
signal?: AbortSignal
|
|
137
166
|
credentials?: RequestCredentials
|
|
138
167
|
options?: FetchOptions
|
|
139
|
-
contentType?:
|
|
168
|
+
contentType?: ContentType
|
|
140
169
|
responseType?: ResponseType
|
|
141
170
|
throwOnError?: boolean
|
|
142
171
|
client?: ClientInstance<TRequest, TResponse>
|
|
143
172
|
transport?: Transport<TRequest, TResponse>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
serializer?: Serializers
|
|
174
|
+
codecs?: Record<string, Codec>
|
|
175
|
+
validator?: { request?: Validator; response?: Validator; error?: Validator }
|
|
147
176
|
security?: Array<Auth>
|
|
148
177
|
auth?: AuthResolver
|
|
149
178
|
}
|
|
@@ -162,8 +191,7 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
|
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
/**
|
|
165
|
-
* Client-level configuration shared by every call an instance makes
|
|
166
|
-
* overrides these.
|
|
194
|
+
* Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
|
|
167
195
|
*/
|
|
168
196
|
export type ClientConfig<TRequest = Request, TResponse = Response> = {
|
|
169
197
|
baseURL?: string
|
|
@@ -172,14 +200,13 @@ export type ClientConfig<TRequest = Request, TResponse = Response> = {
|
|
|
172
200
|
options?: FetchOptions
|
|
173
201
|
throwOnError?: boolean
|
|
174
202
|
transport?: Transport<TRequest, TResponse>
|
|
175
|
-
|
|
176
|
-
|
|
203
|
+
serializer?: Serializers
|
|
204
|
+
codecs?: Record<string, Codec>
|
|
177
205
|
auth?: AuthResolver
|
|
178
206
|
}
|
|
179
207
|
|
|
180
208
|
/**
|
|
181
|
-
* The normalized request the transport receives
|
|
182
|
-
* header work; the transport only performs the send.
|
|
209
|
+
* The normalized request the transport receives, with all serialization, auth, and header work already done.
|
|
183
210
|
*/
|
|
184
211
|
export type ResolvedRequest = {
|
|
185
212
|
url: string
|
|
@@ -193,21 +220,20 @@ export type ResolvedRequest = {
|
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
/**
|
|
196
|
-
* What a transport returns: the parsed body plus the native request
|
|
197
|
-
* so status, headers, and the raw body never have to grow the result type.
|
|
223
|
+
* What a transport returns: the parsed body plus the native request and response objects.
|
|
198
224
|
*/
|
|
199
225
|
export type TransportResult<TData = unknown, TRequest = Request, TResponse = Response> = {
|
|
200
226
|
data: TData
|
|
201
227
|
status: number
|
|
202
228
|
statusText: string
|
|
203
229
|
headers: Headers
|
|
230
|
+
contentType?: string
|
|
204
231
|
request: TRequest
|
|
205
232
|
response: TResponse
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
/**
|
|
209
|
-
* The per-plugin send
|
|
210
|
-
* plugin-ky a ky instance. Supplied to `createClientCore` as `defaultTransport`.
|
|
236
|
+
* The per-plugin send, supplied to `createClientCore` as `defaultTransport`.
|
|
211
237
|
*/
|
|
212
238
|
export type Transport<TRequest = Request, TResponse = Response> = (request: ResolvedRequest) => Promise<TransportResult<unknown, TRequest, TResponse>>
|
|
213
239
|
|
|
@@ -218,18 +244,15 @@ export type CallResult<TRequest = Request, TResponse = Response> = {
|
|
|
218
244
|
status: number
|
|
219
245
|
data: unknown
|
|
220
246
|
error: unknown
|
|
247
|
+
contentType: string | undefined
|
|
221
248
|
request: TRequest
|
|
222
249
|
response: TResponse
|
|
223
250
|
}
|
|
224
251
|
|
|
225
|
-
/**
|
|
226
|
-
* A registered interceptor with its ejection id.
|
|
227
|
-
*/
|
|
228
252
|
export type InterceptorFn<T> = (value: T) => T | Promise<T>
|
|
229
253
|
|
|
230
254
|
/**
|
|
231
|
-
* A single interceptor channel
|
|
232
|
-
* `use` / `eject` / `update` API.
|
|
255
|
+
* A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API.
|
|
233
256
|
*/
|
|
234
257
|
export type InterceptorStack<T> = {
|
|
235
258
|
use: (fn: InterceptorFn<T>) => number
|
|
@@ -261,22 +284,23 @@ export type ClientInstance<TRequest = Request, TResponse = Response> = {
|
|
|
261
284
|
}
|
|
262
285
|
|
|
263
286
|
/**
|
|
264
|
-
* Thrown for
|
|
265
|
-
* error body and the native request/response stay reachable on the error.
|
|
287
|
+
* Thrown for a non-2xx response, so a resolved call always means success.
|
|
266
288
|
*/
|
|
267
289
|
export class ResponseError<TError = unknown, TRequest = Request, TResponse = Response> extends Error {
|
|
268
290
|
data: TError
|
|
269
291
|
status: number
|
|
270
292
|
statusText: string
|
|
293
|
+
contentType: string | undefined
|
|
271
294
|
request: TRequest
|
|
272
295
|
response: TResponse
|
|
273
296
|
|
|
274
|
-
constructor(config: { data: TError; status: number; statusText: string; request: TRequest; response: TResponse }) {
|
|
297
|
+
constructor(config: { data: TError; status: number; statusText: string; contentType?: string; request: TRequest; response: TResponse }) {
|
|
275
298
|
super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
|
|
276
299
|
this.name = 'ResponseError'
|
|
277
300
|
this.data = config.data
|
|
278
301
|
this.status = config.status
|
|
279
302
|
this.statusText = config.statusText
|
|
303
|
+
this.contentType = config.contentType
|
|
280
304
|
this.request = config.request
|
|
281
305
|
this.response = config.response
|
|
282
306
|
}
|
|
@@ -284,74 +308,6 @@ export class ResponseError<TError = unknown, TRequest = Request, TResponse = Res
|
|
|
284
308
|
|
|
285
309
|
export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
|
|
286
310
|
|
|
287
|
-
function isFormBody(body: unknown): body is BodyInit {
|
|
288
|
-
return (
|
|
289
|
-
body instanceof FormData ||
|
|
290
|
-
body instanceof URLSearchParams ||
|
|
291
|
-
body instanceof Blob ||
|
|
292
|
-
body instanceof ArrayBuffer ||
|
|
293
|
-
ArrayBuffer.isView(body) ||
|
|
294
|
-
typeof body === 'string'
|
|
295
|
-
)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function appendFormDataValue(formData: FormData, key: string, value: unknown): void {
|
|
299
|
-
if (value === undefined || value === null) return
|
|
300
|
-
if (value instanceof Blob) formData.append(key, value)
|
|
301
|
-
else if (value instanceof Date) formData.append(key, value.toISOString())
|
|
302
|
-
else if (typeof value === 'object') formData.append(key, JSON.stringify(value))
|
|
303
|
-
else formData.append(key, String(value))
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
|
|
308
|
-
* For `multipart/form-data` plain objects become `FormData` and for
|
|
309
|
-
* `application/x-www-form-urlencoded` they become `URLSearchParams`.
|
|
310
|
-
*/
|
|
311
|
-
export const defaultBodySerializer: BodySerializer = (body, contentType) => {
|
|
312
|
-
if (body === undefined || body === null) return undefined
|
|
313
|
-
if (isFormBody(body)) return body as BodyInit
|
|
314
|
-
if (contentType?.includes('multipart/form-data')) {
|
|
315
|
-
const formData = new FormData()
|
|
316
|
-
for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
|
|
317
|
-
if (Array.isArray(value)) for (const item of value) appendFormDataValue(formData, key, item)
|
|
318
|
-
else appendFormDataValue(formData, key, value)
|
|
319
|
-
}
|
|
320
|
-
return formData
|
|
321
|
-
}
|
|
322
|
-
if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
323
|
-
return new URLSearchParams(body as Record<string, string>)
|
|
324
|
-
}
|
|
325
|
-
return JSON.stringify(body)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function appendQueryValue(search: URLSearchParams, key: string, value: unknown): void {
|
|
329
|
-
if (value === undefined || value === null) return
|
|
330
|
-
if (Array.isArray(value)) {
|
|
331
|
-
for (const item of value) appendQueryValue(search, key, item)
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
if (typeof value === 'object') {
|
|
335
|
-
for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
|
|
336
|
-
appendQueryValue(search, `${key}[${prop}]`, propValue)
|
|
337
|
-
}
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
search.append(key, String(value))
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Default query serializer: arrays explode into repeated keys and nested objects use the
|
|
345
|
-
* `deepObject` style (`key[prop]=value`).
|
|
346
|
-
*/
|
|
347
|
-
export const defaultQuerySerializer: QuerySerializer = (params) => {
|
|
348
|
-
const search = new URLSearchParams()
|
|
349
|
-
for (const [key, value] of Object.entries(params)) {
|
|
350
|
-
appendQueryValue(search, key, value)
|
|
351
|
-
}
|
|
352
|
-
return search.toString()
|
|
353
|
-
}
|
|
354
|
-
|
|
355
311
|
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
356
312
|
if (!headers) return {}
|
|
357
313
|
const entries = Array.isArray(headers) ? headers : Object.entries(headers)
|
|
@@ -368,21 +324,30 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
|
|
|
368
324
|
}
|
|
369
325
|
|
|
370
326
|
/**
|
|
371
|
-
* Joins the
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
327
|
+
* Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, shared by the send path and `getUrl`.
|
|
328
|
+
*/
|
|
329
|
+
function serializeUrl({
|
|
330
|
+
parts,
|
|
331
|
+
pathParams,
|
|
332
|
+
search,
|
|
333
|
+
pathSerializer = defaultPathSerializer,
|
|
334
|
+
pathStyles,
|
|
335
|
+
}: {
|
|
336
|
+
parts: Array<string | undefined>
|
|
337
|
+
pathParams: Record<string, unknown>
|
|
338
|
+
search: string
|
|
339
|
+
pathSerializer?: PathSerializer
|
|
340
|
+
pathStyles?: Record<string, PathParamStyle>
|
|
341
|
+
}): string {
|
|
376
342
|
const path = parts
|
|
377
343
|
.filter(Boolean)
|
|
378
344
|
.join('')
|
|
379
|
-
.replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
345
|
+
.replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
|
|
380
346
|
return path + (search ? `?${search}` : '')
|
|
381
347
|
}
|
|
382
348
|
|
|
383
349
|
/**
|
|
384
|
-
* Creates a transport-agnostic interceptor channel
|
|
385
|
-
* removes one by id and `update` swaps its function in place without reordering.
|
|
350
|
+
* Creates a transport-agnostic interceptor channel that runs interceptors in registration order.
|
|
386
351
|
*/
|
|
387
352
|
export function createInterceptorStack<T>(): InterceptorStack<T> {
|
|
388
353
|
let entries: Array<{ id: number; fn: InterceptorFn<T> }> = []
|
|
@@ -411,10 +376,7 @@ export function createInterceptorStack<T>(): InterceptorStack<T> {
|
|
|
411
376
|
}
|
|
412
377
|
|
|
413
378
|
/**
|
|
414
|
-
* Walks the per-operation security in order and places the first resolved token on the request,
|
|
415
|
-
* mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
|
|
416
|
-
* Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
|
|
417
|
-
* `name` in the header, query, or cookie.
|
|
379
|
+
* Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place.
|
|
418
380
|
*/
|
|
419
381
|
export async function resolveAuth(params: {
|
|
420
382
|
security: Array<Auth> | undefined
|
|
@@ -441,14 +403,38 @@ export async function resolveAuth(params: {
|
|
|
441
403
|
}
|
|
442
404
|
}
|
|
443
405
|
|
|
444
|
-
async function
|
|
445
|
-
if (!
|
|
446
|
-
return (
|
|
406
|
+
async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
|
|
407
|
+
if (!validator) return value
|
|
408
|
+
return validateStandardSchema(validator, value)
|
|
447
409
|
}
|
|
448
410
|
|
|
449
411
|
/**
|
|
450
|
-
*
|
|
451
|
-
|
|
412
|
+
* The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters.
|
|
413
|
+
*/
|
|
414
|
+
function baseContentType(value: string | null | undefined): string | undefined {
|
|
415
|
+
if (!value) return undefined
|
|
416
|
+
return value.split(';')[0]!.trim().toLowerCase() || undefined
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Reads the negotiated response content type from the response headers as a base media type.
|
|
421
|
+
*/
|
|
422
|
+
function getResponseContentType(headers: Headers | Record<string, string> | undefined): string | undefined {
|
|
423
|
+
if (!headers) return undefined
|
|
424
|
+
const value = headers instanceof Headers ? headers.get('Content-Type') : (headers['Content-Type'] ?? headers['content-type'])
|
|
425
|
+
return baseContentType(value)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Normalizes the `contentType` option to its `{ request, response }` form, treating a bare string as the request content type.
|
|
430
|
+
*/
|
|
431
|
+
function resolveContentType(contentType: ContentType | undefined): { request?: string; response?: string } {
|
|
432
|
+
if (typeof contentType === 'string') return { request: contentType }
|
|
433
|
+
return contentType ?? {}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Builds the shared client core bound to a transport, exported by each plugin as `client` plus a `createClient` factory.
|
|
452
438
|
*/
|
|
453
439
|
export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
454
440
|
options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
|
|
@@ -464,11 +450,17 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
464
450
|
|
|
465
451
|
const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
|
|
466
452
|
const transport = requestConfig.transport ?? config.transport ?? defaultTransport
|
|
467
|
-
const querySerializer = requestConfig.
|
|
468
|
-
const bodySerializer = requestConfig.
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
453
|
+
const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
|
|
454
|
+
const bodySerializer = requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer
|
|
455
|
+
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
456
|
+
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
457
|
+
|
|
458
|
+
const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header))
|
|
459
|
+
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
460
|
+
const requestContentType = requestContentTypeOption ?? headers['Content-Type'] ?? headers['content-type']
|
|
461
|
+
if (responseContentType && headers['Accept'] === undefined && headers['accept'] === undefined) {
|
|
462
|
+
headers['Accept'] = responseContentType
|
|
463
|
+
}
|
|
472
464
|
|
|
473
465
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
474
466
|
|
|
@@ -479,17 +471,32 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
479
471
|
query,
|
|
480
472
|
})
|
|
481
473
|
|
|
474
|
+
if (requestConfig.cookies) {
|
|
475
|
+
const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie)
|
|
476
|
+
if (cookie) headers.Cookie = [headers.Cookie, cookie].filter(Boolean).join('; ')
|
|
477
|
+
}
|
|
478
|
+
|
|
482
479
|
const rawBody = requestConfig.body
|
|
483
|
-
const validatedBody = await
|
|
484
|
-
const
|
|
480
|
+
const validatedBody = await runValidator(requestConfig.validator?.request, rawBody)
|
|
481
|
+
const requestContentTypeBase = baseContentType(requestContentType)
|
|
482
|
+
const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
|
|
483
|
+
const body = contentCodec?.serialize
|
|
484
|
+
? contentCodec.serialize(validatedBody, requestContentType)
|
|
485
|
+
: bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body })
|
|
485
486
|
// A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary.
|
|
486
487
|
if (body instanceof FormData) {
|
|
487
488
|
delete headers['Content-Type']
|
|
488
489
|
delete headers['content-type']
|
|
489
|
-
} else if (
|
|
490
|
-
headers['Content-Type'] =
|
|
490
|
+
} else if (requestContentTypeOption) {
|
|
491
|
+
headers['Content-Type'] = requestContentTypeOption
|
|
491
492
|
}
|
|
492
|
-
const url = serializeUrl(
|
|
493
|
+
const url = serializeUrl({
|
|
494
|
+
parts: [config.baseURL, requestConfig.baseURL, requestConfig.url],
|
|
495
|
+
pathParams: requestConfig.path ?? {},
|
|
496
|
+
search: querySerializer(query, requestConfig.styles?.query),
|
|
497
|
+
pathSerializer,
|
|
498
|
+
pathStyles: requestConfig.styles?.path,
|
|
499
|
+
})
|
|
493
500
|
|
|
494
501
|
const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
|
|
495
502
|
|
|
@@ -511,11 +518,21 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
511
518
|
const isSuccess = result.status >= 200 && result.status < 300
|
|
512
519
|
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
513
520
|
|
|
521
|
+
const contentType = result.contentType ?? getResponseContentType(result.headers)
|
|
522
|
+
let decoded = result.data
|
|
523
|
+
if (contentType) {
|
|
524
|
+
const codec = codecs[contentType]
|
|
525
|
+
if (codec?.deserialize) decoded = await codec.deserialize(result.data, contentType)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const parsedErrorData = !isSuccess ? await runValidator(requestConfig.validator?.error, decoded) : undefined
|
|
529
|
+
|
|
514
530
|
if (!isSuccess && throwOnError) {
|
|
515
531
|
const error = new ResponseError({
|
|
516
|
-
data:
|
|
532
|
+
data: parsedErrorData,
|
|
517
533
|
status: result.status,
|
|
518
534
|
statusText: result.statusText,
|
|
535
|
+
contentType,
|
|
519
536
|
request: result.request,
|
|
520
537
|
response: result.response,
|
|
521
538
|
})
|
|
@@ -523,13 +540,14 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
523
540
|
throw error
|
|
524
541
|
}
|
|
525
542
|
|
|
526
|
-
const data = isSuccess ? await
|
|
527
|
-
const error = isSuccess ? undefined :
|
|
543
|
+
const data = isSuccess ? await runValidator(requestConfig.validator?.response, decoded) : undefined
|
|
544
|
+
const error = isSuccess ? undefined : parsedErrorData
|
|
528
545
|
|
|
529
546
|
return {
|
|
530
547
|
status: result.status,
|
|
531
548
|
data,
|
|
532
549
|
error,
|
|
550
|
+
contentType,
|
|
533
551
|
request: result.request,
|
|
534
552
|
response: result.response,
|
|
535
553
|
}
|
|
@@ -541,9 +559,16 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
541
559
|
return config
|
|
542
560
|
}
|
|
543
561
|
client.getUrl = (requestConfig) => {
|
|
544
|
-
const querySerializer = requestConfig.
|
|
562
|
+
const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
|
|
563
|
+
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
545
564
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
546
|
-
return serializeUrl(
|
|
565
|
+
return serializeUrl({
|
|
566
|
+
parts: [config.baseURL, requestConfig.baseURL, requestConfig.url],
|
|
567
|
+
pathParams: requestConfig.path ?? {},
|
|
568
|
+
search: querySerializer(query, requestConfig.styles?.query),
|
|
569
|
+
pathSerializer,
|
|
570
|
+
pathStyles: requestConfig.styles?.path,
|
|
571
|
+
})
|
|
547
572
|
}
|
|
548
573
|
client.interceptors = interceptors
|
|
549
574
|
client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next })
|
|
@@ -556,6 +581,7 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
556
581
|
*/
|
|
557
582
|
function detectResponseType(contentType: string | null): ResponseType | undefined {
|
|
558
583
|
if (!contentType) return undefined
|
|
584
|
+
if (contentType.includes('text/event-stream')) return 'stream'
|
|
559
585
|
if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
|
|
560
586
|
if (contentType.includes('text/')) return 'text'
|
|
561
587
|
if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
|
|
@@ -563,9 +589,7 @@ function detectResponseType(contentType: string | null): ResponseType | undefine
|
|
|
563
589
|
}
|
|
564
590
|
|
|
565
591
|
/**
|
|
566
|
-
* Parses a `fetch` response body
|
|
567
|
-
* An explicit `responseType`, or one detected from the `Content-Type` header, forces the matching
|
|
568
|
-
* `Response` method; otherwise the body is read as text and `JSON.parse`d, falling back to raw text.
|
|
592
|
+
* Parses a `fetch` response body using the `responseType` (explicit or detected from the `Content-Type`), falling back to JSON-then-text.
|
|
569
593
|
*/
|
|
570
594
|
async function parseResponse(response: Response, responseType?: ResponseType): Promise<unknown> {
|
|
571
595
|
if (response.status === 204 || response.status === 205 || response.status === 304 || !response.body) {
|
|
@@ -599,8 +623,109 @@ async function parseResponse(response: Response, responseType?: ResponseType): P
|
|
|
599
623
|
}
|
|
600
624
|
|
|
601
625
|
/**
|
|
602
|
-
*
|
|
603
|
-
|
|
626
|
+
* One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise.
|
|
627
|
+
*/
|
|
628
|
+
export type ServerSentEvent<TData = unknown> = {
|
|
629
|
+
data: TData
|
|
630
|
+
event?: string
|
|
631
|
+
id?: string
|
|
632
|
+
retry?: number
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function* readBytes(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<Uint8Array> {
|
|
636
|
+
if (!('getReader' in stream)) {
|
|
637
|
+
yield* stream
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const reader = stream.getReader()
|
|
642
|
+
try {
|
|
643
|
+
while (true) {
|
|
644
|
+
const { done, value } = await reader.read()
|
|
645
|
+
if (done) return
|
|
646
|
+
yield value
|
|
647
|
+
}
|
|
648
|
+
} finally {
|
|
649
|
+
await reader.cancel().catch(() => {})
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function parseEvent<TData>(raw: string): ServerSentEvent<TData> | undefined {
|
|
654
|
+
const data: Array<string> = []
|
|
655
|
+
const event: ServerSentEvent<TData> = { data: undefined as TData }
|
|
656
|
+
let seen = false
|
|
657
|
+
|
|
658
|
+
for (const line of raw.split('\n')) {
|
|
659
|
+
if (!line || line.startsWith(':')) continue
|
|
660
|
+
seen = true
|
|
661
|
+
const index = line.indexOf(':')
|
|
662
|
+
const field = index === -1 ? line : line.slice(0, index)
|
|
663
|
+
const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '')
|
|
664
|
+
if (field === 'data') data.push(value)
|
|
665
|
+
else if (field === 'event') event.event = value
|
|
666
|
+
else if (field === 'id') event.id = value
|
|
667
|
+
else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!seen) return undefined
|
|
671
|
+
|
|
672
|
+
if (data.length) {
|
|
673
|
+
const joined = data.join('\n')
|
|
674
|
+
try {
|
|
675
|
+
event.data = JSON.parse(joined) as TData
|
|
676
|
+
} catch {
|
|
677
|
+
event.data = joined as TData
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return event
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop.
|
|
685
|
+
*/
|
|
686
|
+
export async function* parseEventStream<TData = unknown>(
|
|
687
|
+
stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
|
|
688
|
+
): AsyncGenerator<ServerSentEvent<TData>> {
|
|
689
|
+
const decoder = new TextDecoder()
|
|
690
|
+
const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n')
|
|
691
|
+
let buffer = ''
|
|
692
|
+
|
|
693
|
+
for await (const chunk of readBytes(stream)) {
|
|
694
|
+
const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n')
|
|
695
|
+
buffer = blocks.pop() ?? ''
|
|
696
|
+
for (const block of blocks) {
|
|
697
|
+
const event = parseEvent<TData>(block)
|
|
698
|
+
if (event) yield event
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const event = parseEvent<TData>(normalize(buffer + decoder.decode()))
|
|
703
|
+
if (event) yield event
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* The resolved shape returned by a generated `text/event-stream` operation: the typed event
|
|
708
|
+
* `stream` plus the native `response`.
|
|
709
|
+
*/
|
|
710
|
+
export type EventStreamResult<TData = unknown, TResponse = Response> = {
|
|
711
|
+
stream: AsyncGenerator<ServerSentEvent<TData>>
|
|
712
|
+
response: TResponse
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing
|
|
717
|
+
* the parsed events as a typed async iterator. Generated SSE operations call this.
|
|
718
|
+
*/
|
|
719
|
+
export async function toEventStream<TData = unknown>(result: Promise<{ data: unknown; response: Response }>): Promise<EventStreamResult<TData>> {
|
|
720
|
+
const { data, response } = await result
|
|
721
|
+
return {
|
|
722
|
+
response,
|
|
723
|
+
stream: parseEventStream<TData>(data as ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>),
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* The default transport that sends the resolved request through `globalThis.fetch` and returns the parsed body with the native request and response.
|
|
604
729
|
*/
|
|
605
730
|
const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
|
|
606
731
|
const init: RequestInit = {
|
|
@@ -621,6 +746,7 @@ const defaultTransport: Transport = async (request: ResolvedRequest): Promise<Tr
|
|
|
621
746
|
status: response.status,
|
|
622
747
|
statusText: response.statusText,
|
|
623
748
|
headers: response.headers,
|
|
749
|
+
contentType: getResponseContentType(response.headers),
|
|
624
750
|
request: nativeRequest,
|
|
625
751
|
response,
|
|
626
752
|
}
|