@kubb/plugin-fetch 5.0.0-beta.76 → 5.0.0-beta.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +242 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +242 -87
- package/dist/index.js.map +1 -1
- package/package.json +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 +311 -170
- 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
|
-
|
|
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.
|
|
95
|
+
*/
|
|
96
|
+
export type Deserializer<T = unknown> = (raw: unknown, contentType: string) => T | Promise<T>
|
|
97
|
+
|
|
98
|
+
/**
|
|
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.
|
|
100
|
+
*/
|
|
101
|
+
export type ContentBodySerializer = (body: unknown, contentType?: string) => BodyInit | undefined
|
|
102
|
+
|
|
103
|
+
/**
|
|
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.
|
|
76
107
|
*/
|
|
77
|
-
export type
|
|
108
|
+
export type Codec = {
|
|
109
|
+
serialize?: ContentBodySerializer
|
|
110
|
+
deserialize?: Deserializer
|
|
111
|
+
}
|
|
78
112
|
|
|
79
113
|
/**
|
|
80
|
-
*
|
|
81
|
-
* `ArrayBuffer`, and string bodies pass through untouched.
|
|
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`.
|
|
82
115
|
*/
|
|
83
|
-
export type
|
|
116
|
+
export type ContentType = string | { request?: string; response?: string }
|
|
84
117
|
|
|
85
118
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `
|
|
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).
|
|
89
123
|
*/
|
|
90
|
-
export type
|
|
124
|
+
export type Validator<T = unknown> = StandardSchemaValidator<T>
|
|
91
125
|
|
|
92
126
|
/**
|
|
93
|
-
* A resolved security scheme carried on each generated call's `security` array
|
|
94
|
-
* to the configured `auth` resolver and places the returned token accordingly.
|
|
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,20 +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
|
-
*
|
|
117
|
-
|
|
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`.
|
|
148
|
+
*/
|
|
149
|
+
export type FetchOptions = RequestInit & { next?: Record<string, unknown> }
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options.
|
|
118
153
|
*/
|
|
119
154
|
export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Response> = {
|
|
120
155
|
baseURL?: string
|
|
@@ -123,18 +158,21 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
|
|
|
123
158
|
path?: Record<string, unknown>
|
|
124
159
|
query?: unknown
|
|
125
160
|
params?: unknown
|
|
161
|
+
cookies?: Record<string, unknown>
|
|
126
162
|
body?: TBody
|
|
127
163
|
headers?: HeadersInit
|
|
164
|
+
styles?: Styles
|
|
128
165
|
signal?: AbortSignal
|
|
129
166
|
credentials?: RequestCredentials
|
|
130
|
-
|
|
167
|
+
options?: FetchOptions
|
|
168
|
+
contentType?: ContentType
|
|
131
169
|
responseType?: ResponseType
|
|
132
170
|
throwOnError?: boolean
|
|
133
171
|
client?: ClientInstance<TRequest, TResponse>
|
|
134
172
|
transport?: Transport<TRequest, TResponse>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
173
|
+
serializer?: Serializers
|
|
174
|
+
codecs?: Record<string, Codec>
|
|
175
|
+
validator?: { request?: Validator; response?: Validator; error?: Validator }
|
|
138
176
|
security?: Array<Auth>
|
|
139
177
|
auth?: AuthResolver
|
|
140
178
|
}
|
|
@@ -153,23 +191,22 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
|
|
|
153
191
|
}
|
|
154
192
|
|
|
155
193
|
/**
|
|
156
|
-
* Client-level configuration shared by every call an instance makes
|
|
157
|
-
* overrides these.
|
|
194
|
+
* Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`.
|
|
158
195
|
*/
|
|
159
196
|
export type ClientConfig<TRequest = Request, TResponse = Response> = {
|
|
160
197
|
baseURL?: string
|
|
161
198
|
headers?: HeadersInit
|
|
162
199
|
credentials?: RequestCredentials
|
|
200
|
+
options?: FetchOptions
|
|
163
201
|
throwOnError?: boolean
|
|
164
202
|
transport?: Transport<TRequest, TResponse>
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
serializer?: Serializers
|
|
204
|
+
codecs?: Record<string, Codec>
|
|
167
205
|
auth?: AuthResolver
|
|
168
206
|
}
|
|
169
207
|
|
|
170
208
|
/**
|
|
171
|
-
* The normalized request the transport receives
|
|
172
|
-
* header work; the transport only performs the send.
|
|
209
|
+
* The normalized request the transport receives, with all serialization, auth, and header work already done.
|
|
173
210
|
*/
|
|
174
211
|
export type ResolvedRequest = {
|
|
175
212
|
url: string
|
|
@@ -178,25 +215,25 @@ export type ResolvedRequest = {
|
|
|
178
215
|
body?: BodyInit
|
|
179
216
|
signal?: AbortSignal
|
|
180
217
|
credentials?: RequestCredentials
|
|
218
|
+
options?: FetchOptions
|
|
181
219
|
responseType?: ResponseType
|
|
182
220
|
}
|
|
183
221
|
|
|
184
222
|
/**
|
|
185
|
-
* What a transport returns: the parsed body plus the native request
|
|
186
|
-
* 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.
|
|
187
224
|
*/
|
|
188
225
|
export type TransportResult<TData = unknown, TRequest = Request, TResponse = Response> = {
|
|
189
226
|
data: TData
|
|
190
227
|
status: number
|
|
191
228
|
statusText: string
|
|
192
229
|
headers: Headers
|
|
230
|
+
contentType?: string
|
|
193
231
|
request: TRequest
|
|
194
232
|
response: TResponse
|
|
195
233
|
}
|
|
196
234
|
|
|
197
235
|
/**
|
|
198
|
-
* The per-plugin send
|
|
199
|
-
* plugin-ky a ky instance. Supplied to `createClientCore` as `defaultTransport`.
|
|
236
|
+
* The per-plugin send, supplied to `createClientCore` as `defaultTransport`.
|
|
200
237
|
*/
|
|
201
238
|
export type Transport<TRequest = Request, TResponse = Response> = (request: ResolvedRequest) => Promise<TransportResult<unknown, TRequest, TResponse>>
|
|
202
239
|
|
|
@@ -207,18 +244,15 @@ export type CallResult<TRequest = Request, TResponse = Response> = {
|
|
|
207
244
|
status: number
|
|
208
245
|
data: unknown
|
|
209
246
|
error: unknown
|
|
247
|
+
contentType: string | undefined
|
|
210
248
|
request: TRequest
|
|
211
249
|
response: TResponse
|
|
212
250
|
}
|
|
213
251
|
|
|
214
|
-
/**
|
|
215
|
-
* A registered interceptor with its ejection id.
|
|
216
|
-
*/
|
|
217
252
|
export type InterceptorFn<T> = (value: T) => T | Promise<T>
|
|
218
253
|
|
|
219
254
|
/**
|
|
220
|
-
* A single interceptor channel
|
|
221
|
-
* `use` / `eject` / `update` API.
|
|
255
|
+
* A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API.
|
|
222
256
|
*/
|
|
223
257
|
export type InterceptorStack<T> = {
|
|
224
258
|
use: (fn: InterceptorFn<T>) => number
|
|
@@ -250,22 +284,23 @@ export type ClientInstance<TRequest = Request, TResponse = Response> = {
|
|
|
250
284
|
}
|
|
251
285
|
|
|
252
286
|
/**
|
|
253
|
-
* Thrown for
|
|
254
|
-
* 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.
|
|
255
288
|
*/
|
|
256
289
|
export class ResponseError<TError = unknown, TRequest = Request, TResponse = Response> extends Error {
|
|
257
290
|
data: TError
|
|
258
291
|
status: number
|
|
259
292
|
statusText: string
|
|
293
|
+
contentType: string | undefined
|
|
260
294
|
request: TRequest
|
|
261
295
|
response: TResponse
|
|
262
296
|
|
|
263
|
-
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 }) {
|
|
264
298
|
super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`)
|
|
265
299
|
this.name = 'ResponseError'
|
|
266
300
|
this.data = config.data
|
|
267
301
|
this.status = config.status
|
|
268
302
|
this.statusText = config.statusText
|
|
303
|
+
this.contentType = config.contentType
|
|
269
304
|
this.request = config.request
|
|
270
305
|
this.response = config.response
|
|
271
306
|
}
|
|
@@ -273,74 +308,6 @@ export class ResponseError<TError = unknown, TRequest = Request, TResponse = Res
|
|
|
273
308
|
|
|
274
309
|
export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
|
|
275
310
|
|
|
276
|
-
function isFormBody(body: unknown): body is BodyInit {
|
|
277
|
-
return (
|
|
278
|
-
body instanceof FormData ||
|
|
279
|
-
body instanceof URLSearchParams ||
|
|
280
|
-
body instanceof Blob ||
|
|
281
|
-
body instanceof ArrayBuffer ||
|
|
282
|
-
ArrayBuffer.isView(body) ||
|
|
283
|
-
typeof body === 'string'
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function appendFormDataValue(formData: FormData, key: string, value: unknown): void {
|
|
288
|
-
if (value === undefined || value === null) return
|
|
289
|
-
if (value instanceof Blob) formData.append(key, value)
|
|
290
|
-
else if (value instanceof Date) formData.append(key, value.toISOString())
|
|
291
|
-
else if (typeof value === 'object') formData.append(key, JSON.stringify(value))
|
|
292
|
-
else formData.append(key, String(value))
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Default body serializer: passes binary/form bodies through and JSON-serializes everything else.
|
|
297
|
-
* For `multipart/form-data` plain objects become `FormData` and for
|
|
298
|
-
* `application/x-www-form-urlencoded` they become `URLSearchParams`.
|
|
299
|
-
*/
|
|
300
|
-
export const defaultBodySerializer: BodySerializer = (body, contentType) => {
|
|
301
|
-
if (body === undefined || body === null) return undefined
|
|
302
|
-
if (isFormBody(body)) return body as BodyInit
|
|
303
|
-
if (contentType?.includes('multipart/form-data')) {
|
|
304
|
-
const formData = new FormData()
|
|
305
|
-
for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
|
|
306
|
-
if (Array.isArray(value)) for (const item of value) appendFormDataValue(formData, key, item)
|
|
307
|
-
else appendFormDataValue(formData, key, value)
|
|
308
|
-
}
|
|
309
|
-
return formData
|
|
310
|
-
}
|
|
311
|
-
if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
312
|
-
return new URLSearchParams(body as Record<string, string>)
|
|
313
|
-
}
|
|
314
|
-
return JSON.stringify(body)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function appendQueryValue(search: URLSearchParams, key: string, value: unknown): void {
|
|
318
|
-
if (value === undefined || value === null) return
|
|
319
|
-
if (Array.isArray(value)) {
|
|
320
|
-
for (const item of value) appendQueryValue(search, key, item)
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
if (typeof value === 'object') {
|
|
324
|
-
for (const [prop, propValue] of Object.entries(value as Record<string, unknown>)) {
|
|
325
|
-
appendQueryValue(search, `${key}[${prop}]`, propValue)
|
|
326
|
-
}
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
search.append(key, String(value))
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Default query serializer: arrays explode into repeated keys and nested objects use the
|
|
334
|
-
* `deepObject` style (`key[prop]=value`).
|
|
335
|
-
*/
|
|
336
|
-
export const defaultQuerySerializer: QuerySerializer = (params) => {
|
|
337
|
-
const search = new URLSearchParams()
|
|
338
|
-
for (const [key, value] of Object.entries(params)) {
|
|
339
|
-
appendQueryValue(search, key, value)
|
|
340
|
-
}
|
|
341
|
-
return search.toString()
|
|
342
|
-
}
|
|
343
|
-
|
|
344
311
|
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
345
312
|
if (!headers) return {}
|
|
346
313
|
const entries = Array.isArray(headers) ? headers : Object.entries(headers)
|
|
@@ -357,21 +324,30 @@ function mergeHeaders(...sources: Array<HeadersInit | undefined>): Record<string
|
|
|
357
324
|
}
|
|
358
325
|
|
|
359
326
|
/**
|
|
360
|
-
* Joins the
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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 {
|
|
365
342
|
const path = parts
|
|
366
343
|
.filter(Boolean)
|
|
367
344
|
.join('')
|
|
368
|
-
.replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
345
|
+
.replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] }))
|
|
369
346
|
return path + (search ? `?${search}` : '')
|
|
370
347
|
}
|
|
371
348
|
|
|
372
349
|
/**
|
|
373
|
-
* Creates a transport-agnostic interceptor channel
|
|
374
|
-
* 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.
|
|
375
351
|
*/
|
|
376
352
|
export function createInterceptorStack<T>(): InterceptorStack<T> {
|
|
377
353
|
let entries: Array<{ id: number; fn: InterceptorFn<T> }> = []
|
|
@@ -400,10 +376,7 @@ export function createInterceptorStack<T>(): InterceptorStack<T> {
|
|
|
400
376
|
}
|
|
401
377
|
|
|
402
378
|
/**
|
|
403
|
-
* Walks the per-operation security in order and places the first resolved token on the request,
|
|
404
|
-
* mutating `headers` / `query` in place. Bearer (and oauth2 / openIdConnect) tokens become a `Bearer`
|
|
405
|
-
* Authorization header, basic credentials are base64-encoded, and an apiKey is placed under its
|
|
406
|
-
* `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.
|
|
407
380
|
*/
|
|
408
381
|
export async function resolveAuth(params: {
|
|
409
382
|
security: Array<Auth> | undefined
|
|
@@ -430,14 +403,38 @@ export async function resolveAuth(params: {
|
|
|
430
403
|
}
|
|
431
404
|
}
|
|
432
405
|
|
|
433
|
-
async function
|
|
434
|
-
if (!
|
|
435
|
-
return (
|
|
406
|
+
async function runValidator<T>(validator: Validator<T> | undefined, value: T): Promise<T> {
|
|
407
|
+
if (!validator) return value
|
|
408
|
+
return validateStandardSchema(validator, value)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
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)
|
|
436
426
|
}
|
|
437
427
|
|
|
438
428
|
/**
|
|
439
|
-
*
|
|
440
|
-
|
|
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.
|
|
441
438
|
*/
|
|
442
439
|
export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
443
440
|
options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
|
|
@@ -453,11 +450,17 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
453
450
|
|
|
454
451
|
const client = (async <TBody = unknown>(requestConfig: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>> => {
|
|
455
452
|
const transport = requestConfig.transport ?? config.transport ?? defaultTransport
|
|
456
|
-
const querySerializer = requestConfig.
|
|
457
|
-
const bodySerializer = requestConfig.
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
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
|
+
}
|
|
461
464
|
|
|
462
465
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
463
466
|
|
|
@@ -468,17 +471,34 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
468
471
|
query,
|
|
469
472
|
})
|
|
470
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
|
+
|
|
471
479
|
const rawBody = requestConfig.body
|
|
472
|
-
const validatedBody = await
|
|
473
|
-
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 })
|
|
474
486
|
// A FormData body must keep its Content-Type unset so the runtime appends the multipart boundary.
|
|
475
487
|
if (body instanceof FormData) {
|
|
476
488
|
delete headers['Content-Type']
|
|
477
489
|
delete headers['content-type']
|
|
478
|
-
} else if (
|
|
479
|
-
headers['Content-Type'] =
|
|
490
|
+
} else if (requestContentTypeOption) {
|
|
491
|
+
headers['Content-Type'] = requestContentTypeOption
|
|
480
492
|
}
|
|
481
|
-
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
|
+
})
|
|
500
|
+
|
|
501
|
+
const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
|
|
482
502
|
|
|
483
503
|
let resolvedRequest: ResolvedRequest = {
|
|
484
504
|
url,
|
|
@@ -487,6 +507,7 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
487
507
|
body,
|
|
488
508
|
signal: requestConfig.signal,
|
|
489
509
|
credentials: requestConfig.credentials,
|
|
510
|
+
options,
|
|
490
511
|
responseType: requestConfig.responseType,
|
|
491
512
|
}
|
|
492
513
|
resolvedRequest = await interceptors.request.run(resolvedRequest)
|
|
@@ -497,11 +518,21 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
497
518
|
const isSuccess = result.status >= 200 && result.status < 300
|
|
498
519
|
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
499
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
|
+
|
|
500
530
|
if (!isSuccess && throwOnError) {
|
|
501
531
|
const error = new ResponseError({
|
|
502
|
-
data:
|
|
532
|
+
data: parsedErrorData,
|
|
503
533
|
status: result.status,
|
|
504
534
|
statusText: result.statusText,
|
|
535
|
+
contentType,
|
|
505
536
|
request: result.request,
|
|
506
537
|
response: result.response,
|
|
507
538
|
})
|
|
@@ -509,13 +540,14 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
509
540
|
throw error
|
|
510
541
|
}
|
|
511
542
|
|
|
512
|
-
const data = isSuccess ? await
|
|
513
|
-
const error = isSuccess ? undefined :
|
|
543
|
+
const data = isSuccess ? await runValidator(requestConfig.validator?.response, decoded) : undefined
|
|
544
|
+
const error = isSuccess ? undefined : parsedErrorData
|
|
514
545
|
|
|
515
546
|
return {
|
|
516
547
|
status: result.status,
|
|
517
548
|
data,
|
|
518
549
|
error,
|
|
550
|
+
contentType,
|
|
519
551
|
request: result.request,
|
|
520
552
|
response: result.response,
|
|
521
553
|
}
|
|
@@ -527,9 +559,16 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
527
559
|
return config
|
|
528
560
|
}
|
|
529
561
|
client.getUrl = (requestConfig) => {
|
|
530
|
-
const querySerializer = requestConfig.
|
|
562
|
+
const querySerializer = requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer
|
|
563
|
+
const pathSerializer = requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer
|
|
531
564
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
532
|
-
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
|
+
})
|
|
533
572
|
}
|
|
534
573
|
client.interceptors = interceptors
|
|
535
574
|
client.createClient = (next) => createClientCore({ defaultTransport, ...config, ...next })
|
|
@@ -542,6 +581,7 @@ export function createClientCore<TRequest = Request, TResponse = Response>(
|
|
|
542
581
|
*/
|
|
543
582
|
function detectResponseType(contentType: string | null): ResponseType | undefined {
|
|
544
583
|
if (!contentType) return undefined
|
|
584
|
+
if (contentType.includes('text/event-stream')) return 'stream'
|
|
545
585
|
if (contentType.includes('application/json') || contentType.includes('text/json')) return 'json'
|
|
546
586
|
if (contentType.includes('text/')) return 'text'
|
|
547
587
|
if (contentType.includes('image/') || contentType.includes('application/octet-stream')) return 'blob'
|
|
@@ -549,9 +589,7 @@ function detectResponseType(contentType: string | null): ResponseType | undefine
|
|
|
549
589
|
}
|
|
550
590
|
|
|
551
591
|
/**
|
|
552
|
-
* Parses a `fetch` response body
|
|
553
|
-
* An explicit `responseType`, or one detected from the `Content-Type` header, forces the matching
|
|
554
|
-
* `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.
|
|
555
593
|
*/
|
|
556
594
|
async function parseResponse(response: Response, responseType?: ResponseType): Promise<unknown> {
|
|
557
595
|
if (response.status === 204 || response.status === 205 || response.status === 304 || !response.body) {
|
|
@@ -585,11 +623,113 @@ async function parseResponse(response: Response, responseType?: ResponseType): P
|
|
|
585
623
|
}
|
|
586
624
|
|
|
587
625
|
/**
|
|
588
|
-
*
|
|
589
|
-
|
|
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.
|
|
590
729
|
*/
|
|
591
730
|
const defaultTransport: Transport = async (request: ResolvedRequest): Promise<TransportResult> => {
|
|
592
731
|
const init: RequestInit = {
|
|
732
|
+
...request.options, // cache, mode, redirect, keepalive, duplex, next, …
|
|
593
733
|
method: request.method,
|
|
594
734
|
headers: request.headers,
|
|
595
735
|
body: request.body,
|
|
@@ -606,6 +746,7 @@ const defaultTransport: Transport = async (request: ResolvedRequest): Promise<Tr
|
|
|
606
746
|
status: response.status,
|
|
607
747
|
statusText: response.statusText,
|
|
608
748
|
headers: response.headers,
|
|
749
|
+
contentType: getResponseContentType(response.headers),
|
|
609
750
|
request: nativeRequest,
|
|
610
751
|
response,
|
|
611
752
|
}
|