@kubb/plugin-client 5.12.0 → 6.0.1
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/README.md +1 -1
- package/dist/client-C-0-puI2.d.ts +554 -0
- package/dist/index.cjs +1377 -784
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -78
- package/dist/index.d.ts +117 -78
- package/dist/index.js +1375 -786
- package/dist/index.js.map +1 -1
- package/dist/templates/client.d.ts +3 -2
- package/dist/templates/core.d.ts +3 -3
- package/dist/templates/mediaTypes/formData.d.ts +5 -0
- package/dist/templates/mediaTypes/urlencoded.d.ts +5 -0
- package/dist/templates/mediaTypes.d.ts +2 -0
- package/dist/templates/serializers/cookies.d.ts +5 -0
- package/dist/templates/serializers/headers.d.ts +5 -0
- package/dist/templates/serializers/path.d.ts +5 -0
- package/dist/templates/serializers/pathStyles.d.ts +5 -0
- package/dist/templates/serializers/query.d.ts +5 -0
- package/dist/templates/serializers/queryStyles.d.ts +14 -0
- package/dist/templates/serializers.d.ts +118 -2
- package/dist/templates/sse.d.ts +20 -113
- package/dist/templates/standardSchema.d.ts +81 -2
- package/docs/guides/authentication.md +12 -2
- package/docs/guides/calling-operations.md +8 -6
- package/docs/guides/migrate-to-v6.md +139 -0
- package/docs/guides/validate-requests-and-responses.md +13 -8
- package/docs/guides/write-your-client.md +118 -12
- package/docs/options.md +5 -3
- package/docs/overview.md +3 -1
- package/kubb-extension.json +21 -3
- package/package.json +6 -5
- package/src/builders/callConfig.ts +80 -0
- package/src/builders/contentType.ts +35 -0
- package/src/builders/generics.ts +29 -0
- package/src/builders/operationBody.ts +43 -0
- package/src/builders/operationData.ts +64 -0
- package/src/builders/returnStatement.ts +103 -0
- package/src/builders/security.ts +125 -0
- package/src/builders/signature.ts +71 -0
- package/src/builders/styles.ts +94 -0
- package/src/builders/validator.ts +84 -0
- package/src/builders/validatorOptions.ts +53 -0
- package/src/clientTypes.ts +195 -0
- package/src/components/ClientRuntime.tsx +97 -0
- package/src/components/Operation.tsx +72 -0
- package/src/components/SdkClient.tsx +100 -0
- package/src/components/SdkFacade.tsx +35 -0
- package/src/generators/clientGenerator.tsx +91 -6
- package/src/generators/sdkGenerator.tsx +208 -0
- package/src/index.ts +3 -1
- package/src/macros.ts +9 -0
- package/src/plugin.ts +7 -8
- package/src/resolver.ts +28 -0
- package/src/setup.ts +90 -0
- package/src/types.ts +4 -2
- package/templates/client.ts +2 -1
- package/templates/core.ts +735 -286
- package/templates/mediaTypes/formData.ts +59 -0
- package/templates/mediaTypes/urlencoded.ts +33 -0
- package/templates/mediaTypes.ts +147 -0
- package/templates/serializers/cookies.ts +58 -0
- package/templates/serializers/headers.ts +35 -0
- package/templates/serializers/path.ts +19 -0
- package/templates/serializers/pathStyles.ts +27 -0
- package/templates/serializers/query.ts +21 -0
- package/templates/serializers/queryStyles.ts +107 -0
- package/templates/serializers.ts +48 -364
- package/templates/sse.ts +87 -178
- package/templates/standardSchema.ts +16 -37
- package/dist/client-BxO6Dygu.d.ts +0 -355
- package/dist/serializers-Ds8zZwOO.d.ts +0 -190
- package/dist/standardSchema-BShJWz2i.d.ts +0 -104
package/templates/core.ts
CHANGED
|
@@ -1,108 +1,147 @@
|
|
|
1
|
-
import type { NativeRequest, NativeResponse } from './client.ts'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
defaultPathSerializer,
|
|
7
|
-
defaultQuerySerializer,
|
|
8
|
-
isDefaultJsonBody,
|
|
9
|
-
isJsonMediaType,
|
|
10
|
-
serializeCookies,
|
|
11
|
-
} from './serializers.ts'
|
|
12
|
-
import type { HeadersInit, HttpMethod, PathParamStyle, PathSerializer, RequestBody, Serializers, Styles } from './serializers.ts'
|
|
1
|
+
import type { NativeOptions, NativeRequest, NativeResponse } from './client.ts'
|
|
2
|
+
import { defaultMediaTypes, isEncodedBody, resolveMediaType } from './mediaTypes.ts'
|
|
3
|
+
import type { MediaTypeContext, MediaTypes } from './mediaTypes.ts'
|
|
4
|
+
import { baseContentType, toValue } from './serializers.ts'
|
|
5
|
+
import type { HeadersInit, HttpMethod, RequestBody, Serializers, Styles } from './serializers.ts'
|
|
13
6
|
import { isValidationEnabled, runValidator, type StandardSchemaValidator } from './standardSchema.ts'
|
|
14
7
|
import type { ValidateOption, ValidationErrorHandler } from './standardSchema.ts'
|
|
15
8
|
|
|
9
|
+
export * from './mediaTypes.ts'
|
|
10
|
+
|
|
16
11
|
/**
|
|
17
|
-
*
|
|
12
|
+
* The status keys that count as success. `2xx` covers every 2xx status.
|
|
18
13
|
*/
|
|
19
|
-
export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226'
|
|
14
|
+
export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226' | '2xx'
|
|
20
15
|
|
|
21
16
|
export type SuccessOf<TResponses> = TResponses[Extract<keyof TResponses, SuccessStatusCode>]
|
|
22
17
|
|
|
23
18
|
export type ErrorOf<TResponses> = TResponses[Exclude<keyof TResponses, SuccessStatusCode>]
|
|
24
19
|
|
|
20
|
+
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
|
|
21
|
+
|
|
22
|
+
type ToNumber<T> = T extends `${infer TNumber extends number}` ? TNumber : never
|
|
23
|
+
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
25
|
+
* Maps a status key such as `404` or `4xx` to its numeric statuses. `default` becomes `number`.
|
|
27
26
|
*/
|
|
28
|
-
export type ToStatusNumber<TStatus> = TStatus extends `${infer TNumber extends number}`
|
|
27
|
+
export type ToStatusNumber<TStatus, TKeys = never> = TStatus extends `${infer TNumber extends number}`
|
|
28
|
+
? TNumber
|
|
29
|
+
: TStatus extends `${infer TFirst extends '1' | '2' | '3' | '4' | '5'}xx`
|
|
30
|
+
? Exclude<ToNumber<`${TFirst}${Digit}${Digit}`>, ToNumber<TKeys>>
|
|
31
|
+
: number
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
*
|
|
34
|
+
* The `error` of a failed call without a body, so `if (error)` still sees the failure.
|
|
32
35
|
*/
|
|
36
|
+
export type EmptyErrorBody = { status: number; statusText: string }
|
|
37
|
+
|
|
33
38
|
export type DataOf<T> = T extends { contentType: string; data: infer TData } ? TData : T
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
|
-
*
|
|
41
|
+
* A response's `Headers`, whose `get` suggests the header names the spec declares. Values stay strings, as sent.
|
|
37
42
|
*/
|
|
38
|
-
export type
|
|
43
|
+
export type ResponseHeaders<THeaders = unknown> = Omit<Headers, 'get'> & {
|
|
44
|
+
get: (name: Extract<keyof THeaders, string> | (string & {})) => string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The media types `streamMediaTypes` reads as a stream: a response with one of them resolves to its raw body.
|
|
48
|
+
type StreamContentType =
|
|
49
|
+
| `text/event-stream${string}`
|
|
50
|
+
| `application/jsonl${string}`
|
|
51
|
+
| `application/x-ndjson${string}`
|
|
52
|
+
| `application/json-seq${string}`
|
|
53
|
+
| `application/${string}+json-seq${string}`
|
|
54
|
+
|
|
55
|
+
type HeadersFor<THeaders, TStatus> = ResponseHeaders<TStatus extends keyof THeaders ? THeaders[TStatus] : unknown>
|
|
56
|
+
|
|
57
|
+
export type SuccessVariant<TStatus, TEntry, TRequest, TResponse, TKeys = never, THeaders = unknown> = TEntry extends { contentType: string; data: unknown }
|
|
39
58
|
? TEntry extends { contentType: infer TContentType; data: infer TData }
|
|
40
|
-
? {
|
|
59
|
+
? {
|
|
60
|
+
status: ToStatusNumber<TStatus, TKeys>
|
|
61
|
+
data: TContentType extends StreamContentType ? ReadableStream<Uint8Array> : TData
|
|
62
|
+
error: undefined
|
|
63
|
+
contentType: TContentType
|
|
64
|
+
headers: HeadersFor<THeaders, TStatus>
|
|
65
|
+
request: TRequest
|
|
66
|
+
response: TResponse
|
|
67
|
+
}
|
|
41
68
|
: never
|
|
42
|
-
: {
|
|
69
|
+
: {
|
|
70
|
+
status: ToStatusNumber<TStatus, TKeys>
|
|
71
|
+
data: TEntry
|
|
72
|
+
error: undefined
|
|
73
|
+
contentType: string | undefined
|
|
74
|
+
headers: HeadersFor<THeaders, TStatus>
|
|
75
|
+
request: TRequest
|
|
76
|
+
response: TResponse
|
|
77
|
+
}
|
|
43
78
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse> = TStatus extends SuccessStatusCode
|
|
48
|
-
? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse>
|
|
79
|
+
export type ResultByStatus<TResponses, TStatus extends keyof TResponses, TRequest, TResponse, THeaders = unknown> = TStatus extends SuccessStatusCode
|
|
80
|
+
? SuccessVariant<TStatus, TResponses[TStatus], TRequest, TResponse, keyof TResponses, THeaders>
|
|
49
81
|
: {
|
|
50
|
-
status: ToStatusNumber<TStatus>
|
|
82
|
+
status: ToStatusNumber<TStatus, keyof TResponses>
|
|
51
83
|
data: undefined
|
|
52
|
-
error: DataOf<TResponses[TStatus]>
|
|
84
|
+
error: DataOf<TResponses[TStatus]> | EmptyErrorBody
|
|
53
85
|
contentType: string | undefined
|
|
86
|
+
headers: HeadersFor<THeaders, TStatus>
|
|
54
87
|
request: TRequest
|
|
55
88
|
response: TResponse
|
|
56
89
|
}
|
|
57
90
|
|
|
58
|
-
export type ResultUnion<TResponses, TRequest, TResponse> = {
|
|
59
|
-
[TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
|
|
91
|
+
export type ResultUnion<TResponses, TRequest, TResponse, THeaders = unknown> = {
|
|
92
|
+
[TStatus in keyof TResponses]: ResultByStatus<TResponses, TStatus, TRequest, TResponse, THeaders>
|
|
60
93
|
}[keyof TResponses]
|
|
61
94
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*/
|
|
65
|
-
export type SuccessResultUnion<TResponses, TRequest, TResponse> = {
|
|
66
|
-
[TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse>
|
|
95
|
+
export type SuccessResultUnion<TResponses, TRequest, TResponse, THeaders = unknown> = {
|
|
96
|
+
[TStatus in Extract<keyof TResponses, SuccessStatusCode>]: ResultByStatus<TResponses, TStatus, TRequest, TResponse, THeaders>
|
|
67
97
|
}[Extract<keyof TResponses, SuccessStatusCode>]
|
|
68
98
|
|
|
69
99
|
/**
|
|
70
|
-
*
|
|
100
|
+
* The result of a call, narrowed by `status`. With `throwOnError`, it holds only the success variants. `THeaders` keys the
|
|
101
|
+
* declared response headers by status like the responses record, so `headers.get` suggests them for the narrowed status.
|
|
71
102
|
*/
|
|
72
|
-
export type RequestResult<
|
|
73
|
-
|
|
103
|
+
export type RequestResult<
|
|
104
|
+
TResponses,
|
|
105
|
+
ThrowOnError extends boolean = true,
|
|
106
|
+
TRequest = NativeRequest,
|
|
107
|
+
TResponse = NativeResponse,
|
|
108
|
+
THeaders = unknown,
|
|
109
|
+
> = ThrowOnError extends true
|
|
110
|
+
? [SuccessResultUnion<TResponses, TRequest, TResponse, THeaders>] extends [never]
|
|
74
111
|
? {
|
|
75
112
|
status: number
|
|
76
113
|
data: SuccessOf<TResponses>
|
|
77
114
|
error: undefined
|
|
78
115
|
contentType: string | undefined
|
|
116
|
+
headers: ResponseHeaders
|
|
79
117
|
request: TRequest
|
|
80
118
|
response: TResponse
|
|
81
119
|
}
|
|
82
|
-
: SuccessResultUnion<TResponses, TRequest, TResponse>
|
|
83
|
-
: [ResultUnion<TResponses, TRequest, TResponse>] extends [never]
|
|
84
|
-
? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse }
|
|
85
|
-
: ResultUnion<TResponses, TRequest, TResponse>
|
|
120
|
+
: SuccessResultUnion<TResponses, TRequest, TResponse, THeaders>
|
|
121
|
+
: [ResultUnion<TResponses, TRequest, TResponse, THeaders>] extends [never]
|
|
122
|
+
? { status: number; data: undefined; error: undefined; contentType: string | undefined; headers: ResponseHeaders; request: TRequest; response: TResponse }
|
|
123
|
+
: ResultUnion<TResponses, TRequest, TResponse, THeaders>
|
|
86
124
|
|
|
87
125
|
/**
|
|
88
|
-
*
|
|
126
|
+
* `RequestResult` with the declared response headers by status: `RequestResultWithHeaders<ListPetsResponses, { '200': ListPetsStatus200Headers }>`.
|
|
127
|
+
*/
|
|
128
|
+
export type RequestResultWithHeaders<TResponses, THeaders, ThrowOnError extends boolean = true> = RequestResult<
|
|
129
|
+
TResponses,
|
|
130
|
+
ThrowOnError,
|
|
131
|
+
NativeRequest,
|
|
132
|
+
NativeResponse,
|
|
133
|
+
THeaders
|
|
134
|
+
>
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A result promise whose `unwrap()` resolves to the success body.
|
|
89
138
|
*/
|
|
90
139
|
export type Unwrappable<T extends { data: unknown; error: unknown }> = Promise<T> & {
|
|
91
140
|
unwrap: () => Promise<Extract<T, { error: undefined }>['data']>
|
|
92
141
|
}
|
|
93
142
|
|
|
94
143
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```ts
|
|
99
|
-
* const { data, error } = await getPetById({ path: { petId: 1 } })
|
|
100
|
-
* ```
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* ```ts
|
|
104
|
-
* const pet = await getPetById({ path: { petId: 1 } }).unwrap()
|
|
105
|
-
* ```
|
|
144
|
+
* Adds `unwrap()` to a result promise. It rejects with `error` when the result has one.
|
|
106
145
|
*/
|
|
107
146
|
export function withUnwrap<T extends { data: unknown; error: unknown }>(promise: Promise<T>): Unwrappable<T> {
|
|
108
147
|
const unwrappable = promise as Unwrappable<T>
|
|
@@ -114,64 +153,68 @@ export function withUnwrap<T extends { data: unknown; error: unknown }>(promise:
|
|
|
114
153
|
return unwrappable
|
|
115
154
|
}
|
|
116
155
|
|
|
117
|
-
|
|
118
|
-
* The shape a generated operation returns with `returnType: 'data'`: the success body when
|
|
119
|
-
* `throwOnError` is true, or the full `RequestResult` when it is false.
|
|
120
|
-
*/
|
|
121
|
-
export type UnwrappedResult<TResponses, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = ThrowOnError extends true
|
|
122
|
-
? RequestResult<TResponses, true, TRequest, TResponse>['data']
|
|
123
|
-
: RequestResult<TResponses, ThrowOnError, TRequest, TResponse>
|
|
156
|
+
export type Unwrapped<T, ThrowOnError extends boolean> = ThrowOnError extends true ? (T extends { data: infer TData } ? TData : never) : T
|
|
124
157
|
|
|
125
158
|
/**
|
|
126
|
-
*
|
|
159
|
+
* What an operation returns with `returnType: 'data'`: the success body, or the full result when `throwOnError` is false.
|
|
127
160
|
*/
|
|
128
|
-
export
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata'
|
|
161
|
+
export type UnwrappedResult<
|
|
162
|
+
TResponses,
|
|
163
|
+
ThrowOnError extends boolean = true,
|
|
164
|
+
TRequest = NativeRequest,
|
|
165
|
+
TResponse = NativeResponse,
|
|
166
|
+
THeaders = unknown,
|
|
167
|
+
> = Unwrapped<RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>, ThrowOnError>
|
|
136
168
|
|
|
137
169
|
/**
|
|
138
|
-
*
|
|
139
|
-
* types that do not share a single parser. Keys are base media types, such as `audio/mpeg` or `audio/*`.
|
|
170
|
+
* `UnwrappedResult` with the declared response headers by status, for the full result a `throwOnError: false` call resolves to.
|
|
140
171
|
*/
|
|
141
|
-
export type
|
|
172
|
+
export type UnwrappedResultWithHeaders<TResponses, THeaders, ThrowOnError extends boolean = true> = UnwrappedResult<
|
|
173
|
+
TResponses,
|
|
174
|
+
ThrowOnError,
|
|
175
|
+
NativeRequest,
|
|
176
|
+
NativeResponse,
|
|
177
|
+
THeaders
|
|
178
|
+
>
|
|
142
179
|
|
|
143
180
|
/**
|
|
144
|
-
*
|
|
181
|
+
* Resolves to the success body when `throwOnError` is true, or to the full result when it is false.
|
|
145
182
|
*/
|
|
146
|
-
export
|
|
183
|
+
export function unwrapResult<T extends { data: unknown; error: unknown }, ThrowOnError extends boolean>(
|
|
184
|
+
promise: Promise<T>,
|
|
185
|
+
throwOnError: ThrowOnError,
|
|
186
|
+
): Promise<Unwrapped<T, ThrowOnError>> {
|
|
187
|
+
return promise.then((result) => (throwOnError ? result.data : result) as Unwrapped<T, ThrowOnError>)
|
|
188
|
+
}
|
|
147
189
|
|
|
148
190
|
/**
|
|
149
|
-
*
|
|
191
|
+
* The call's `throwOnError`, or the operation's generated default when the call leaves it unset.
|
|
150
192
|
*/
|
|
151
|
-
export
|
|
193
|
+
export function resolveThrowOnError<ThrowOnError extends boolean>(throwOnError: ThrowOnError | undefined, fallback: boolean): ThrowOnError {
|
|
194
|
+
return (throwOnError ?? fallback) as ThrowOnError
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown }
|
|
198
|
+
|
|
199
|
+
export type RequestCredentials = 'omit' | 'same-origin' | 'include'
|
|
152
200
|
|
|
153
201
|
/**
|
|
154
|
-
*
|
|
202
|
+
* Reads a 2xx body as this format instead of by its media type. An error body still uses its media type.
|
|
155
203
|
*/
|
|
156
|
-
export type
|
|
157
|
-
serialize?: ContentBodySerializer
|
|
158
|
-
deserialize?: Deserializer
|
|
159
|
-
}
|
|
204
|
+
export type ParseAs = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream'
|
|
160
205
|
|
|
161
206
|
/**
|
|
162
|
-
*
|
|
207
|
+
* The request content type, or `{ request, response }` to also set `Accept`.
|
|
163
208
|
*/
|
|
164
209
|
export type ContentType = string | { request?: string; response?: string }
|
|
165
210
|
|
|
166
|
-
/**
|
|
167
|
-
* Validates request bodies and params, success bodies, or non-throwing error bodies with Standard Schema.
|
|
168
|
-
*/
|
|
169
211
|
export type Validator<T = unknown> = StandardSchemaValidator<T>
|
|
170
212
|
|
|
171
213
|
export type { ValidateOption, ValidationErrorContext, ValidationErrorHandler } from './standardSchema.ts'
|
|
172
214
|
|
|
173
215
|
/**
|
|
174
|
-
* A
|
|
216
|
+
* A security scheme of a generated call, passed to the `auth` resolver. OAuth2 `flows` and `metadataUrl` (RFC 8414) and
|
|
217
|
+
* the OpenID Connect `url` tell the resolver where to get a token.
|
|
175
218
|
*/
|
|
176
219
|
export type Auth = {
|
|
177
220
|
type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect'
|
|
@@ -179,49 +222,116 @@ export type Auth = {
|
|
|
179
222
|
name?: string
|
|
180
223
|
in?: 'header' | 'query' | 'cookie'
|
|
181
224
|
scopes?: Array<string>
|
|
225
|
+
flows?: Record<string, { authorizationUrl?: string; deviceAuthorizationUrl?: string; tokenUrl?: string; refreshUrl?: string }>
|
|
226
|
+
metadataUrl?: string
|
|
227
|
+
url?: string
|
|
182
228
|
}
|
|
183
229
|
|
|
184
230
|
/**
|
|
185
|
-
*
|
|
231
|
+
* One scheme, or the schemes one requirement sends together.
|
|
232
|
+
*/
|
|
233
|
+
export type SecurityRequirement = Auth | Array<Auth>
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* `user:password` for basic auth, or `undefined` to skip the scheme.
|
|
186
237
|
*/
|
|
187
238
|
export type AuthToken = string | undefined
|
|
188
239
|
|
|
189
240
|
/**
|
|
190
|
-
*
|
|
241
|
+
* A static token, or a callback per scheme. The first requirement whose schemes all get a token is sent.
|
|
191
242
|
*/
|
|
192
243
|
export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise<AuthToken>)
|
|
193
244
|
|
|
245
|
+
/**
|
|
246
|
+
* One Server-Sent Event. `data` is parsed JSON when it parses, otherwise the raw string.
|
|
247
|
+
*/
|
|
248
|
+
export type ServerSentEvent<TData = unknown> = {
|
|
249
|
+
data: TData
|
|
250
|
+
event: string
|
|
251
|
+
id?: string
|
|
252
|
+
retry?: number
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* An event the document types by name, plus the `id` and `retry` any event may carry.
|
|
257
|
+
*/
|
|
258
|
+
export type TypedEvent<TEvent> = TEvent & Pick<ServerSentEvent, 'id' | 'retry'>
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Per-call options of a stream operation.
|
|
262
|
+
*/
|
|
263
|
+
export type EventStreamOptions = {
|
|
264
|
+
/**
|
|
265
|
+
* The most characters one event or JSON line may hold. A larger one throws a `RangeError`, and `Infinity` turns the limit off.
|
|
266
|
+
* @default 16777216
|
|
267
|
+
*/
|
|
268
|
+
maxEventSize?: number | undefined
|
|
269
|
+
/**
|
|
270
|
+
* Ends the stream when an event's raw `data` or a JSON line equals this value, such as `[DONE]`.
|
|
271
|
+
*/
|
|
272
|
+
terminator?: string | undefined
|
|
273
|
+
/**
|
|
274
|
+
* Maps each Server-Sent Event before validation.
|
|
275
|
+
*/
|
|
276
|
+
transform?: (event: ServerSentEvent) => ServerSentEvent
|
|
277
|
+
/**
|
|
278
|
+
* What `validator.response` checks on each event: the payload (`data`) or the whole event (`event`).
|
|
279
|
+
*/
|
|
280
|
+
eventShape?: 'event' | 'data'
|
|
281
|
+
/**
|
|
282
|
+
* Keeps each Server-Sent Event's `data` as the raw text instead of JSON-parsing it. The generated client sets it when the
|
|
283
|
+
* document types `data` as a string. A list of event names keeps it for those events only.
|
|
284
|
+
*/
|
|
285
|
+
rawData?: boolean | Array<string> | undefined
|
|
286
|
+
}
|
|
287
|
+
|
|
194
288
|
export type RequestConfig<TBody = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = {
|
|
195
289
|
baseURL?: string
|
|
196
290
|
url?: string
|
|
197
291
|
method?: HttpMethod
|
|
198
292
|
path?: unknown
|
|
199
293
|
query?: unknown
|
|
200
|
-
/**
|
|
294
|
+
/**
|
|
295
|
+
* Alias of `query`.
|
|
296
|
+
*/
|
|
201
297
|
params?: unknown
|
|
202
298
|
cookies?: unknown
|
|
203
299
|
body?: TBody
|
|
204
300
|
headers?: unknown
|
|
205
|
-
styles?: Styles
|
|
206
|
-
signal?: AbortSignal
|
|
207
|
-
credentials?: RequestCredentials
|
|
208
|
-
options?:
|
|
301
|
+
styles?: Styles | undefined
|
|
302
|
+
signal?: AbortSignal | undefined
|
|
303
|
+
credentials?: RequestCredentials | undefined
|
|
304
|
+
options?: NativeOptions | undefined
|
|
209
305
|
contentType?: ContentType
|
|
210
|
-
|
|
306
|
+
parseAs?: ParseAs
|
|
211
307
|
throwOnError?: boolean
|
|
212
308
|
client?: ClientInstance<TRequest, TResponse>
|
|
213
309
|
transport?: Transport<TRequest, TResponse>
|
|
214
310
|
serializer?: Serializers
|
|
215
|
-
|
|
216
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Merged per key over the client's media types.
|
|
313
|
+
*/
|
|
314
|
+
mediaTypes?: MediaTypes
|
|
315
|
+
/**
|
|
316
|
+
* `error` takes one validator, or a map by status such as `{ 404: schema, '5xx': schema, default: schema }`.
|
|
317
|
+
*/
|
|
318
|
+
validator?: {
|
|
319
|
+
request?: Validator
|
|
320
|
+
path?: Validator
|
|
321
|
+
query?: Validator
|
|
322
|
+
headers?: Validator
|
|
323
|
+
cookies?: Validator
|
|
324
|
+
response?: Validator
|
|
325
|
+
error?: Validator | Record<string, Validator>
|
|
326
|
+
}
|
|
217
327
|
onValidationError?: ValidationErrorHandler
|
|
218
328
|
validate?: ValidateOption
|
|
219
|
-
security?: Array<
|
|
329
|
+
security?: Array<SecurityRequirement>
|
|
220
330
|
auth?: AuthResolver
|
|
221
331
|
}
|
|
222
332
|
|
|
223
333
|
/**
|
|
224
|
-
*
|
|
334
|
+
* The options of a generated operation: its typed request groups plus the call config.
|
|
225
335
|
*/
|
|
226
336
|
export type Options<TData extends DataShape, ThrowOnError extends boolean = true, TRequest = NativeRequest, TResponse = NativeResponse> = Omit<
|
|
227
337
|
RequestConfig<unknown, TRequest, TResponse>,
|
|
@@ -232,107 +342,187 @@ export type Options<TData extends DataShape, ThrowOnError extends boolean = true
|
|
|
232
342
|
throwOnError?: ThrowOnError
|
|
233
343
|
}
|
|
234
344
|
|
|
345
|
+
export type StreamOptions<TData extends DataShape, ThrowOnError extends boolean = true> = Options<TData, ThrowOnError> & EventStreamOptions
|
|
346
|
+
|
|
235
347
|
/**
|
|
236
|
-
*
|
|
348
|
+
* Config shared by every call of a client. A call's own config overrides it.
|
|
237
349
|
*/
|
|
238
350
|
export type ClientConfig<TRequest = NativeRequest, TResponse = NativeResponse> = {
|
|
239
351
|
baseURL?: string
|
|
240
352
|
headers?: HeadersInit
|
|
241
353
|
credentials?: RequestCredentials
|
|
242
|
-
options?:
|
|
354
|
+
options?: NativeOptions
|
|
243
355
|
throwOnError?: boolean
|
|
244
356
|
transport?: Transport<TRequest, TResponse>
|
|
245
357
|
serializer?: Serializers
|
|
246
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Merged per key over `defaultMediaTypes`.
|
|
360
|
+
*/
|
|
361
|
+
mediaTypes?: MediaTypes
|
|
247
362
|
onValidationError?: ValidationErrorHandler
|
|
248
363
|
validate?: ValidateOption
|
|
249
364
|
auth?: AuthResolver
|
|
250
365
|
}
|
|
251
366
|
|
|
252
367
|
/**
|
|
253
|
-
* The
|
|
368
|
+
* The request a transport sends, with its URL, headers, body, and auth already resolved.
|
|
254
369
|
*/
|
|
255
370
|
export type ResolvedRequest = {
|
|
256
371
|
url: string
|
|
257
372
|
method: string
|
|
258
373
|
headers: Record<string, string>
|
|
259
|
-
body?: RequestBody
|
|
260
|
-
signal?: AbortSignal
|
|
261
|
-
credentials?: RequestCredentials
|
|
262
|
-
options?:
|
|
263
|
-
|
|
374
|
+
body?: RequestBody | undefined
|
|
375
|
+
signal?: AbortSignal | undefined
|
|
376
|
+
credentials?: RequestCredentials | undefined
|
|
377
|
+
options?: NativeOptions | undefined
|
|
378
|
+
/**
|
|
379
|
+
* Read the response as a stream. A transport that buffers bodies, such as axios, must stream it.
|
|
380
|
+
*/
|
|
381
|
+
stream?: boolean
|
|
264
382
|
}
|
|
265
383
|
|
|
266
384
|
/**
|
|
267
|
-
*
|
|
385
|
+
* A transport's `Response` with the native request and response that results and errors carry.
|
|
268
386
|
*/
|
|
269
|
-
export type TransportResult<
|
|
270
|
-
|
|
271
|
-
status: number
|
|
272
|
-
statusText: string
|
|
273
|
-
headers: Headers
|
|
274
|
-
contentType?: string
|
|
387
|
+
export type TransportResult<TRequest = NativeRequest, TResponse = NativeResponse> = {
|
|
388
|
+
webResponse: Response
|
|
275
389
|
request: TRequest
|
|
276
390
|
response: TResponse
|
|
277
391
|
}
|
|
278
392
|
|
|
279
393
|
/**
|
|
280
|
-
*
|
|
394
|
+
* Sends a resolved request and returns its `Response` with the body unread. The client decodes it.
|
|
281
395
|
*/
|
|
282
396
|
export type Transport<TRequest = NativeRequest, TResponse = NativeResponse> = (
|
|
283
397
|
request: ResolvedRequest,
|
|
284
|
-
) => Promise<TransportResult<
|
|
398
|
+
) => Promise<Response | TransportResult<TRequest, TResponse>>
|
|
285
399
|
|
|
286
|
-
|
|
400
|
+
/**
|
|
401
|
+
* What a response interceptor gets: the decoded body with the status, headers, and native objects.
|
|
402
|
+
*/
|
|
403
|
+
export type DecodedResponse<TData = unknown, TRequest = NativeRequest, TResponse = NativeResponse> = {
|
|
404
|
+
data: TData
|
|
287
405
|
status: number
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
contentType
|
|
406
|
+
statusText: string
|
|
407
|
+
headers: Headers
|
|
408
|
+
contentType?: string | undefined
|
|
291
409
|
request: TRequest
|
|
292
410
|
response: TResponse
|
|
293
411
|
}
|
|
294
412
|
|
|
295
|
-
export type InterceptorFn<T, TContext = never> = (value: T, context?: TContext) =>
|
|
413
|
+
export type InterceptorFn<T, TContext = never, TResult = T> = (value: T, context?: TContext) => TResult | Promise<TResult>
|
|
296
414
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
*/
|
|
300
|
-
export type InterceptorStack<T, TContext = never> = {
|
|
301
|
-
use: (fn: InterceptorFn<T, TContext>) => number
|
|
415
|
+
export type InterceptorStack<T, TContext = never, TResult = T> = {
|
|
416
|
+
use: (fn: InterceptorFn<T, TContext, TResult>) => number
|
|
302
417
|
eject: (id: number) => void
|
|
303
|
-
update: (id: number, fn: InterceptorFn<T, TContext>) => void
|
|
418
|
+
update: (id: number, fn: InterceptorFn<T, TContext, TResult>) => void
|
|
304
419
|
run: (value: T, context?: TContext) => Promise<T>
|
|
305
420
|
}
|
|
306
421
|
|
|
307
422
|
export type Interceptors<TRequest = NativeRequest, TResponse = NativeResponse> = {
|
|
308
423
|
request: InterceptorStack<ResolvedRequest, RequestConfig<unknown, TRequest, TResponse>>
|
|
309
|
-
response: InterceptorStack<
|
|
310
|
-
|
|
424
|
+
response: InterceptorStack<DecodedResponse<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>
|
|
425
|
+
/**
|
|
426
|
+
* Sees every failure: a `ResponseError`, a network error, an abort, or a validation error. Return another error to throw it instead.
|
|
427
|
+
*/
|
|
428
|
+
error: InterceptorStack<ResponseError<unknown, TRequest, TResponse> | Error, RequestConfig<unknown, TRequest, TResponse>, Error | void>
|
|
311
429
|
}
|
|
312
430
|
|
|
313
431
|
export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>
|
|
314
432
|
|
|
433
|
+
type StreamParts<TEvent> = AsyncIterable<TEvent> & {
|
|
434
|
+
stream: AsyncGenerator<TEvent>
|
|
435
|
+
/**
|
|
436
|
+
* The last event ID the server sent, for a `Last-Event-ID` header on a new call.
|
|
437
|
+
*/
|
|
438
|
+
readonly lastEventId: string
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
type WithEvents<TResult, TEvent> = TResult extends { error: undefined } ? Omit<TResult, 'data'> & { data: AsyncGenerator<TEvent> } : TResult
|
|
442
|
+
|
|
315
443
|
/**
|
|
316
|
-
*
|
|
317
|
-
|
|
444
|
+
* The result of a stream call. Iterate it, or its `data`, for the items. A failed call yields none.
|
|
445
|
+
*/
|
|
446
|
+
export type StreamResult<
|
|
447
|
+
TResponses,
|
|
448
|
+
ThrowOnError extends boolean = true,
|
|
449
|
+
TEvent = ServerSentEvent<SuccessOf<TResponses>>,
|
|
450
|
+
TRequest = NativeRequest,
|
|
451
|
+
TResponse = NativeResponse,
|
|
452
|
+
THeaders = unknown,
|
|
453
|
+
> = WithEvents<RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>, TEvent> & StreamParts<TEvent>
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* `StreamResult` with the declared response headers by status: `StreamResultWithHeaders<StreamEventsResponses, { '200': StreamEventsStatus200Headers }>`.
|
|
457
|
+
*/
|
|
458
|
+
export type StreamResultWithHeaders<TResponses, THeaders, ThrowOnError extends boolean = true, TEvent = ServerSentEvent<SuccessOf<TResponses>>> = StreamResult<
|
|
459
|
+
TResponses,
|
|
460
|
+
ThrowOnError,
|
|
461
|
+
TEvent,
|
|
462
|
+
NativeRequest,
|
|
463
|
+
NativeResponse,
|
|
464
|
+
THeaders
|
|
465
|
+
>
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* What a stream operation returns with `returnType: 'data'`.
|
|
469
|
+
*/
|
|
470
|
+
export type UnwrappedStreamResult<
|
|
471
|
+
TResponses,
|
|
472
|
+
ThrowOnError extends boolean = true,
|
|
473
|
+
TEvent = ServerSentEvent<SuccessOf<TResponses>>,
|
|
474
|
+
THeaders = unknown,
|
|
475
|
+
> = Unwrapped<StreamResult<TResponses, ThrowOnError, TEvent, NativeRequest, NativeResponse, THeaders>, ThrowOnError>
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* `UnwrappedStreamResult` with the declared response headers by status, for the full result a `throwOnError: false` call resolves to.
|
|
479
|
+
*/
|
|
480
|
+
export type UnwrappedStreamResultWithHeaders<
|
|
481
|
+
TResponses,
|
|
482
|
+
THeaders,
|
|
483
|
+
ThrowOnError extends boolean = true,
|
|
484
|
+
TEvent = ServerSentEvent<SuccessOf<TResponses>>,
|
|
485
|
+
> = UnwrappedStreamResult<TResponses, ThrowOnError, TEvent, THeaders>
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* A client. Call it to send a request.
|
|
318
489
|
*/
|
|
319
490
|
export type ClientInstance<TRequest = NativeRequest, TResponse = NativeResponse> = {
|
|
320
|
-
<
|
|
491
|
+
<TResponses = unknown, ThrowOnError extends boolean = true, THeaders = unknown>(
|
|
492
|
+
config: RequestConfig<unknown, TRequest, TResponse> & { throwOnError?: ThrowOnError },
|
|
493
|
+
): Promise<RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>>
|
|
494
|
+
/**
|
|
495
|
+
* Sends the request and reads a 2xx body item by item. A body whose media type cannot stream throws a `TypeError`.
|
|
496
|
+
*/
|
|
497
|
+
stream: <TResponses = unknown, ThrowOnError extends boolean = true, TItem = ServerSentEvent<SuccessOf<TResponses>>, THeaders = unknown>(
|
|
498
|
+
config: RequestConfig<unknown, TRequest, TResponse> & EventStreamOptions & { throwOnError?: ThrowOnError },
|
|
499
|
+
) => Promise<StreamResult<TResponses, ThrowOnError, TItem, TRequest, TResponse, THeaders>>
|
|
321
500
|
getConfig: () => ClientConfig<TRequest, TResponse>
|
|
322
501
|
setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>
|
|
323
502
|
getUrl: (config: UrlConfig) => string
|
|
324
503
|
interceptors: Interceptors<TRequest, TResponse>
|
|
504
|
+
/**
|
|
505
|
+
* A client with this config and no interceptors.
|
|
506
|
+
*/
|
|
325
507
|
createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
|
|
508
|
+
/**
|
|
509
|
+
* A client with this config that runs this client's interceptors before its own.
|
|
510
|
+
*/
|
|
511
|
+
extend: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
|
|
326
512
|
}
|
|
327
513
|
|
|
328
514
|
/**
|
|
329
|
-
* Thrown for a non-2xx response
|
|
515
|
+
* Thrown for a non-2xx response when `throwOnError` is true.
|
|
330
516
|
*/
|
|
331
517
|
export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse = NativeResponse> extends Error {
|
|
332
518
|
data: TError
|
|
333
519
|
status: number
|
|
334
520
|
statusText: string
|
|
335
521
|
contentType: string | undefined
|
|
522
|
+
/**
|
|
523
|
+
* The response's `Headers`, the same on every transport.
|
|
524
|
+
*/
|
|
525
|
+
headers: ResponseHeaders
|
|
336
526
|
request: TRequest
|
|
337
527
|
response: TResponse
|
|
338
528
|
|
|
@@ -340,13 +530,14 @@ export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse
|
|
|
340
530
|
data: TError
|
|
341
531
|
status: number
|
|
342
532
|
statusText: string
|
|
343
|
-
contentType?: string
|
|
533
|
+
contentType?: string | undefined
|
|
534
|
+
headers?: Headers | undefined
|
|
344
535
|
request: TRequest
|
|
345
536
|
response: TResponse
|
|
346
|
-
method?: string
|
|
347
|
-
url?: string
|
|
537
|
+
method?: string | undefined
|
|
538
|
+
url?: string | undefined
|
|
348
539
|
}) {
|
|
349
|
-
// The query and hash
|
|
540
|
+
// The query and hash stay out of the message, so secrets in them do not reach logs.
|
|
350
541
|
const cleanUrl = config.url?.split(/[?#]/)[0]
|
|
351
542
|
const target = [config.method, cleanUrl].filter(Boolean).join(' ')
|
|
352
543
|
const statusText = config.statusText?.trim()
|
|
@@ -356,12 +547,13 @@ export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse
|
|
|
356
547
|
this.status = config.status
|
|
357
548
|
this.statusText = config.statusText
|
|
358
549
|
this.contentType = config.contentType
|
|
550
|
+
this.headers = config.headers ?? new Headers()
|
|
359
551
|
this.request = config.request
|
|
360
552
|
this.response = config.response
|
|
361
553
|
}
|
|
362
554
|
|
|
363
555
|
/**
|
|
364
|
-
* Matches on `name`,
|
|
556
|
+
* Matches on `name`, since every generated client has its own `ResponseError` class.
|
|
365
557
|
*/
|
|
366
558
|
static is(error: unknown): error is ResponseError<unknown, unknown, unknown> {
|
|
367
559
|
return error instanceof Error && error.name === 'ResponseError'
|
|
@@ -370,8 +562,41 @@ export class ResponseError<TError = unknown, TRequest = NativeRequest, TResponse
|
|
|
370
562
|
|
|
371
563
|
export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
|
|
372
564
|
|
|
565
|
+
type ErrorStatus<TResponses> = Exclude<keyof TResponses, SuccessStatusCode>
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The `ResponseError` an operation throws. Checking `status` narrows `data` to that status's body, and `headers.get` to
|
|
569
|
+
* the header names in `THeaders`, the same record `RequestResult` takes.
|
|
570
|
+
*/
|
|
571
|
+
export type ResponseErrorOf<TResponses, TRequest = NativeRequest, TResponse = NativeResponse, THeaders = unknown> = [ErrorStatus<TResponses>] extends [never]
|
|
572
|
+
? ResponseError<unknown, TRequest, TResponse>
|
|
573
|
+
: {
|
|
574
|
+
[TStatus in ErrorStatus<TResponses>]: ResponseError<DataOf<TResponses[TStatus]>, TRequest, TResponse> & {
|
|
575
|
+
status: ToStatusNumber<TStatus, keyof TResponses>
|
|
576
|
+
headers: HeadersFor<THeaders, TStatus>
|
|
577
|
+
}
|
|
578
|
+
}[ErrorStatus<TResponses>]
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Narrows a caught error to the `ResponseError` of the operation whose responses record you pass.
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```ts
|
|
585
|
+
* try {
|
|
586
|
+
* await getPetById({ path: { petId: 7 } })
|
|
587
|
+
* } catch (error) {
|
|
588
|
+
* if (isResponseError<GetPetByIdResponses>(error) && error.status === 404) error.data // GetPetById404
|
|
589
|
+
* }
|
|
590
|
+
* ```
|
|
591
|
+
*/
|
|
592
|
+
export function isResponseError<TResponses = unknown, TRequest = NativeRequest, TResponse = NativeResponse, THeaders = unknown>(
|
|
593
|
+
error: unknown,
|
|
594
|
+
): error is ResponseErrorOf<TResponses, TRequest, TResponse, THeaders> {
|
|
595
|
+
return ResponseError.is(error)
|
|
596
|
+
}
|
|
597
|
+
|
|
373
598
|
function headerEntries(headers: object): Array<[string, unknown]> {
|
|
374
|
-
if (Array.isArray(headers)) return headers
|
|
599
|
+
if (Array.isArray(headers)) return headers as Array<[string, unknown]>
|
|
375
600
|
return headers instanceof Headers ? [...headers] : Object.entries(headers)
|
|
376
601
|
}
|
|
377
602
|
|
|
@@ -381,7 +606,8 @@ function serializeHeaders(headers: HeadersInit | undefined): Record<string, stri
|
|
|
381
606
|
const result: Record<string, string> = {}
|
|
382
607
|
for (const [key, value] of entries) {
|
|
383
608
|
if (value === undefined || value === null) continue
|
|
384
|
-
|
|
609
|
+
// A `Date` is a unit like in the parameter serializers, not an object to send as JSON.
|
|
610
|
+
result[key] = typeof value === 'object' && !(value instanceof Date) ? JSON.stringify(value) : toValue(value)
|
|
385
611
|
}
|
|
386
612
|
return result
|
|
387
613
|
}
|
|
@@ -404,36 +630,45 @@ function collectHeadersCaseInsensitive(...sources: Array<unknown>): Record<strin
|
|
|
404
630
|
return result
|
|
405
631
|
}
|
|
406
632
|
|
|
407
|
-
|
|
633
|
+
/**
|
|
634
|
+
* The value of a header, matched by name in any letter case.
|
|
635
|
+
*/
|
|
636
|
+
export function getHeader(headers: Record<string, string>, name: string): string | undefined {
|
|
408
637
|
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
|
|
409
638
|
return key ? headers[key] : undefined
|
|
410
639
|
}
|
|
411
640
|
|
|
641
|
+
function deleteHeader(headers: Record<string, string>, name: string): void {
|
|
642
|
+
for (const key of Object.keys(headers)) {
|
|
643
|
+
if (key.toLowerCase() === name.toLowerCase()) delete headers[key]
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
412
647
|
function serializeUrl({
|
|
413
648
|
parts,
|
|
414
649
|
pathParams,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
650
|
+
query,
|
|
651
|
+
serializers,
|
|
652
|
+
styles,
|
|
418
653
|
}: {
|
|
419
654
|
parts: Array<string | undefined>
|
|
420
655
|
pathParams: Record<string, unknown>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
656
|
+
query: Record<string, unknown>
|
|
657
|
+
serializers: Serializers
|
|
658
|
+
styles?: Styles | undefined
|
|
424
659
|
}): string {
|
|
425
660
|
const path = parts
|
|
426
|
-
.
|
|
427
|
-
.
|
|
428
|
-
.replace(/\{([^{}]+)\}/g, (_, key: string) =>
|
|
661
|
+
// A base URL that ends in `/` would give `//` before a path that starts with one.
|
|
662
|
+
.reduce<string>((url, part) => (part ? url + (url.endsWith('/') && part.startsWith('/') ? part.slice(1) : part) : url), '')
|
|
663
|
+
.replace(/\{([^{}]+)\}/g, (_, key: string) => requireSerializer(serializers, 'path')({ name: key, value: pathParams[key], options: styles?.path?.[key] }))
|
|
664
|
+
// A query without values needs no serializer.
|
|
665
|
+
const hasQuery = Object.values(query).some((value) => value !== undefined && value !== null)
|
|
666
|
+
const search = serializers.query || hasQuery ? requireSerializer(serializers, 'query')(query, styles?.query) : ''
|
|
429
667
|
return path + (search ? `?${search}` : '')
|
|
430
668
|
}
|
|
431
669
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
*/
|
|
435
|
-
export function createInterceptorStack<T, TContext = never>(): InterceptorStack<T, TContext> {
|
|
436
|
-
let entries: Array<{ id: number; fn: InterceptorFn<T, TContext> }> = []
|
|
670
|
+
export function createInterceptorStack<T, TContext = never, TResult = T>(): InterceptorStack<T, TContext, TResult> {
|
|
671
|
+
let entries: Array<{ id: number; fn: InterceptorFn<T, TContext, TResult> }> = []
|
|
437
672
|
let counter = 0
|
|
438
673
|
return {
|
|
439
674
|
use(fn) {
|
|
@@ -451,18 +686,38 @@ export function createInterceptorStack<T, TContext = never>(): InterceptorStack<
|
|
|
451
686
|
async run(value, context) {
|
|
452
687
|
let result = value
|
|
453
688
|
for (const entry of entries) {
|
|
454
|
-
|
|
689
|
+
const next = await entry.fn(result, context)
|
|
690
|
+
if (next !== undefined) result = next as T
|
|
455
691
|
}
|
|
456
692
|
return result
|
|
457
693
|
},
|
|
458
694
|
}
|
|
459
695
|
}
|
|
460
696
|
|
|
697
|
+
function placeToken({ scheme, token, headers, query }: { scheme: Auth; token: string; headers: Record<string, string>; query: Record<string, unknown> }): void {
|
|
698
|
+
if (scheme.type !== 'apiKey') {
|
|
699
|
+
if (getHeader(headers, 'Authorization') === undefined) {
|
|
700
|
+
headers['Authorization'] = scheme.scheme === 'basic' ? `Basic ${btoa(String.fromCharCode(...new TextEncoder().encode(token)))}` : `Bearer ${token}`
|
|
701
|
+
}
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
const name = scheme.name ?? 'Authorization'
|
|
705
|
+
if (scheme.in === 'query') {
|
|
706
|
+
if (query[name] === undefined) query[name] = token
|
|
707
|
+
return
|
|
708
|
+
}
|
|
709
|
+
if (scheme.in === 'cookie') {
|
|
710
|
+
appendCookie(headers, `${name}=${token}`)
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
if (getHeader(headers, name) === undefined) headers[name] = token
|
|
714
|
+
}
|
|
715
|
+
|
|
461
716
|
/**
|
|
462
|
-
*
|
|
717
|
+
* Adds the tokens of the first security requirement whose schemes all get one to `headers` and `query`.
|
|
463
718
|
*/
|
|
464
719
|
export async function resolveAuth(params: {
|
|
465
|
-
security: Array<
|
|
720
|
+
security: Array<SecurityRequirement> | undefined
|
|
466
721
|
auth: AuthResolver | undefined
|
|
467
722
|
headers: Record<string, string>
|
|
468
723
|
query: Record<string, unknown>
|
|
@@ -470,94 +725,117 @@ export async function resolveAuth(params: {
|
|
|
470
725
|
const { security, auth, headers, query } = params
|
|
471
726
|
if (!security?.length || auth === undefined) return
|
|
472
727
|
|
|
473
|
-
for (const
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (getHeader(headers, 'Authorization') === undefined) {
|
|
479
|
-
headers['Authorization'] = scheme.scheme === 'basic' ? `Basic ${btoa(String.fromCharCode(...new TextEncoder().encode(token)))}` : `Bearer ${token}`
|
|
480
|
-
}
|
|
481
|
-
return
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const name = scheme.name ?? 'Authorization'
|
|
485
|
-
if (scheme.in === 'query') {
|
|
486
|
-
if (query[name] === undefined) query[name] = token
|
|
487
|
-
return
|
|
488
|
-
}
|
|
489
|
-
if (scheme.in === 'cookie') {
|
|
490
|
-
headers['Cookie'] = [headers['Cookie'], `${name}=${token}`].filter(Boolean).join('; ')
|
|
491
|
-
return
|
|
492
|
-
}
|
|
493
|
-
if (getHeader(headers, name) === undefined) headers[name] = token
|
|
728
|
+
for (const requirement of security) {
|
|
729
|
+
const schemes = Array.isArray(requirement) ? requirement : [requirement]
|
|
730
|
+
const tokens = await Promise.all(schemes.map((scheme) => (typeof auth === 'function' ? auth(scheme) : auth)))
|
|
731
|
+
if (tokens.some((token) => token === undefined)) continue
|
|
732
|
+
schemes.forEach((scheme, index) => placeToken({ scheme, token: tokens[index]!, headers, query }))
|
|
494
733
|
return
|
|
495
734
|
}
|
|
496
735
|
}
|
|
497
736
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
737
|
+
function appendCookie(headers: Record<string, string>, cookie: string): void {
|
|
738
|
+
const key = Object.keys(headers).find((name) => name.toLowerCase() === 'cookie') ?? 'Cookie'
|
|
739
|
+
headers[key] = [headers[key], cookie].filter(Boolean).join('; ')
|
|
501
740
|
}
|
|
502
741
|
|
|
503
742
|
/**
|
|
504
|
-
*
|
|
505
|
-
*
|
|
743
|
+
* The `RequestInit` of a resolved request, for `fetch` or a library that takes the same init, such as ky.
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```ts
|
|
747
|
+
* client.setConfig({ transport: (request) => ky(request.url, { ...toRequestInit(request), throwHttpErrors: false, retry: 0, timeout: false }) })
|
|
748
|
+
* ```
|
|
506
749
|
*/
|
|
507
|
-
export function
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return undefined
|
|
750
|
+
export function toRequestInit(request: ResolvedRequest): RequestInit {
|
|
751
|
+
const init: RequestInit = {
|
|
752
|
+
...request.options,
|
|
753
|
+
method: request.method,
|
|
754
|
+
headers: request.headers,
|
|
755
|
+
body: request.body ?? null,
|
|
756
|
+
signal: request.signal ?? null,
|
|
757
|
+
}
|
|
758
|
+
if (request.credentials) init.credentials = request.credentials
|
|
759
|
+
return init
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function pickErrorValidator(error: Validator | Record<string, Validator> | undefined, status: number): Validator | undefined {
|
|
763
|
+
if (!error || '~standard' in error) return error as Validator | undefined
|
|
764
|
+
const range = String(status)[0]
|
|
765
|
+
return error[String(status)] ?? error[`${range}xx`] ?? error['default']
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Checks the shape, so a `Response` from another realm or a polyfill counts too.
|
|
769
|
+
function isResponse(value: unknown): value is Response {
|
|
770
|
+
return typeof (value as Response | undefined)?.arrayBuffer === 'function'
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Leaves out the body, which was already sent. A relative URL keeps the resolved request.
|
|
774
|
+
function toNativeRequest(request: ResolvedRequest): Request | ResolvedRequest {
|
|
775
|
+
try {
|
|
776
|
+
return new Request(request.url, { ...toRequestInit(request), body: null })
|
|
777
|
+
} catch {
|
|
778
|
+
return request
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function readAs(response: Response, parseAs: Exclude<ParseAs, 'json'>): Promise<unknown> {
|
|
783
|
+
switch (parseAs) {
|
|
784
|
+
case 'text':
|
|
785
|
+
return response.text()
|
|
786
|
+
case 'blob':
|
|
787
|
+
return response.blob()
|
|
788
|
+
case 'arrayBuffer':
|
|
789
|
+
return response.arrayBuffer()
|
|
790
|
+
case 'stream':
|
|
791
|
+
return response.body ?? undefined
|
|
792
|
+
}
|
|
521
793
|
}
|
|
522
794
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (!headers) return undefined
|
|
526
|
-
const value = headers instanceof Headers ? headers.get('Content-Type') : (headers['Content-Type'] ?? headers['content-type'])
|
|
527
|
-
return baseContentType(value)
|
|
795
|
+
function isEmpty(response: Response, method: string): boolean {
|
|
796
|
+
return [204, 205, 304].includes(response.status) || method === 'HEAD' || !response.body || response.headers.get('Content-Length') === '0'
|
|
528
797
|
}
|
|
529
798
|
|
|
530
|
-
function
|
|
799
|
+
function mergeConfig<T extends { headers?: HeadersInit; mediaTypes?: MediaTypes; serializer?: Serializers }>(config: T, next: T): T {
|
|
531
800
|
return {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
801
|
+
...config,
|
|
802
|
+
...next,
|
|
803
|
+
headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) },
|
|
804
|
+
mediaTypes: { ...config.mediaTypes, ...next.mediaTypes },
|
|
805
|
+
serializer: { ...config.serializer, ...next.serializer },
|
|
535
806
|
}
|
|
536
807
|
}
|
|
537
808
|
|
|
809
|
+
// The serializer for `name`, which `.kubb/client.ts` registers for a spec that needs it.
|
|
810
|
+
function requireSerializer<TName extends keyof Serializers>(serializers: Serializers, name: TName): NonNullable<Serializers[TName]> {
|
|
811
|
+
const serializer = serializers[name]
|
|
812
|
+
if (!serializer) throw new TypeError(`Cannot send ${name} parameters without a serializer, register serializer.${name} on the client.`)
|
|
813
|
+
return serializer
|
|
814
|
+
}
|
|
815
|
+
|
|
538
816
|
async function resolveRequest<TBody, TRequest, TResponse>({
|
|
539
817
|
config,
|
|
540
818
|
requestConfig,
|
|
541
819
|
}: {
|
|
542
820
|
config: ClientConfig<TRequest, TResponse>
|
|
543
821
|
requestConfig: RequestConfig<TBody, TRequest, TResponse>
|
|
544
|
-
}): Promise<{ request: ResolvedRequest;
|
|
545
|
-
const
|
|
546
|
-
const
|
|
822
|
+
}): Promise<{ request: ResolvedRequest; mediaTypes: MediaTypes; validate: ValidateOption | undefined; responseContentType: string | undefined }> {
|
|
823
|
+
const serializers = { ...config.serializer, ...requestConfig.serializer }
|
|
824
|
+
const mediaTypes = { ...defaultMediaTypes, ...config.mediaTypes, ...requestConfig.mediaTypes }
|
|
547
825
|
const onValidationError = requestConfig.onValidationError ?? config.onValidationError
|
|
548
826
|
const validate = requestConfig.validate ?? config.validate
|
|
549
827
|
const validationContext = { direction: 'request', method: requestConfig.method, url: requestConfig.url } as const
|
|
550
828
|
|
|
551
|
-
// Params are encoded before anything reads them: headers before styling, query before auth adds its values.
|
|
552
829
|
const paramValues = {
|
|
553
830
|
path: { ...(requestConfig.path as Record<string, unknown> | undefined) },
|
|
554
831
|
query: { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) },
|
|
555
832
|
headers: collectHeadersCaseInsensitive(config.headers, requestConfig.headers),
|
|
833
|
+
cookies: requestConfig.cookies as Record<string, unknown> | undefined,
|
|
556
834
|
}
|
|
557
|
-
const [validatedPath, validatedQuery, validatedHeaders] = await Promise.all(
|
|
558
|
-
(['path', 'query', 'headers'] as const).map((source) =>
|
|
835
|
+
const [validatedPath, validatedQuery, validatedHeaders, validatedCookies] = await Promise.all(
|
|
836
|
+
(['path', 'query', 'headers', 'cookies'] as const).map((source) =>
|
|
559
837
|
runValidator({
|
|
560
|
-
validator: isValidationEnabled(validate, 'params') ? requestConfig.validator?.[source] : undefined,
|
|
838
|
+
validator: isValidationEnabled(validate, 'params') && paramValues[source] !== undefined ? requestConfig.validator?.[source] : undefined,
|
|
561
839
|
value: paramValues[source],
|
|
562
840
|
context: { ...validationContext, source },
|
|
563
841
|
onValidationError,
|
|
@@ -565,10 +843,10 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
565
843
|
),
|
|
566
844
|
)
|
|
567
845
|
|
|
568
|
-
const
|
|
846
|
+
const headerValues = validatedHeaders as HeadersInit | undefined
|
|
847
|
+
const headers = serializeHeaders(serializers.header ? serializers.header(headerValues, requestConfig.styles?.header) : headerValues)
|
|
569
848
|
const contentType = requestConfig.contentType
|
|
570
849
|
const { request: requestContentTypeOption, response: responseContentType } = typeof contentType === 'string' ? { request: contentType } : (contentType ?? {})
|
|
571
|
-
const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
|
|
572
850
|
if (responseContentType && getHeader(headers, 'accept') === undefined) {
|
|
573
851
|
headers['Accept'] = responseContentType
|
|
574
852
|
}
|
|
@@ -582,9 +860,11 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
582
860
|
query,
|
|
583
861
|
})
|
|
584
862
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
863
|
+
const cookies = validatedCookies as Record<string, unknown> | undefined
|
|
864
|
+
// An object with only nullish values sends nothing, so it needs no serializer.
|
|
865
|
+
if (cookies && Object.values(cookies).some((value) => value !== undefined && value !== null)) {
|
|
866
|
+
const cookie = requireSerializer(serializers, 'cookie')(cookies, requestConfig.styles?.cookie)
|
|
867
|
+
if (cookie) appendCookie(headers, cookie)
|
|
588
868
|
}
|
|
589
869
|
|
|
590
870
|
const validatedBody = await runValidator({
|
|
@@ -593,129 +873,298 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
593
873
|
context: { ...validationContext, source: 'body' },
|
|
594
874
|
onValidationError,
|
|
595
875
|
})
|
|
596
|
-
const
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (key.toLowerCase() === 'content-type') delete headers[key]
|
|
610
|
-
}
|
|
876
|
+
const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type') ?? (isEncodedBody(validatedBody) ? undefined : 'application/json')
|
|
877
|
+
const body =
|
|
878
|
+
// `null` is a valid JSON body, such as a merge patch that clears a resource.
|
|
879
|
+
validatedBody === undefined
|
|
880
|
+
? undefined
|
|
881
|
+
: resolveMediaType({ mediaTypes, contentType: requestContentType, use: 'encode' })?.encode?.(validatedBody, {
|
|
882
|
+
contentType: requestContentType ?? '',
|
|
883
|
+
signal: requestConfig.signal,
|
|
884
|
+
config: requestConfig,
|
|
885
|
+
})
|
|
886
|
+
if (body !== undefined && requestContentType && getHeader(headers, 'content-type') !== requestContentType) {
|
|
887
|
+
deleteHeader(headers, 'content-type')
|
|
888
|
+
headers['Content-Type'] = requestContentType
|
|
611
889
|
}
|
|
890
|
+
// The transport sets the multipart boundary.
|
|
891
|
+
if (body instanceof FormData) deleteHeader(headers, 'content-type')
|
|
612
892
|
|
|
613
893
|
const url = serializeUrl({
|
|
614
894
|
parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
|
|
615
895
|
pathParams: (validatedPath ?? {}) as Record<string, unknown>,
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
896
|
+
query,
|
|
897
|
+
serializers,
|
|
898
|
+
styles: requestConfig.styles,
|
|
619
899
|
})
|
|
620
900
|
|
|
621
901
|
const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined
|
|
902
|
+
const accept = getHeader(headers, 'accept')
|
|
622
903
|
|
|
623
904
|
return {
|
|
624
|
-
|
|
905
|
+
mediaTypes,
|
|
625
906
|
validate,
|
|
907
|
+
responseContentType,
|
|
626
908
|
request: {
|
|
627
909
|
url,
|
|
628
910
|
method: (requestConfig.method ?? 'GET').toUpperCase(),
|
|
629
911
|
headers,
|
|
630
912
|
body,
|
|
631
|
-
signal: requestConfig.signal,
|
|
913
|
+
signal: signalOf({ signal: requestConfig.signal, options }),
|
|
632
914
|
credentials: requestConfig.credentials ?? config.credentials,
|
|
633
915
|
options,
|
|
634
|
-
|
|
916
|
+
stream:
|
|
917
|
+
requestConfig.parseAs === 'stream' || Boolean(accept?.split(',').some((type) => resolveMediaType({ mediaTypes, contentType: type, use: 'stream' }))),
|
|
635
918
|
},
|
|
636
919
|
}
|
|
637
920
|
}
|
|
638
921
|
|
|
922
|
+
// A signal in `options` counts too, since every transport spreads `options` under the resolved fields.
|
|
923
|
+
function signalOf(config: { signal?: AbortSignal | undefined; options?: unknown }): AbortSignal | undefined {
|
|
924
|
+
return config.signal ?? (config.options as { signal?: AbortSignal } | undefined)?.signal
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function* readItems({
|
|
928
|
+
items,
|
|
929
|
+
validateItem,
|
|
930
|
+
signal,
|
|
931
|
+
}: {
|
|
932
|
+
items: AsyncIterable<unknown>
|
|
933
|
+
validateItem: (item: unknown) => unknown
|
|
934
|
+
signal: AbortSignal | undefined
|
|
935
|
+
}): AsyncGenerator<unknown> {
|
|
936
|
+
try {
|
|
937
|
+
for await (const item of items) {
|
|
938
|
+
yield await validateItem(item)
|
|
939
|
+
if (signal?.aborted) throw signal.reason
|
|
940
|
+
}
|
|
941
|
+
} catch (error) {
|
|
942
|
+
// A transport reports an abort in its own error type, such as axios's `CanceledError`.
|
|
943
|
+
throw signal?.aborted ? signal.reason : error
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
type InterceptorRuns<TRequest, TResponse> = {
|
|
948
|
+
request: (value: ResolvedRequest, context?: RequestConfig<unknown, TRequest, TResponse>) => Promise<ResolvedRequest>
|
|
949
|
+
response: (
|
|
950
|
+
value: DecodedResponse<unknown, TRequest, TResponse>,
|
|
951
|
+
context?: RequestConfig<unknown, TRequest, TResponse>,
|
|
952
|
+
) => Promise<DecodedResponse<unknown, TRequest, TResponse>>
|
|
953
|
+
error: (value: Error, context?: RequestConfig<unknown, TRequest, TResponse>) => Promise<Error>
|
|
954
|
+
}
|
|
955
|
+
|
|
639
956
|
/**
|
|
640
|
-
* Creates
|
|
957
|
+
* Creates a client that sends through `defaultTransport`.
|
|
641
958
|
*/
|
|
642
959
|
export function createClientCore<TRequest = NativeRequest, TResponse = NativeResponse>(
|
|
643
|
-
options: { defaultTransport: Transport<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
|
|
960
|
+
options: { defaultTransport: Transport<TRequest, TResponse>; parent?: InterceptorRuns<TRequest, TResponse> } & ClientConfig<TRequest, TResponse>,
|
|
644
961
|
): ClientInstance<TRequest, TResponse> {
|
|
645
|
-
const { defaultTransport, ...initialConfig } = options
|
|
962
|
+
const { defaultTransport, parent, ...initialConfig } = options
|
|
646
963
|
let config: ClientConfig<TRequest, TResponse> = { ...initialConfig }
|
|
647
964
|
|
|
648
|
-
const
|
|
649
|
-
request: createInterceptorStack
|
|
650
|
-
response: createInterceptorStack
|
|
651
|
-
error: createInterceptorStack
|
|
965
|
+
const own: Interceptors<TRequest, TResponse> = {
|
|
966
|
+
request: createInterceptorStack(),
|
|
967
|
+
response: createInterceptorStack(),
|
|
968
|
+
error: createInterceptorStack(),
|
|
969
|
+
}
|
|
970
|
+
const run: InterceptorRuns<TRequest, TResponse> = {
|
|
971
|
+
request: async (value, context) => own.request.run(parent ? await parent.request(value, context) : value, context),
|
|
972
|
+
response: async (value, context) => own.response.run(parent ? await parent.response(value, context) : value, context),
|
|
973
|
+
error: async (value, context) => own.error.run(parent ? await parent.error(value, context) : value, context),
|
|
652
974
|
}
|
|
653
975
|
|
|
654
|
-
|
|
976
|
+
async function send(requestConfig: RequestConfig<unknown, TRequest, TResponse> & EventStreamOptions, streaming: boolean) {
|
|
655
977
|
const transport = requestConfig.transport ?? config.transport ?? defaultTransport
|
|
656
|
-
|
|
978
|
+
// Read before the first await, so a `setConfig` during the call does not change it.
|
|
979
|
+
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
980
|
+
const onValidationError = requestConfig.onValidationError ?? config.onValidationError
|
|
981
|
+
const { request, mediaTypes, validate, responseContentType } = await resolveRequest({ config, requestConfig })
|
|
982
|
+
if (streaming) {
|
|
983
|
+
request.stream = true
|
|
984
|
+
// `client.stream` without a response content type would otherwise ask for the default, often JSON.
|
|
985
|
+
if (getHeader(request.headers, 'accept') === undefined) request.headers['Accept'] = 'text/event-stream'
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const resolvedRequest = await run.request(request, requestConfig)
|
|
989
|
+
const sent = await transport(resolvedRequest)
|
|
990
|
+
const {
|
|
991
|
+
webResponse,
|
|
992
|
+
request: nativeRequest,
|
|
993
|
+
response: nativeResponse,
|
|
994
|
+
} = isResponse(sent) ? { webResponse: sent, request: toNativeRequest(resolvedRequest) as TRequest, response: sent as TResponse & Response } : sent
|
|
995
|
+
|
|
996
|
+
const status = webResponse.status
|
|
997
|
+
const isSuccess = status >= 200 && status < 300
|
|
998
|
+
const header = webResponse.headers.get('Content-Type')
|
|
999
|
+
const contentType = header ?? (isSuccess ? responseContentType : undefined) ?? '*/*'
|
|
1000
|
+
const cursor = { lastEventId: '' }
|
|
1001
|
+
const context: MediaTypeContext = { contentType, status, signal: resolvedRequest.signal, config: requestConfig, cursor }
|
|
1002
|
+
const streamType = isSuccess ? resolveMediaType({ mediaTypes, contentType, use: 'stream' }) : undefined
|
|
1003
|
+
const decode = async (): Promise<unknown> => {
|
|
1004
|
+
if (isEmpty(webResponse, resolvedRequest.method)) return undefined
|
|
1005
|
+
if (streamType) return webResponse.body
|
|
1006
|
+
const parseAs = isSuccess ? requestConfig.parseAs : undefined
|
|
1007
|
+
if (parseAs && parseAs !== 'json') return readAs(webResponse, parseAs)
|
|
1008
|
+
// `parseAs: 'json'` uses the registered JSON handler, so a lenient one applies.
|
|
1009
|
+
return resolveMediaType({ mediaTypes, contentType: parseAs === 'json' ? 'application/json' : contentType, use: 'decode' })?.decode?.(webResponse, context)
|
|
1010
|
+
}
|
|
657
1011
|
|
|
658
|
-
const
|
|
659
|
-
|
|
1012
|
+
const result = await run.response(
|
|
1013
|
+
{
|
|
1014
|
+
data: await decode(),
|
|
1015
|
+
status,
|
|
1016
|
+
statusText: webResponse.statusText,
|
|
1017
|
+
headers: webResponse.headers,
|
|
1018
|
+
contentType: baseContentType(header),
|
|
1019
|
+
request: nativeRequest,
|
|
1020
|
+
response: nativeResponse,
|
|
1021
|
+
},
|
|
1022
|
+
requestConfig,
|
|
1023
|
+
)
|
|
660
1024
|
|
|
661
|
-
const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true
|
|
662
1025
|
const validator = requestConfig.validator
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
const
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const
|
|
1026
|
+
const resultSuccess = result.status >= 200 && result.status < 300
|
|
1027
|
+
const direction = resultSuccess ? 'response' : 'error'
|
|
1028
|
+
const validationContext = { direction, source: 'body', method: resolvedRequest.method, url: resolvedRequest.url, status: result.status } as const
|
|
1029
|
+
const validatorFor = isValidationEnabled(validate, direction)
|
|
1030
|
+
? resultSuccess
|
|
1031
|
+
? validator?.response
|
|
1032
|
+
: pickErrorValidator(validator?.error, result.status)
|
|
1033
|
+
: undefined
|
|
1034
|
+
const rawStream = result.data instanceof ReadableStream
|
|
1035
|
+
const readsItems = streaming && resultSuccess && streamType?.stream !== undefined && rawStream
|
|
1036
|
+
|
|
1037
|
+
if (streaming && resultSuccess && !readsItems && result.data != null) {
|
|
1038
|
+
// Release the connection, since nobody reads this body.
|
|
1039
|
+
if (rawStream) await (result.data as ReadableStream).cancel().catch(() => {})
|
|
1040
|
+
// `cause` is set afterwards, since the constructor option needs the ES2022 library.
|
|
1041
|
+
const error: TypeError & { cause?: unknown } = new TypeError(`Expected a streamed body, got ${result.contentType ?? 'a body without a content type'}`)
|
|
1042
|
+
error.cause = result.data
|
|
1043
|
+
throw error
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const data = rawStream ? result.data : await runValidator({ validator: validatorFor, value: result.data, context: validationContext, onValidationError })
|
|
1047
|
+
if (!resultSuccess && throwOnError) {
|
|
1048
|
+
throw new ResponseError({
|
|
679
1049
|
data,
|
|
680
1050
|
status: result.status,
|
|
681
1051
|
statusText: result.statusText,
|
|
682
|
-
contentType,
|
|
1052
|
+
contentType: result.contentType,
|
|
1053
|
+
headers: result.headers,
|
|
683
1054
|
request: result.request,
|
|
684
1055
|
response: result.response,
|
|
685
1056
|
method: resolvedRequest.method,
|
|
686
1057
|
url: resolvedRequest.url,
|
|
687
1058
|
})
|
|
688
|
-
await interceptors.error.run(responseError, requestConfig)
|
|
689
|
-
throw responseError
|
|
690
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
const validateItem = async (item: unknown) => {
|
|
1062
|
+
if (!validatorFor) return item
|
|
1063
|
+
const validate = (value: unknown) => runValidator({ validator: validatorFor, value, context: validationContext, onValidationError })
|
|
1064
|
+
if (requestConfig.eventShape === 'data') {
|
|
1065
|
+
const event = item as ServerSentEvent
|
|
1066
|
+
return { ...event, data: await validate(event.data) }
|
|
1067
|
+
}
|
|
1068
|
+
// Merged over the event, since the schema may not declare `id` and `retry`.
|
|
1069
|
+
if (requestConfig.eventShape === 'event') return { ...(item as object), ...((await validate(item)) as object) }
|
|
1070
|
+
return validate(item)
|
|
1071
|
+
}
|
|
1072
|
+
const items = readsItems && streamType?.stream && result.data instanceof ReadableStream ? streamType.stream(result.data, context) : undefined
|
|
691
1073
|
return {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1074
|
+
cursor,
|
|
1075
|
+
items: readItems({ items: items ?? emptyItems(), validateItem, signal: resolvedRequest.signal }),
|
|
1076
|
+
result: {
|
|
1077
|
+
status: result.status,
|
|
1078
|
+
data: resultSuccess ? data : undefined,
|
|
1079
|
+
error: resultSuccess
|
|
1080
|
+
? undefined
|
|
1081
|
+
: data == null || data === ''
|
|
1082
|
+
? ({ status: result.status, statusText: result.statusText } satisfies EmptyErrorBody)
|
|
1083
|
+
: data,
|
|
1084
|
+
contentType: result.contentType,
|
|
1085
|
+
headers: result.headers,
|
|
1086
|
+
request: result.request,
|
|
1087
|
+
response: result.response,
|
|
1088
|
+
},
|
|
698
1089
|
}
|
|
699
|
-
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function call<T>(requestConfig: RequestConfig<unknown, TRequest, TResponse>, work: () => Promise<T>): Promise<T> {
|
|
1093
|
+
const signal = signalOf(requestConfig)
|
|
1094
|
+
try {
|
|
1095
|
+
return await work()
|
|
1096
|
+
} catch (caught) {
|
|
1097
|
+
const error: unknown = signal?.aborted ? signal.reason : caught
|
|
1098
|
+
throw error instanceof Error ? await run.error(error, requestConfig) : error
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const client = (<TResponses, ThrowOnError extends boolean, THeaders>(requestConfig: RequestConfig<unknown, TRequest, TResponse>) =>
|
|
1103
|
+
call(
|
|
1104
|
+
requestConfig,
|
|
1105
|
+
async () => (await send(requestConfig, false)).result as RequestResult<TResponses, ThrowOnError, TRequest, TResponse, THeaders>,
|
|
1106
|
+
)) as ClientInstance<TRequest, TResponse>
|
|
1107
|
+
|
|
1108
|
+
client.stream = <TResponses, ThrowOnError extends boolean, TItem, THeaders>(
|
|
1109
|
+
requestConfig: RequestConfig<unknown, TRequest, TResponse> & EventStreamOptions,
|
|
1110
|
+
) =>
|
|
1111
|
+
call(requestConfig, async () => {
|
|
1112
|
+
const { result, items, cursor } = await send(requestConfig, true)
|
|
1113
|
+
const stream = items as AsyncGenerator<TItem>
|
|
1114
|
+
const streamed = {
|
|
1115
|
+
...result,
|
|
1116
|
+
stream,
|
|
1117
|
+
data: result.error === undefined ? stream : undefined,
|
|
1118
|
+
get lastEventId() {
|
|
1119
|
+
return cursor.lastEventId
|
|
1120
|
+
},
|
|
1121
|
+
[Symbol.asyncIterator]: () => stream,
|
|
1122
|
+
}
|
|
1123
|
+
return streamed as unknown as StreamResult<TResponses, ThrowOnError, TItem, TRequest, TResponse, THeaders>
|
|
1124
|
+
})
|
|
700
1125
|
|
|
701
1126
|
client.getConfig = () => config
|
|
702
1127
|
client.setConfig = (next) => {
|
|
703
|
-
config =
|
|
1128
|
+
config = mergeConfig(config, next)
|
|
704
1129
|
return config
|
|
705
1130
|
}
|
|
706
1131
|
client.getUrl = (requestConfig) => {
|
|
707
|
-
const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
|
|
708
1132
|
const query: Record<string, unknown> = { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) }
|
|
709
1133
|
return serializeUrl({
|
|
710
1134
|
parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
|
|
711
1135
|
pathParams: (requestConfig.path ?? {}) as Record<string, unknown>,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1136
|
+
query,
|
|
1137
|
+
serializers: { ...config.serializer, ...requestConfig.serializer },
|
|
1138
|
+
styles: requestConfig.styles,
|
|
715
1139
|
})
|
|
716
1140
|
}
|
|
717
|
-
client.interceptors =
|
|
718
|
-
client.createClient = (next) => createClientCore({ defaultTransport, ...config,
|
|
1141
|
+
client.interceptors = own
|
|
1142
|
+
client.createClient = (next = {}) => createClientCore({ defaultTransport, ...mergeConfig(config, next) })
|
|
1143
|
+
client.extend = (next = {}) => createClientCore({ defaultTransport, parent: run, ...mergeConfig(config, next) })
|
|
719
1144
|
|
|
720
1145
|
return client
|
|
721
1146
|
}
|
|
1147
|
+
|
|
1148
|
+
async function* emptyItems(): AsyncGenerator<unknown> {}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* A client created on first use, so importing an operation runs no setup. Config set before that is passed to `create`.
|
|
1152
|
+
*/
|
|
1153
|
+
export function createLazyClient<TRequest = NativeRequest, TResponse = NativeResponse>(
|
|
1154
|
+
create: (config: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>,
|
|
1155
|
+
): ClientInstance<TRequest, TResponse> {
|
|
1156
|
+
let instance: ClientInstance<TRequest, TResponse> | undefined
|
|
1157
|
+
let pending: ClientConfig<TRequest, TResponse> = {}
|
|
1158
|
+
const get = () => (instance ??= create(pending))
|
|
1159
|
+
|
|
1160
|
+
const client = (<TResponses, ThrowOnError extends boolean>(requestConfig: RequestConfig<unknown, TRequest, TResponse> & { throwOnError?: ThrowOnError }) =>
|
|
1161
|
+
get()<TResponses, ThrowOnError>(requestConfig)) as ClientInstance<TRequest, TResponse>
|
|
1162
|
+
client.stream = (requestConfig) => get().stream(requestConfig)
|
|
1163
|
+
client.getConfig = () => instance?.getConfig() ?? pending
|
|
1164
|
+
client.setConfig = (next) => (instance ? instance.setConfig(next) : (pending = mergeConfig(pending, next)))
|
|
1165
|
+
client.getUrl = (requestConfig) => get().getUrl(requestConfig)
|
|
1166
|
+
client.createClient = (next) => get().createClient(next)
|
|
1167
|
+
client.extend = (next) => get().extend(next)
|
|
1168
|
+
Object.defineProperty(client, 'interceptors', { get: () => get().interceptors, enumerable: true })
|
|
1169
|
+
return client
|
|
1170
|
+
}
|